sbuilder-mcp 0.36.1 → 0.38.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 +19 -0
- package/CHANGELOG.vi.md +19 -0
- package/dist/catalog/element-search.js +4 -2
- package/dist/catalog/elements.generated.js +227 -9
- package/dist/catalog/translations.generated.js +4 -1
- package/dist/domains/site/inert.js +13 -0
- package/dist/domains/site/vocabulary.js +114 -7
- package/dist/tools/importpage.js +18 -3
- package/dist/vision/capture.js +58 -6
- package/dist/vision/shoot.js +9 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,25 @@ 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.38.0] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- The catalog gained a 112th element, spline-scene: an interactive 3D scene embedded from a Spline viewer link, seeded by default with a real scene from Spline's own demo project, with a warning at creation time telling the caller to replace it with the merchant's own export before publishing.
|
|
13
|
+
- config.animation grew from five keys to ten (intensity, trigger, range, repeat, and alternate join active, type, easing, delay, and duration), including a new `trigger: "view"` for reveal-on-scroll animation, compiled as a pure-CSS animation-timeline override with no added script.
|
|
14
|
+
- sb_traits_for now returns an animation_values field listing every legal value for each config.animation key (all 46 types, 7 easings, every intensity and trigger) for any element that carries an animation control.
|
|
15
|
+
- sb_set now warns when config.animation combines alternate:true with a finite repeat count, since that combination publishes a node that finishes on its opacity:0 keyframe and is invisible to every visitor, and when intensity or trigger is set to a value the platform does not recognize.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- config.animation is no longer base-only: sb_set now writes it per breakpoint like any other config key, instead of always routing it to base, so an animation can now be turned off on mobile.
|
|
19
|
+
|
|
20
|
+
## [0.37.0] - 2026-09-11
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- sb_import and sb_import_site now take a `nav_timeout_ms` argument (5,000-120,000ms, default 30,000) to raise how long a capture waits for a slow origin to answer at all.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- sb_import_site's timeout failure now reports the number of seconds the page actually waited and suggests raising `nav_timeout_ms`, instead of always naming the unmodified default even after a caller had already raised it.
|
|
27
|
+
|
|
9
28
|
## [0.36.1] - 2026-09-11
|
|
10
29
|
|
|
11
30
|
- fix(import,api): a capture waited on iframes, and a recovery list was unreadable
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,25 @@ 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.38.0] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Catalog có thêm element thứ 112, spline-scene: một cảnh 3D tương tác được nhúng từ link viewer của Spline, mặc định được gán sẵn một cảnh thật từ project demo của chính Spline, kèm cảnh báo ngay lúc tạo yêu cầu caller thay bằng file export của merchant trước khi publish.
|
|
13
|
+
- config.animation tăng từ năm key lên mười key (intensity, trigger, range, repeat và alternate được thêm bên cạnh active, type, easing, delay và duration), trong đó có `trigger: "view"` mới cho hiệu ứng xuất hiện khi cuộn trang, được biên dịch thành một override CSS thuần bằng animation-timeline, không cần thêm script.
|
|
14
|
+
- sb_traits_for giờ trả về thêm trường animation_values liệt kê mọi giá trị hợp lệ cho từng key của config.animation (đủ 46 type, 7 easing, mọi intensity và trigger) với bất kỳ element nào có control animation.
|
|
15
|
+
- sb_set giờ cảnh báo khi config.animation kết hợp alternate:true với một số lần repeat hữu hạn, vì tổ hợp này publish một node kết thúc ở keyframe opacity:0 và không visitor nào nhìn thấy được, cũng như khi intensity hoặc trigger được đặt một giá trị mà nền tảng không nhận ra.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- config.animation không còn base-only nữa: sb_set giờ ghi giá trị này theo từng breakpoint như mọi config key khác, thay vì luôn dồn về base, nên giờ có thể tắt animation riêng trên mobile.
|
|
19
|
+
|
|
20
|
+
## [0.37.0] - 2026-09-11
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- sb_import và sb_import_site giờ nhận thêm tham số `nav_timeout_ms` (5.000-120.000ms, mặc định 30.000) để tăng thời gian chờ trang trả lời khi nguồn phản hồi chậm.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Lỗi timeout của sb_import_site giờ báo đúng số giây trang thực sự đã chờ và gợi ý tăng `nav_timeout_ms`, thay vì luôn nêu giá trị mặc định chưa đổi ngay cả khi caller đã tự tăng nó.
|
|
27
|
+
|
|
9
28
|
## [0.36.0] - 2026-09-11
|
|
10
29
|
|
|
11
30
|
### Added
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ELEMENTS, TRAIT_WRITES } from './elements.generated.js';
|
|
2
|
-
import { animationVocabulary, vocabulariesForWrites } from '../domains/site/vocabulary.js';
|
|
2
|
+
import { animationValues, animationVocabulary, vocabulariesForWrites } from '../domains/site/vocabulary.js';
|
|
3
3
|
import { neverTranslatedOn, translatableSpecials } from '../domains/site/translate.js';
|
|
4
4
|
/** Eight to choose from; the hints for the chosen one come with sb_traits_for. */
|
|
5
5
|
export const DEFAULT_CATALOG_LIMIT = 8;
|
|
@@ -130,7 +130,9 @@ export function traitsFor(type, control) {
|
|
|
130
130
|
// with a required gate, and the three ways to get it wrong all render
|
|
131
131
|
// NOTHING rather than something else. An element that does not offer the
|
|
132
132
|
// control says nothing, so this is silent on the other 38.
|
|
133
|
-
...(el.controls.includes('animation')
|
|
133
|
+
...(el.controls.includes('animation')
|
|
134
|
+
? { animation: animationVocabulary(), animation_values: animationValues() }
|
|
135
|
+
: {}),
|
|
134
136
|
isContainer: el.isContainer,
|
|
135
137
|
isRootOnly: el.isRootOnly,
|
|
136
138
|
childAllows: el.childAllows,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const ELEMENT_SOURCE = {
|
|
2
|
-
"count":
|
|
2
|
+
"count": 112,
|
|
3
3
|
"docSchemaVersion": 2
|
|
4
4
|
};
|
|
5
5
|
export const ELEMENTS = {
|
|
@@ -3686,6 +3686,146 @@ export const ELEMENTS = {
|
|
|
3686
3686
|
"player"
|
|
3687
3687
|
]
|
|
3688
3688
|
},
|
|
3689
|
+
"spline-scene": {
|
|
3690
|
+
"type": "spline-scene",
|
|
3691
|
+
"label": "Spline scene",
|
|
3692
|
+
"category": "media",
|
|
3693
|
+
"isContainer": false,
|
|
3694
|
+
"isRootOnly": false,
|
|
3695
|
+
"locked": false,
|
|
3696
|
+
"hideInLayer": false,
|
|
3697
|
+
"childAllows": [],
|
|
3698
|
+
"defaults": {
|
|
3699
|
+
"specials": {
|
|
3700
|
+
"sceneUrl": "https://prod.spline.design/HqdfCmOueigtautT/scene.splinecode",
|
|
3701
|
+
"posterUrl": "",
|
|
3702
|
+
"sceneControls": []
|
|
3703
|
+
},
|
|
3704
|
+
"style": {
|
|
3705
|
+
"width": "100%",
|
|
3706
|
+
"height": "480px"
|
|
3707
|
+
},
|
|
3708
|
+
"config": {
|
|
3709
|
+
"background": "transparent",
|
|
3710
|
+
"eventsTarget": "local",
|
|
3711
|
+
"mobileMode": "scene"
|
|
3712
|
+
}
|
|
3713
|
+
},
|
|
3714
|
+
"inspector": [
|
|
3715
|
+
{
|
|
3716
|
+
"tab": "general",
|
|
3717
|
+
"groups": [
|
|
3718
|
+
{
|
|
3719
|
+
"key": "size",
|
|
3720
|
+
"label": "Size",
|
|
3721
|
+
"controls": [
|
|
3722
|
+
"width_select",
|
|
3723
|
+
"size_bounds"
|
|
3724
|
+
]
|
|
3725
|
+
},
|
|
3726
|
+
{
|
|
3727
|
+
"key": "spline",
|
|
3728
|
+
"label": "Spline",
|
|
3729
|
+
"controls": [
|
|
3730
|
+
"spline_source"
|
|
3731
|
+
]
|
|
3732
|
+
},
|
|
3733
|
+
{
|
|
3734
|
+
"key": "scene_display",
|
|
3735
|
+
"label": "Display",
|
|
3736
|
+
"controls": [
|
|
3737
|
+
"scene_display"
|
|
3738
|
+
]
|
|
3739
|
+
},
|
|
3740
|
+
{
|
|
3741
|
+
"key": "scene_controls",
|
|
3742
|
+
"label": "Scene controls",
|
|
3743
|
+
"controls": [
|
|
3744
|
+
"scene_controls"
|
|
3745
|
+
]
|
|
3746
|
+
},
|
|
3747
|
+
{
|
|
3748
|
+
"key": "shape",
|
|
3749
|
+
"label": "Shape",
|
|
3750
|
+
"controls": [
|
|
3751
|
+
"border",
|
|
3752
|
+
"corner",
|
|
3753
|
+
"shadow"
|
|
3754
|
+
]
|
|
3755
|
+
}
|
|
3756
|
+
]
|
|
3757
|
+
},
|
|
3758
|
+
{
|
|
3759
|
+
"tab": "advanced",
|
|
3760
|
+
"groups": [
|
|
3761
|
+
{
|
|
3762
|
+
"key": "spacing",
|
|
3763
|
+
"label": "Spacing",
|
|
3764
|
+
"controls": [
|
|
3765
|
+
"padding_margin"
|
|
3766
|
+
]
|
|
3767
|
+
},
|
|
3768
|
+
{
|
|
3769
|
+
"key": "display",
|
|
3770
|
+
"label": "Display",
|
|
3771
|
+
"controls": [
|
|
3772
|
+
"display"
|
|
3773
|
+
]
|
|
3774
|
+
},
|
|
3775
|
+
{
|
|
3776
|
+
"key": "animation",
|
|
3777
|
+
"label": "Animation",
|
|
3778
|
+
"controls": [
|
|
3779
|
+
"animation"
|
|
3780
|
+
]
|
|
3781
|
+
},
|
|
3782
|
+
{
|
|
3783
|
+
"key": "class_css",
|
|
3784
|
+
"label": "Class",
|
|
3785
|
+
"controls": [
|
|
3786
|
+
"class_css"
|
|
3787
|
+
]
|
|
3788
|
+
}
|
|
3789
|
+
]
|
|
3790
|
+
}
|
|
3791
|
+
],
|
|
3792
|
+
"controls": [
|
|
3793
|
+
"width_select",
|
|
3794
|
+
"size_bounds",
|
|
3795
|
+
"spline_source",
|
|
3796
|
+
"scene_display",
|
|
3797
|
+
"scene_controls",
|
|
3798
|
+
"border",
|
|
3799
|
+
"corner",
|
|
3800
|
+
"shadow",
|
|
3801
|
+
"padding_margin",
|
|
3802
|
+
"display",
|
|
3803
|
+
"animation",
|
|
3804
|
+
"class_css"
|
|
3805
|
+
],
|
|
3806
|
+
"description": "An interactive 3D scene made in Spline, embedded from its viewer URL.",
|
|
3807
|
+
"useWhen": [
|
|
3808
|
+
"For a hero or product moment that should move in 3D as the visitor scrolls or hovers",
|
|
3809
|
+
"When the merchant already designs in Spline and has a .splinecode link"
|
|
3810
|
+
],
|
|
3811
|
+
"avoidWhen": [
|
|
3812
|
+
"For a plain product photo — use image",
|
|
3813
|
+
"For a video — use video / youtube / vimeo",
|
|
3814
|
+
"More than three 3D scenes on one page (each one costs ~600 KB of JS once it scrolls into view)"
|
|
3815
|
+
],
|
|
3816
|
+
"contentTips": [
|
|
3817
|
+
"Paste the link from Spline: Export → Viewer → copy link (…/scene.splinecode)",
|
|
3818
|
+
"Add a poster image so the box is not blank while the scene loads"
|
|
3819
|
+
],
|
|
3820
|
+
"semantics": [
|
|
3821
|
+
"3d",
|
|
3822
|
+
"spline",
|
|
3823
|
+
"scene",
|
|
3824
|
+
"webgl",
|
|
3825
|
+
"animation",
|
|
3826
|
+
"interactive"
|
|
3827
|
+
]
|
|
3828
|
+
},
|
|
3689
3829
|
"vimeo": {
|
|
3690
3830
|
"type": "vimeo",
|
|
3691
3831
|
"label": "Vimeo",
|
|
@@ -36195,7 +36335,6 @@ export const BASE_ONLY_CONFIG = [
|
|
|
36195
36335
|
"collectionId",
|
|
36196
36336
|
"collectionType",
|
|
36197
36337
|
"quantity",
|
|
36198
|
-
"animation",
|
|
36199
36338
|
"rowLimit"
|
|
36200
36339
|
];
|
|
36201
36340
|
/**
|
|
@@ -36207,20 +36346,38 @@ export const BASE_ONLY_EXCEPTIONS = [
|
|
|
36207
36346
|
"quantity-button:iconSize"
|
|
36208
36347
|
];
|
|
36209
36348
|
/**
|
|
36210
|
-
* The ENTRANCE ANIMATION's vocabulary — config.animation, offered by
|
|
36211
|
-
*
|
|
36349
|
+
* The ENTRANCE ANIMATION's vocabulary — config.animation, offered by most of
|
|
36350
|
+
* the element library and describable by nothing until this table existed.
|
|
36212
36351
|
*
|
|
36213
36352
|
* Three ways to miss, all silent (AnimationTypeOf answers "" and no keyframes,
|
|
36214
36353
|
* no rule and no error are emitted, through save, publish and render):
|
|
36215
|
-
* - it is an OBJECT, not a string
|
|
36354
|
+
* - it is an OBJECT, not a string
|
|
36216
36355
|
* - active:true is REQUIRED; a stored type is deliberately NOT consent,
|
|
36217
36356
|
* because the panel keeps the type when the switch goes off
|
|
36218
36357
|
* - type is a keyframe key spelled with UNDERSCORES: fade_in, never fade-in
|
|
36219
36358
|
*
|
|
36220
36359
|
* easing is the mild one: an unrecognised value falls back to "ease".
|
|
36221
36360
|
*
|
|
36222
|
-
*
|
|
36223
|
-
*
|
|
36361
|
+
* TWO THINGS THIS TABLE USED TO SAY THAT ARE NO LONGER TRUE, kept as a
|
|
36362
|
+
* correction because both were recorded here as settled facts:
|
|
36363
|
+
*
|
|
36364
|
+
* - IT IS NOT BASE-ONLY ANY MORE. The compiler reads config through
|
|
36365
|
+
* MergeNamespace and emits per lane, so the key left the platform's
|
|
36366
|
+
* base-only ledger — and the side effect is the thing merchants ask for
|
|
36367
|
+
* most, an animation that is off on mobile. A caller still writing it to
|
|
36368
|
+
* base gets the cascade's fallback layer, which is correct but cannot vary.
|
|
36369
|
+
* - REVEAL-ON-SCROLL HAS AN ANSWER. trigger:"view" compiles to
|
|
36370
|
+
* animation-timeline: view() inside an @supports override, so it costs no
|
|
36371
|
+
* JavaScript and the engines without it keep animating at first paint.
|
|
36372
|
+
*
|
|
36373
|
+
* The object now carries ten keys. intensity travels as CSS variables (a
|
|
36374
|
+
* distance is a quantity, so it reaches the page per breakpoint) and ABSENCE IS
|
|
36375
|
+
* NOT medium — a document with no intensity keeps the old 0.5s fallback.
|
|
36376
|
+
*
|
|
36377
|
+
* alternateNeedsInfinite is the guard worth reading before using repeat:
|
|
36378
|
+
* alternate with an EVEN finite count finishes on the from keyframe, and every
|
|
36379
|
+
* entrance keyframe starts at opacity:0 — so the node publishes INVISIBLE. The
|
|
36380
|
+
* compiler honours alternate only alongside an infinite repeat.
|
|
36224
36381
|
*/
|
|
36225
36382
|
/**
|
|
36226
36383
|
* The eight node-style keys a THEME TEXT STYLE controls, and the var prop each
|
|
@@ -36237,18 +36394,79 @@ export const BASE_ONLY_EXCEPTIONS = [
|
|
|
36237
36394
|
export const TEXT_STYLE_KEYS = [["fontFamily", "family"], ["fontSize", "size"], ["fontWeight", "weight"], ["fontStyle", "style"], ["lineHeight", "line-height"], ["letterSpacing", "letter-spacing"], ["textTransform", "transform"], ["color", "color"]];
|
|
36238
36395
|
export const ANIMATION = {
|
|
36239
36396
|
"types": [
|
|
36397
|
+
"blur_in",
|
|
36398
|
+
"bounce",
|
|
36399
|
+
"bounce_in",
|
|
36400
|
+
"elastic_in",
|
|
36240
36401
|
"fade_in",
|
|
36402
|
+
"fade_in_bottom_left",
|
|
36403
|
+
"fade_in_bottom_right",
|
|
36404
|
+
"fade_in_down",
|
|
36405
|
+
"fade_in_left",
|
|
36406
|
+
"fade_in_right",
|
|
36407
|
+
"fade_in_top_left",
|
|
36408
|
+
"fade_in_top_right",
|
|
36409
|
+
"fade_in_up",
|
|
36410
|
+
"flash",
|
|
36411
|
+
"flip",
|
|
36412
|
+
"flip_in_x",
|
|
36413
|
+
"flip_in_y",
|
|
36414
|
+
"head_shake",
|
|
36415
|
+
"heart_beat",
|
|
36416
|
+
"jack_in_the_box",
|
|
36417
|
+
"jello",
|
|
36418
|
+
"light_speed_in_left",
|
|
36419
|
+
"light_speed_in_right",
|
|
36420
|
+
"pulse",
|
|
36421
|
+
"roll_in",
|
|
36422
|
+
"rotate_in",
|
|
36423
|
+
"rotate_in_down_left",
|
|
36424
|
+
"rotate_in_down_right",
|
|
36425
|
+
"rotate_in_up_left",
|
|
36426
|
+
"rotate_in_up_right",
|
|
36427
|
+
"rubber_band",
|
|
36428
|
+
"shake_x",
|
|
36429
|
+
"shake_y",
|
|
36241
36430
|
"slide_down",
|
|
36431
|
+
"slide_in_down",
|
|
36432
|
+
"slide_in_left",
|
|
36433
|
+
"slide_in_right",
|
|
36434
|
+
"slide_in_up",
|
|
36435
|
+
"slide_left",
|
|
36436
|
+
"slide_right",
|
|
36242
36437
|
"slide_up",
|
|
36243
|
-
"
|
|
36438
|
+
"swing",
|
|
36439
|
+
"tada",
|
|
36440
|
+
"wobble",
|
|
36441
|
+
"zoom_in",
|
|
36442
|
+
"zoom_out"
|
|
36244
36443
|
],
|
|
36245
36444
|
"easings": [
|
|
36246
36445
|
"ease",
|
|
36247
36446
|
"ease-in",
|
|
36248
36447
|
"ease-out",
|
|
36249
|
-
"
|
|
36448
|
+
"ease-in-out",
|
|
36449
|
+
"linear",
|
|
36450
|
+
"spring",
|
|
36451
|
+
"bounce"
|
|
36250
36452
|
],
|
|
36251
36453
|
"easingFallback": "ease",
|
|
36252
36454
|
"durationDefault": 0.5,
|
|
36455
|
+
"intensities": [
|
|
36456
|
+
"soft",
|
|
36457
|
+
"medium",
|
|
36458
|
+
"strong"
|
|
36459
|
+
],
|
|
36460
|
+
"intensityDurations": {
|
|
36461
|
+
"soft": 0.4,
|
|
36462
|
+
"medium": 0.6,
|
|
36463
|
+
"strong": 0.9
|
|
36464
|
+
},
|
|
36465
|
+
"triggers": [
|
|
36466
|
+
"view"
|
|
36467
|
+
],
|
|
36468
|
+
"rangeDefault": 60,
|
|
36469
|
+
"repeatMax": 100,
|
|
36470
|
+
"alternateNeedsInfinite": true,
|
|
36253
36471
|
"readBy": "AnimationTypeOf + CompileEntranceAnimationCSS"
|
|
36254
36472
|
};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
export const TRANSLATION_SOURCE = {
|
|
4
4
|
"elements": 58,
|
|
5
5
|
"pairs": 141,
|
|
6
|
-
"neverKeys":
|
|
6
|
+
"neverKeys": 159,
|
|
7
7
|
"entityTypes": 12
|
|
8
8
|
};
|
|
9
9
|
/** Every entity type a translation record can name, including "node". */
|
|
@@ -402,6 +402,7 @@ export const NEVER_TRANSLATED = [
|
|
|
402
402
|
"payload",
|
|
403
403
|
"picker",
|
|
404
404
|
"poster",
|
|
405
|
+
"posterUrl",
|
|
405
406
|
"prefillValue",
|
|
406
407
|
"presetCode",
|
|
407
408
|
"provinceField",
|
|
@@ -409,6 +410,8 @@ export const NEVER_TRANSLATED = [
|
|
|
409
410
|
"required",
|
|
410
411
|
"reviewForm",
|
|
411
412
|
"sameDefault",
|
|
413
|
+
"sceneControls",
|
|
414
|
+
"sceneUrl",
|
|
412
415
|
"searchBehavior",
|
|
413
416
|
"searchClearSwap",
|
|
414
417
|
"searchDebounceMs",
|
|
@@ -26,6 +26,19 @@ export const INERT_ON_ADD = {
|
|
|
26
26
|
'store that is not in English, set it (e.g. "Trang chủ") or the trail reads half-translated ' +
|
|
27
27
|
'on every product page.',
|
|
28
28
|
},
|
|
29
|
+
// THE SHARPEST OF THE THREE, because the placeholder is not a placeholder: it
|
|
30
|
+
// is a REAL scene on Spline's own servers, and it loads, renders and responds
|
|
31
|
+
// to the mouse. The other two entries here describe an element that looks
|
|
32
|
+
// finished; this one looks finished AND is somebody else's work, published on
|
|
33
|
+
// the merchant's domain. `sb_review` reads a correct tree, `sb_look`
|
|
34
|
+
// photographs a convincing 3D hero, and nothing anywhere says whose it is.
|
|
35
|
+
'spline-scene': {
|
|
36
|
+
note: "A spline-scene is born pointing at SPLINE'S OWN DEMO SCENE (specials.sceneUrl defaults to " +
|
|
37
|
+
'a real prod.spline.design link), so an unset one publishes a 3D hero that loads, moves, ' +
|
|
38
|
+
"and belongs to somebody else — it looks finished, so no check can flag it. Set sceneUrl " +
|
|
39
|
+
'to the merchant\'s own export (Spline: Export → Viewer → the …/scene.splinecode link), and ' +
|
|
40
|
+
'set specials.posterUrl too or the box is blank until the engine chunk arrives.',
|
|
41
|
+
},
|
|
29
42
|
};
|
|
30
43
|
/** The hint for a type, or null. */
|
|
31
44
|
export function inertHint(type) {
|
|
@@ -79,9 +79,31 @@ export function unknownValueNote(key, value) {
|
|
|
79
79
|
* falls back to `ease`, so the animation still runs — it is a wrong answer, not
|
|
80
80
|
* a missing one.
|
|
81
81
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* IT WAS BASE-ONLY AND IS NOT ANY MORE, and the correction is kept in that
|
|
83
|
+
* shape because this file stated it as a settled fact. The compiler used to
|
|
84
|
+
* index `node.Config["animation"]` with no responsive merge, so a per-breakpoint
|
|
85
|
+
* write landed where nothing looked and `baseonly.ts` ROUTED it. Adding an
|
|
86
|
+
* intensity ended that: a distance is a QUANTITY, and this repo's own mandate is
|
|
87
|
+
* that a quantity reaches the page per breakpoint. `CompileEntranceAnimationCSS`
|
|
88
|
+
* now takes a `bp` and emits per lane, the key left the platform's ledger, and
|
|
89
|
+
* the routing stopped on its own — the table is generated, which is exactly why
|
|
90
|
+
* it could. What the caller gains is the thing merchants ask for most: an
|
|
91
|
+
* animation that is off on mobile.
|
|
92
|
+
*
|
|
93
|
+
* THE OBJECT ALSO GREW TO TEN KEYS, and two of them change what is possible
|
|
94
|
+
* rather than merely how it looks:
|
|
95
|
+
*
|
|
96
|
+
* - `trigger: "view"` IS REVEAL-ON-SCROLL, which this repo recorded as having
|
|
97
|
+
* no answer at all ("the platform has nowhere to put it"). It does now:
|
|
98
|
+
* `animation-timeline: view()` inside an `@supports` override, no island and
|
|
99
|
+
* no JavaScript, with the engines that lack it still animating at first
|
|
100
|
+
* paint. A note that says "you cannot" outlives the thing that made it true,
|
|
101
|
+
* and this is the third time that has cost something here.
|
|
102
|
+
* - `repeat` + `alternate` is the one combination that publishes an INVISIBLE
|
|
103
|
+
* node. `alternate` with an EVEN finite count finishes on the `from`
|
|
104
|
+
* keyframe and every entrance keyframe starts at `opacity: 0`, so the author
|
|
105
|
+
* sees it on the canvas and a visitor never sees it at all. The compiler
|
|
106
|
+
* honours `alternate` only alongside `"infinite"`.
|
|
85
107
|
*/
|
|
86
108
|
export const ANIMATION_VOCAB = ANIMATION;
|
|
87
109
|
/**
|
|
@@ -96,10 +118,68 @@ export const ANIMATION_VOCAB = ANIMATION;
|
|
|
96
118
|
* mistake is actually made.
|
|
97
119
|
*/
|
|
98
120
|
export function animationVocabulary() {
|
|
99
|
-
return (
|
|
100
|
-
|
|
101
|
-
'
|
|
102
|
-
'
|
|
121
|
+
return ('config.animation is an OBJECT, never a string: { active: true, type, easing?, intensity?, ' +
|
|
122
|
+
'trigger?, delay?, duration?, repeat?, alternate?, range? }. active:true is REQUIRED (a type ' +
|
|
123
|
+
'alone is not consent) and the type is UNDERSCORED (fade_in, never fade-in); either miss ' +
|
|
124
|
+
'renders no animation at all, with no error. Written per breakpoint like any config, so it ' +
|
|
125
|
+
'can be off on mobile. See animation_values below for what each key accepts.');
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* THE SAME FACTS AS DATA, because forty-six type names are not a sentence.
|
|
129
|
+
*
|
|
130
|
+
* The line above used to enumerate every type, which worked while there were
|
|
131
|
+
* four and became 848 bytes of prose the moment the platform shipped 46 — and
|
|
132
|
+
* the budget test caught it, correctly, for the second time on this same field.
|
|
133
|
+
* The first catch (a six-field object, 12,396 bytes) moved the long form OUT of
|
|
134
|
+
* the trait sheet; this one moves the ENUMERATION out of the prose. What is left
|
|
135
|
+
* in the sentence is what decides whether a write does anything at all.
|
|
136
|
+
*
|
|
137
|
+
* It rides on `sb_traits_for`, which answers for ONE element per call, and not
|
|
138
|
+
* on `sb_catalog_search`, which answers for many — so the cost is paid once by
|
|
139
|
+
* the caller who has already chosen the element it is about to animate.
|
|
140
|
+
*
|
|
141
|
+
* Every value here is GENERATED from the Go that renders, so a platform that
|
|
142
|
+
* adds a trigger or an intensity brings it to this sheet on the next codegen,
|
|
143
|
+
* and one that drops a field stops claiming it.
|
|
144
|
+
*/
|
|
145
|
+
export function animationValues() {
|
|
146
|
+
const a = ANIMATION;
|
|
147
|
+
const mid = a.intensities[Math.floor(a.intensities.length / 2)];
|
|
148
|
+
return {
|
|
149
|
+
type: a.types,
|
|
150
|
+
easing: { values: a.easings, unknown_falls_back_to: a.easingFallback },
|
|
151
|
+
// `absence is not the middle setting` is the platform's own point and the
|
|
152
|
+
// one reading a caller gets wrong without being told; the rest of the
|
|
153
|
+
// intensity story is a duration table, which speaks for itself.
|
|
154
|
+
...(a.intensities.length
|
|
155
|
+
? {
|
|
156
|
+
intensity: {
|
|
157
|
+
values: a.intensities,
|
|
158
|
+
implies_duration_s: a.intensityDurations,
|
|
159
|
+
note: `omitting it is NOT "${mid}" — it keeps the ${a.durationDefault}s default instead`,
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
: {}),
|
|
163
|
+
// THE ONE ENTRY THAT MUST BE PROACTIVE. Every other trap here is caught by
|
|
164
|
+
// animationNote at the moment of the mistake — but there is no mistake to
|
|
165
|
+
// catch in never knowing a capability exists, so reveal-on-scroll has to be
|
|
166
|
+
// discoverable from the sheet itself.
|
|
167
|
+
...(a.triggers.length
|
|
168
|
+
? {
|
|
169
|
+
trigger: {
|
|
170
|
+
values: a.triggers,
|
|
171
|
+
default: 'omitted = runs at first paint',
|
|
172
|
+
view: `reveal-on-scroll (animation-timeline), range = % of entry, default ${a.rangeDefault}`,
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
: {}),
|
|
176
|
+
...(a.repeatMax ? { repeat: `1-${a.repeatMax} or "infinite"; above that is clamped` } : {}),
|
|
177
|
+
...(a.alternateNeedsInfinite
|
|
178
|
+
? { alternate: 'requires repeat:"infinite" — DROPPED on a finite count, which would end invisible' }
|
|
179
|
+
: {}),
|
|
180
|
+
duration_default_s: a.durationDefault,
|
|
181
|
+
read_by: a.readBy,
|
|
182
|
+
};
|
|
103
183
|
}
|
|
104
184
|
/**
|
|
105
185
|
* The warning for a `config.animation` write that will not animate.
|
|
@@ -138,6 +218,33 @@ export function animationNote(value) {
|
|
|
138
218
|
notes.push(`easing ${JSON.stringify(e)} is outside ${ANIMATION.easings.join(', ')}, so the renderer ` +
|
|
139
219
|
`uses "${ANIMATION.easingFallback}" — the animation runs, with a curve you did not choose`);
|
|
140
220
|
}
|
|
221
|
+
const i = value.intensity;
|
|
222
|
+
if (i !== undefined && (typeof i !== 'string' || !ANIMATION.intensities.includes(i))) {
|
|
223
|
+
// Same family as easing — it runs — but the miss is quieter still: no
|
|
224
|
+
// variables are emitted and the keyframes fall back to their own literals,
|
|
225
|
+
// so the node animates at a distance nobody chose and nothing looks broken.
|
|
226
|
+
notes.push(`intensity ${JSON.stringify(i)} is outside ${ANIMATION.intensities.join(', ')}, so no ` +
|
|
227
|
+
'intensity variables are emitted and the keyframes use their built-in distances');
|
|
228
|
+
}
|
|
229
|
+
const tr = value.trigger;
|
|
230
|
+
if (tr !== undefined && (typeof tr !== 'string' || !ANIMATION.triggers.includes(tr))) {
|
|
231
|
+
notes.push(`trigger ${JSON.stringify(tr)} is not ${ANIMATION.triggers.map((v) => JSON.stringify(v)).join(' or ')}, ` +
|
|
232
|
+
'so the animation runs at first paint rather than on scroll — stored, published, and ignored');
|
|
233
|
+
}
|
|
234
|
+
// THE ONE THAT PUBLISHES AN INVISIBLE NODE. `alternate` is honoured only
|
|
235
|
+
// alongside an infinite repeat: with an even finite count the animation
|
|
236
|
+
// finishes on the `from` keyframe, and every entrance keyframe starts at
|
|
237
|
+
// opacity:0. The author sees it play on the canvas and the visitor sees
|
|
238
|
+
// nothing at all, with no error anywhere to explain it.
|
|
239
|
+
if (ANIMATION.alternateNeedsInfinite && value.alternate === true && value.repeat !== 'infinite') {
|
|
240
|
+
notes.push('alternate:true is DROPPED unless repeat is "infinite" — with an even finite count the ' +
|
|
241
|
+
'animation would finish on the from keyframe, which is opacity:0, and publish a node no ' +
|
|
242
|
+
'visitor can see; the renderer refuses that rather than shipping it');
|
|
243
|
+
}
|
|
244
|
+
if (typeof value.repeat === 'number' && ANIMATION.repeatMax && value.repeat > ANIMATION.repeatMax) {
|
|
245
|
+
notes.push(`repeat ${value.repeat} is clamped to ${ANIMATION.repeatMax} — the count reaches CSS as a ` +
|
|
246
|
+
'number, and a document is not a trusted source');
|
|
247
|
+
}
|
|
141
248
|
if (!notes.length)
|
|
142
249
|
return null;
|
|
143
250
|
return `config.animation will not do what this says. ${notes.join('. ')}.`;
|
package/dist/tools/importpage.js
CHANGED
|
@@ -193,6 +193,13 @@ export function registerImportTools(server, ctx, session) {
|
|
|
193
193
|
.max(100)
|
|
194
194
|
.optional()
|
|
195
195
|
.describe('Default 24 — every image is an upload'),
|
|
196
|
+
nav_timeout_ms: z
|
|
197
|
+
.number()
|
|
198
|
+
.int()
|
|
199
|
+
.min(5_000)
|
|
200
|
+
.max(120_000)
|
|
201
|
+
.optional()
|
|
202
|
+
.describe('How long to wait for a page to answer at all. Default 30000; raise it for a slow origin'),
|
|
196
203
|
max_nodes: z
|
|
197
204
|
.number()
|
|
198
205
|
.int()
|
|
@@ -207,7 +214,7 @@ export function registerImportTools(server, ctx, session) {
|
|
|
207
214
|
dry_run: z.boolean().optional(),
|
|
208
215
|
},
|
|
209
216
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
210
|
-
}, async ({ url, site_id: given, max_sections, max_images, max_nodes, upload_images, dry_run }) => {
|
|
217
|
+
}, async ({ url, site_id: given, max_sections, max_images, max_nodes, nav_timeout_ms, upload_images, dry_run }) => {
|
|
211
218
|
const siteId = siteFor(ctx, given);
|
|
212
219
|
// THE TARGET PAGE MUST BE OPEN, and not only because that is where the
|
|
213
220
|
// nodes go: its own heading, button and section are where the tokens come
|
|
@@ -217,6 +224,7 @@ export function registerImportTools(server, ctx, session) {
|
|
|
217
224
|
maxSections: max_sections,
|
|
218
225
|
maxImages: max_images,
|
|
219
226
|
maxNodes: max_nodes,
|
|
227
|
+
navTimeoutMs: nav_timeout_ms,
|
|
220
228
|
});
|
|
221
229
|
const tokens = tokensFromPage(doc.doc);
|
|
222
230
|
const images = imageSources(shot.sections);
|
|
@@ -332,13 +340,20 @@ export function registerImportTools(server, ctx, session) {
|
|
|
332
340
|
exclude: z.array(z.string()).optional(),
|
|
333
341
|
max_images: z.number().int().min(0).max(200).optional().describe('Default 24, whole import'),
|
|
334
342
|
max_nodes: z.number().int().min(1).max(1000).optional().describe('Per page, default 300'),
|
|
343
|
+
nav_timeout_ms: z
|
|
344
|
+
.number()
|
|
345
|
+
.int()
|
|
346
|
+
.min(5_000)
|
|
347
|
+
.max(120_000)
|
|
348
|
+
.optional()
|
|
349
|
+
.describe('How long to wait for a page to answer at all. Default 30000; raise it for a slow origin'),
|
|
335
350
|
upload_images: z.boolean().optional(),
|
|
336
351
|
homepage: z.boolean().optional().describe("Entry into this site's home page, default true"),
|
|
337
352
|
nav: z.boolean().optional().describe('Shared header linking the new pages, default true'),
|
|
338
353
|
dry_run: z.boolean().optional(),
|
|
339
354
|
},
|
|
340
355
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
341
|
-
}, async ({ url, site_id: given, max_pages, depth, include, exclude, max_images, max_nodes, upload_images, homepage, nav, dry_run, }) => {
|
|
356
|
+
}, async ({ url, site_id: given, max_pages, depth, include, exclude, max_images, max_nodes, nav_timeout_ms, upload_images, homepage, nav, dry_run, }) => {
|
|
342
357
|
const siteId = siteFor(ctx, given);
|
|
343
358
|
const entry = normalizeUrl(url);
|
|
344
359
|
if (!entry) {
|
|
@@ -470,7 +485,7 @@ export function registerImportTools(server, ctx, session) {
|
|
|
470
485
|
// 9,829px of catalogue across a dozen collection bands, and 300 cut it
|
|
471
486
|
// off in the middle of the third. A cap is here to stop a runaway page,
|
|
472
487
|
// not to decide how much of an ordinary one survives.
|
|
473
|
-
{ maxImages: max_images ?? 60, maxNodes: max_nodes ?? 900 });
|
|
488
|
+
{ maxImages: max_images ?? 60, maxNodes: max_nodes ?? 900, navTimeoutMs: nav_timeout_ms });
|
|
474
489
|
const byUrl = new Map(shots.map((s) => [s.url, s]));
|
|
475
490
|
// ONE UPLOAD PER IMAGE FOR THE WHOLE SITE, not per page. A logo, a payment
|
|
476
491
|
// strip and a footer badge appear on every page of a real site, and
|
package/dist/vision/capture.js
CHANGED
|
@@ -1075,6 +1075,19 @@ function capturePage(limits) {
|
|
|
1075
1075
|
}
|
|
1076
1076
|
restore(bestState);
|
|
1077
1077
|
}
|
|
1078
|
+
// WHAT FRACTION OF THE PAGE A READER SEES SURVIVED, reported rather than
|
|
1079
|
+
// assumed. Both halves are already computed above for the fallback decision,
|
|
1080
|
+
// so this costs nothing and closes the one failure a capture cannot express:
|
|
1081
|
+
// SETTLING IS NOT FAILING. MEASURED on ttgshop.vn while the origin was taking
|
|
1082
|
+
// 45s to answer — 6 text nodes and 114 characters of a page holding 2,396,
|
|
1083
|
+
// 4.8%, `skipped` empty, no error anywhere. A thin import that says so is one
|
|
1084
|
+
// a caller retries; a silent one ships.
|
|
1085
|
+
//
|
|
1086
|
+
// Against the honest denominator this file already argues for: page chrome is
|
|
1087
|
+
// skipped ON PURPOSE, so counting it would make every correct import of a
|
|
1088
|
+
// nav-heavy site look broken.
|
|
1089
|
+
const kept = textOf(sections);
|
|
1090
|
+
const coverage = contentChars > 0 ? Math.round((kept / contentChars) * 100) : 100;
|
|
1078
1091
|
const link = Array.from(document.querySelectorAll('link[rel="canonical"]'))[0];
|
|
1079
1092
|
const canonical = link ? (link.getAttribute('href') ?? '') : '';
|
|
1080
1093
|
return {
|
|
@@ -1084,6 +1097,7 @@ function capturePage(limits) {
|
|
|
1084
1097
|
...(forms.length ? { forms } : {}),
|
|
1085
1098
|
sections,
|
|
1086
1099
|
skipped,
|
|
1100
|
+
coverage,
|
|
1087
1101
|
};
|
|
1088
1102
|
}
|
|
1089
1103
|
/**
|
|
@@ -1123,7 +1137,23 @@ async function withBrowser(fn) {
|
|
|
1123
1137
|
* `shoot.ts` gives: a page with a poller never goes idle, and waiting for that
|
|
1124
1138
|
* spends the whole budget on a timeout that cannot resolve.
|
|
1125
1139
|
*/
|
|
1126
|
-
|
|
1140
|
+
/**
|
|
1141
|
+
* How long to wait for a page to answer AT ALL, and why it is a knob.
|
|
1142
|
+
*
|
|
1143
|
+
* Thirty seconds is right for the web and wrong for one site on one afternoon.
|
|
1144
|
+
* MEASURED on ttgshop.vn, the same shop this importer was built against: it
|
|
1145
|
+
* answered in 4.6s all morning and then took 45 SECONDS for 151 KB, with the
|
|
1146
|
+
* next request not answering inside 45s at all. Nothing about the page changed
|
|
1147
|
+
* — the origin was simply having a bad day, which is the ordinary condition of
|
|
1148
|
+
* the sites a merchant actually asks to import.
|
|
1149
|
+
*
|
|
1150
|
+
* The default stays 30s, because a caller who says nothing wants a bound rather
|
|
1151
|
+
* than a hang. What was wrong was that the number could not be raised at all:
|
|
1152
|
+
* an import of your OWN slow site had no recourse, and the failure arrives as a
|
|
1153
|
+
* bare Playwright timeout that names the browser rather than the remedy.
|
|
1154
|
+
*/
|
|
1155
|
+
const NAV_TIMEOUT_MS = 30_000;
|
|
1156
|
+
async function readPage(browser, url, width, work, navMs = NAV_TIMEOUT_MS, settleMs) {
|
|
1127
1157
|
let page;
|
|
1128
1158
|
try {
|
|
1129
1159
|
page = await browser.newPage({ viewport: { width, height: 900 } });
|
|
@@ -1142,14 +1172,14 @@ async function readPage(browser, url, width, work) {
|
|
|
1142
1172
|
// earlier: the right question is "has the DOM stopped changing", and
|
|
1143
1173
|
// `settleDom` answers it directly and bounded. A page that genuinely needs
|
|
1144
1174
|
// its images is the SHOOT path's problem, and that one still waits.
|
|
1145
|
-
await page.goto(url, { waitUntil: 'domcontentloaded', timeout:
|
|
1175
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: navMs });
|
|
1146
1176
|
// THE SAME SETTLE `sb_look` USES, not a flat sleep. A fixed 600ms is wrong
|
|
1147
1177
|
// at both ends: example.com is finished long before it, and a page that
|
|
1148
1178
|
// builds itself with scripts is not finished after it — which is exactly the
|
|
1149
1179
|
// page an import is most likely to be pointed at. `settleDom` asks the
|
|
1150
1180
|
// question actually being asked (has the page stopped changing) and answers
|
|
1151
1181
|
// when it becomes true, bounded so a page that never settles is still read.
|
|
1152
|
-
await settleDom(page);
|
|
1182
|
+
await settleDom(page, settleMs);
|
|
1153
1183
|
return await work(page);
|
|
1154
1184
|
}
|
|
1155
1185
|
finally {
|
|
@@ -1164,10 +1194,32 @@ function limitsFrom(opts) {
|
|
|
1164
1194
|
maxNodes: opts.maxNodes ?? 400,
|
|
1165
1195
|
};
|
|
1166
1196
|
}
|
|
1197
|
+
/**
|
|
1198
|
+
* A read that failed, said in the caller's terms.
|
|
1199
|
+
*
|
|
1200
|
+
* Playwright's own timeout names the browser and the wait — "page.goto: Timeout
|
|
1201
|
+
* 30000ms exceeded" — which is true and tells a merchant nothing they can act
|
|
1202
|
+
* on. The remedy is a number they can raise, so the message carries it.
|
|
1203
|
+
*/
|
|
1204
|
+
function readFailure(e) {
|
|
1205
|
+
const msg = e.message ?? String(e);
|
|
1206
|
+
// THE NUMBER COMES OUT OF THE ERROR, never out of the default. The first
|
|
1207
|
+
// version of this printed NAV_TIMEOUT_MS, so a caller who had already raised
|
|
1208
|
+
// the budget to 90s was told the page "did not answer within 30s" — a
|
|
1209
|
+
// confidently wrong number, and one that sends them to change a setting they
|
|
1210
|
+
// had just changed.
|
|
1211
|
+
const hit = /Timeout (\d+)ms exceeded/.exec(msg);
|
|
1212
|
+
if (hit) {
|
|
1213
|
+
const secs = Math.round(Number(hit[1]) / 1000);
|
|
1214
|
+
return (`the page did not answer within ${secs}s. A slow origin is ordinary — raise ` +
|
|
1215
|
+
'nav_timeout_ms and try again, or import fewer pages at once.');
|
|
1216
|
+
}
|
|
1217
|
+
return msg.slice(0, 160);
|
|
1218
|
+
}
|
|
1167
1219
|
/** Open a URL and capture it. */
|
|
1168
1220
|
export async function capture(url, opts = {}) {
|
|
1169
1221
|
const limits = limitsFrom(opts);
|
|
1170
|
-
return withBrowser((browser) => readPage(browser, url, opts.width ?? 1440, (page) => page.evaluate(capturePage, limits)));
|
|
1222
|
+
return withBrowser((browser) => readPage(browser, url, opts.width ?? 1440, (page) => page.evaluate(capturePage, limits), opts.navTimeoutMs, opts.settleMs));
|
|
1171
1223
|
}
|
|
1172
1224
|
/**
|
|
1173
1225
|
* Capture several pages through one browser.
|
|
@@ -1184,11 +1236,11 @@ export async function captureMany(urls, opts = {}) {
|
|
|
1184
1236
|
const out = [];
|
|
1185
1237
|
for (const url of urls) {
|
|
1186
1238
|
try {
|
|
1187
|
-
const result = (await readPage(browser, url, opts.width ?? 1440, (page) => page.evaluate(capturePage, limits)));
|
|
1239
|
+
const result = (await readPage(browser, url, opts.width ?? 1440, (page) => page.evaluate(capturePage, limits), opts.navTimeoutMs, opts.settleMs));
|
|
1188
1240
|
out.push({ url, ok: true, result });
|
|
1189
1241
|
}
|
|
1190
1242
|
catch (e) {
|
|
1191
|
-
out.push({ url, ok: false, why: e
|
|
1243
|
+
out.push({ url, ok: false, why: readFailure(e) });
|
|
1192
1244
|
}
|
|
1193
1245
|
}
|
|
1194
1246
|
return out;
|
package/dist/vision/shoot.js
CHANGED
|
@@ -172,7 +172,7 @@ export async function shoot(url, opts = {}) {
|
|
|
172
172
|
* polling widget from holding the shot forever. A page that never settles is
|
|
173
173
|
* photographed anyway — a late picture beats none.
|
|
174
174
|
*/
|
|
175
|
-
export async function settleDom(page) {
|
|
175
|
+
export async function settleDom(page, capMs = 2_000) {
|
|
176
176
|
await page
|
|
177
177
|
.evaluate(({ quiet, cap }) => new Promise((resolve) => {
|
|
178
178
|
const start = Date.now();
|
|
@@ -194,7 +194,14 @@ export async function settleDom(page) {
|
|
|
194
194
|
resolve();
|
|
195
195
|
}
|
|
196
196
|
}, 50);
|
|
197
|
-
}),
|
|
197
|
+
}),
|
|
198
|
+
// The CAP is a knob because 2,000 ms is right for a healthy origin and
|
|
199
|
+
// wrong for a struggling one. MEASURED on ttgshop.vn while it was taking
|
|
200
|
+
// 45s to answer: the capture returned 6 text nodes and 114 characters of
|
|
201
|
+
// a page that holds 2,396 — 4.8% — and reported NO error, because
|
|
202
|
+
// settling is not failing. A silent thin import is worse than a refused
|
|
203
|
+
// one, and the shoot path keeps the default it was tuned for.
|
|
204
|
+
{ quiet: 250, cap: capMs })
|
|
198
205
|
.catch(() => { });
|
|
199
206
|
}
|
|
200
207
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.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",
|