sbuilder-mcp 0.37.0 → 0.38.1
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/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/vision/capture.js +18 -1
- package/dist/vision/shoot.js +37 -0
- 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.38.1] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- sb_look no longer photographs a reveal-on-scroll band (config.animation trigger:"view") as blank; every animation is now stopped before the shutter opens, since the lazy-image walk returns to the top of the page and would otherwise catch the band back at its opacity:0 starting keyframe.
|
|
13
|
+
- sb_import and sb_import_site no longer drop a `<video>` element that carries a poster image and no explicit width/height, which previously measured as a zero-size box and was skipped like a genuinely hidden element.
|
|
14
|
+
|
|
15
|
+
## [0.38.0] - 2026-09-11
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- 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.
|
|
19
|
+
- 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.
|
|
20
|
+
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
9
26
|
## [0.37.0] - 2026-09-11
|
|
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.38.1] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- sb_look không còn chụp một band hiệu ứng xuất hiện khi cuộn (config.animation trigger:"view") thành ảnh trống nữa; mọi animation giờ được dừng lại trước khi chụp, vì bước quét ảnh lazy-load quay về đầu trang và trước đây sẽ bắt band đó đứng yên ở keyframe khởi đầu opacity:0.
|
|
13
|
+
- sb_import và sb_import_site không còn bỏ sót phần tử `<video>` có ảnh poster nhưng không khai báo width/height tường minh, trước đây phần tử này đo được kích thước bằng 0 nên bị bỏ qua như một phần tử thực sự ẩn.
|
|
14
|
+
|
|
15
|
+
## [0.38.0] - 2026-09-11
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- 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.
|
|
19
|
+
- 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.
|
|
20
|
+
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
9
26
|
## [0.37.0] - 2026-09-11
|
|
10
27
|
|
|
11
28
|
### 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/vision/capture.js
CHANGED
|
@@ -22,7 +22,24 @@ function capturePage(limits) {
|
|
|
22
22
|
if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0')
|
|
23
23
|
return false;
|
|
24
24
|
const r = el.getBoundingClientRect();
|
|
25
|
-
|
|
25
|
+
if (r.width > 0 && r.height > 0)
|
|
26
|
+
return true;
|
|
27
|
+
// A ZERO BOX MEANS "HIDDEN" ONLY FOR AN ELEMENT THAT SIZES ITSELF FROM ITS
|
|
28
|
+
// CONTENT. A `<video>` sizes itself from its MEDIA, and a `poster` is what
|
|
29
|
+
// it measures before any media loads — so a video whose poster has not
|
|
30
|
+
// resolved measures 0×0 while being perfectly present, and Chrome only
|
|
31
|
+
// falls back to 300×150 once that load has actually failed.
|
|
32
|
+
//
|
|
33
|
+
// MEASURED: `<video src poster>` with no explicit width/height was dropped
|
|
34
|
+
// with nothing but a `hidden` count, while the identical element carrying
|
|
35
|
+
// `width`/`height` — or carrying NO poster — came through. So the one video
|
|
36
|
+
// most worth importing, the one with a still frame on it, was the one that
|
|
37
|
+
// disappeared, and only on the pages slow enough for the race to be lost.
|
|
38
|
+
//
|
|
39
|
+
// Narrow on purpose: a `<video>` alone, and only when it names something to
|
|
40
|
+
// play or to show. An empty `<video></video>` is a genuinely empty box and
|
|
41
|
+
// goes on being skipped.
|
|
42
|
+
return el.tagName === 'VIDEO' && !!(el.getAttribute('src') || el.getAttribute('poster'));
|
|
26
43
|
};
|
|
27
44
|
const clean = (s) => (s ?? '').replace(/\s+/g, ' ').trim();
|
|
28
45
|
// ABSOLUTE where it can be, RAW where it cannot. `new URL(rel, base)` throws
|
package/dist/vision/shoot.js
CHANGED
|
@@ -250,6 +250,42 @@ async function settleLazyImages(page) {
|
|
|
250
250
|
.waitForFunction(() => [...document.querySelectorAll('img')].every((i) => i.complete === true), undefined, { timeout: 3_000 })
|
|
251
251
|
.catch(() => { });
|
|
252
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* SETTLE EVERY ENTRANCE ANIMATION, or photograph a band that is not there.
|
|
255
|
+
*
|
|
256
|
+
* A still picture wants the page a visitor ends up looking at, and the settled
|
|
257
|
+
* state of every entrance animation is "visible". Two ways a correct page
|
|
258
|
+
* photographs BLANK without this, both of them new since the platform shipped
|
|
259
|
+
* 46 effects and a scroll trigger, and both silent:
|
|
260
|
+
*
|
|
261
|
+
* - `trigger: "view"` compiles to `animation-timeline: view()`, whose progress
|
|
262
|
+
* is a function of where the element sits in the scrollport. The walk above
|
|
263
|
+
* scrolls and then RETURNS TO THE TOP, so every revealed section is back at
|
|
264
|
+
* its `from` keyframe — `opacity: 0` — at the moment the shutter opens.
|
|
265
|
+
* MEASURED: a four-band page photographed with the third band entirely
|
|
266
|
+
* empty, the other three correct.
|
|
267
|
+
* - `fill: both` applies the `from` keyframe during a `delay`, so a section
|
|
268
|
+
* with a delay long enough to outlast the settle photographs blank too.
|
|
269
|
+
*
|
|
270
|
+
* The honest reading of either picture is "this band is broken", which sends
|
|
271
|
+
* the caller to fix a page that works — the same failure the lazy-image walk
|
|
272
|
+
* above exists to prevent, arriving by a different route that the walk cannot
|
|
273
|
+
* fix because the walk's own return to the top is what causes it.
|
|
274
|
+
*
|
|
275
|
+
* `animation: none` rather than forcing the timeline to `auto`: that one
|
|
276
|
+
* RESTARTS the animation against the document timeline and the shot catches it
|
|
277
|
+
* mid-flight (measured at opacity 0.317). Removing the animation drops the
|
|
278
|
+
* element to its own declared style, which for every entrance effect here is
|
|
279
|
+
* exactly the end state — measured back at opacity 1, with the band's text on
|
|
280
|
+
* screen.
|
|
281
|
+
*
|
|
282
|
+
* Best-effort: a page that refuses a stylesheet is still worth photographing.
|
|
283
|
+
*/
|
|
284
|
+
async function settleAnimations(page) {
|
|
285
|
+
await page
|
|
286
|
+
.addStyleTag({ content: '*,*::before,*::after{animation:none !important}' })
|
|
287
|
+
.catch(() => { });
|
|
288
|
+
}
|
|
253
289
|
async function shootOne(page, url, width, format, opts) {
|
|
254
290
|
// LOAD, then a BOUNDED settle — never `networkidle` alone.
|
|
255
291
|
//
|
|
@@ -287,6 +323,7 @@ async function shootOne(page, url, width, format, opts) {
|
|
|
287
323
|
}
|
|
288
324
|
await settleDom(page);
|
|
289
325
|
await settleLazyImages(page);
|
|
326
|
+
await settleAnimations(page);
|
|
290
327
|
// A RENDERED page carries its node ids as the HTML `id` attribute — not as
|
|
291
328
|
// `data-node-id`, which is the editor CANVAS's hook and never reaches the
|
|
292
329
|
// renderer. Selecting the canvas attribute here returned an empty box list
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.1",
|
|
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",
|