fix(skeleton): put the widget gutter relief on the skeleton's inner item
小工具(首页挂载的 / 侧栏的)骨架加载占位比真实卡片窄一截:card 布局下
每侧窄 8px,视觉上就是「骨架小了一些」。
小工具的卡片走 skip_wrapper=true,直接挂在 .widget-ajaxpager 上
(src/WidgetSystem/PostRenderer.php),不经过 .posts-row,因此只受小工具
这条独立的负边距链作用:
.widget-ajaxpager margin: -8px
.posts-item.card(真实卡片) margin: 8px ← 卡片自身自带
.post_ajax_loader(外层) margin: 8px ← f61142ee 加的补偿
.posts-item.card(骨架内层) margin: 8px ← 同一条卡片规则
骨架于是拿到 8px(外层 loader)+ 8px(内层卡片)= 16px,而真实卡片只有
8px,内容区每侧少 8px。list 模式没暴露是因为 .posts-item 的 8px 来自
`.widget-main-post.style-list .posts-item`,该规则只覆盖 style-list、
不作用于骨架,外层的 8px 恰好补上 list 卡片应有的一份,刚好对齐。
修法:把水平补偿从外层 .post_ajax_loader 移到骨架内层的 .posts-item /
.posts-mini —— 与真实卡片同一个元素、同一条选择器,于是两者逐像素同宽,
也顺带回答了「补偿链的拥有者是谁」这个一直在打补丁的问题。
- assets/css/main.css、assets/css/frontend/post-card.css:.posts-row 与
.widget-ajaxpager 两条链、桌面 8px 与移动 5px 两组,共 8 处规则改为
内层选择器。
- assets/css/routes/user-home.css:面板归零规则同步改到内层,否则它归零
的是已经失效的外层,旧补偿会重新显形。
- tests/widget-system/asset-ledger-contract.test.cjs:把守卫断言改成内层
选择器,并修正规则匹配以支持选择器组(.posts-item, .posts-mini)。
注释说明为何不能补偿外层。
参考主题 zibll 完全没有 .post_ajax_loader 的 margin 规则;AKSRT 需要它,
是因为骨架多包了一层。定位在内层是与参考主题等价的正确解。
Verification:
- node tests/widget-system/asset-ledger-contract.test.cjs 16/16
- node tests/widget-system/post-visual-contract.test.cjs 22/22
- node tests/widget-system/zibll-modern-widgets-contract.test.cjs 10/10
- node tests/widget-system/lazyload-contract.test.cjs 7/7
- node tests/widget-system/frontend-lifecycle-contract.test.cjs 13/13
- node tests/widget-system/page-display-contract.test.cjs 18/18
- node tests/widget-system/message-center-contract.test.cjs 8/8
- php tools/audit-admin-contracts.php errors=0
- build:public 已重建;旧外层规则在源与产物中均已归零
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,8 +16,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 骨架与真实卡片同宽:.posts-row 以 -8px 抵消卡片边距,loader 被上面的
|
/* 骨架与真实卡片同宽:.posts-row 以 -8px 抵消卡片边距,loader 被上面的
|
||||||
:not() 排除在补偿外,会宽出 16px;这里补上等同的水平边距。 */
|
:not() 排除在补偿外,会宽出 16px;这里补上等同的水平边距。
|
||||||
.posts-row > .post_ajax_loader {
|
补偿落在骨架内层的 .posts-item 上(与真实卡片同一元素),不落外层
|
||||||
|
.post_ajax_loader —— 否则内层 .posts-item.card 的 margin:8px 会与外层
|
||||||
|
补偿叠成 16px,小工具 card 骨架因此窄出 8px。 */
|
||||||
|
.posts-row > .post_ajax_loader .posts-item,
|
||||||
|
.posts-row > .post_ajax_loader .posts-mini {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
@@ -456,10 +460,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 骨架与卡片同宽同轴:.widget-ajaxpager / .tab-pane 用负边距抵消卡片的水平
|
/* 骨架与卡片同宽同轴:.widget-ajaxpager / .tab-pane 用负边距抵消卡片的水平
|
||||||
边距,骨架被排除在补偿链外,于是左右各宽出一个 gutter。 */
|
边距。补偿同样落在骨架内层的 .posts-item 上(与真实卡片同一元素),
|
||||||
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader,
|
不落外层 .post_ajax_loader —— 否则内层 .posts-item.card 自带的 margin:8px
|
||||||
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader,
|
会与外层补偿叠成 16px,card 骨架因此窄出 8px。 */
|
||||||
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader {
|
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader .posts-item,
|
||||||
|
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader .posts-item,
|
||||||
|
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader .posts-item {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
@@ -534,7 +540,8 @@
|
|||||||
margin-left: 5px !important;
|
margin-left: 5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.posts-row > .post_ajax_loader {
|
.posts-row > .post_ajax_loader .posts-item,
|
||||||
|
.posts-row > .post_ajax_loader .posts-mini {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
@@ -585,9 +592,9 @@
|
|||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader,
|
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader .posts-item,
|
||||||
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader,
|
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader .posts-item,
|
||||||
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader {
|
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader .posts-item {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -15,8 +15,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 骨架与真实卡片同宽:.posts-row 以 -8px 抵消卡片边距,loader 被上面的
|
/* 骨架与真实卡片同宽:.posts-row 以 -8px 抵消卡片边距,loader 被上面的
|
||||||
:not() 排除在补偿外,会宽出 16px;这里补上等同的水平边距。 */
|
:not() 排除在补偿外,会宽出 16px;这里补上等同的水平边距。
|
||||||
.posts-row > .post_ajax_loader {
|
补偿落在骨架内层的 .posts-item 上(与真实卡片同一元素),不落外层
|
||||||
|
.post_ajax_loader —— 否则内层 .posts-item.card 的 margin:8px 会与外层
|
||||||
|
补偿叠成 16px,小工具 card 骨架因此窄出 8px。 */
|
||||||
|
.posts-row > .post_ajax_loader .posts-item,
|
||||||
|
.posts-row > .post_ajax_loader .posts-mini {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
@@ -455,10 +459,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 骨架与卡片同宽同轴:.widget-ajaxpager / .tab-pane 用负边距抵消卡片的水平
|
/* 骨架与卡片同宽同轴:.widget-ajaxpager / .tab-pane 用负边距抵消卡片的水平
|
||||||
边距,骨架被排除在补偿链外,于是左右各宽出一个 gutter。 */
|
边距。补偿同样落在骨架内层的 .posts-item 上(与真实卡片同一元素),
|
||||||
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader,
|
不落外层 .post_ajax_loader —— 否则内层 .posts-item.card 自带的 margin:8px
|
||||||
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader,
|
会与外层补偿叠成 16px,card 骨架因此窄出 8px。 */
|
||||||
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader {
|
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader .posts-item,
|
||||||
|
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader .posts-item,
|
||||||
|
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader .posts-item {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
@@ -533,7 +539,8 @@
|
|||||||
margin-left: 5px !important;
|
margin-left: 5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.posts-row > .post_ajax_loader {
|
.posts-row > .post_ajax_loader .posts-item,
|
||||||
|
.posts-row > .post_ajax_loader .posts-mini {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
@@ -584,9 +591,9 @@
|
|||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader,
|
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader .posts-item,
|
||||||
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader,
|
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader .posts-item,
|
||||||
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader {
|
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader .posts-item {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-11
@@ -7487,8 +7487,12 @@ badge.top,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 骨架与真实卡片同宽同轴:.posts-row 用 -8px 抵消卡片边距,原 :not() 把骨架
|
/* 骨架与真实卡片同宽同轴:.posts-row 用 -8px 抵消卡片边距,原 :not() 把骨架
|
||||||
排除在补偿外,骨架于是比卡片左右各宽 8px。补上等同边距。 */
|
排除在补偿外,骨架于是比卡片左右各宽 8px。补上等同边距。
|
||||||
.posts-row > .post_ajax_loader {
|
注意补偿落在骨架**内层**的 .posts-item 上(与真实卡片同一元素、同一选择器),
|
||||||
|
不能落在外层 .post_ajax_loader 上 —— 骨架内层本身带 .posts-item.card 的
|
||||||
|
margin:8px,外层再补 8px 就是双重补偿,小工具 card 骨架会窄出 8px。 */
|
||||||
|
.posts-row > .post_ajax_loader .posts-item,
|
||||||
|
.posts-row > .post_ajax_loader .posts-mini {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
@@ -7898,11 +7902,13 @@ badge.top,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 骨架与卡片同宽同轴:.widget-ajaxpager / .tab-pane 用负边距抵消卡片的水平
|
/* 骨架与卡片同宽同轴:.widget-ajaxpager / .tab-pane 用负边距抵消卡片的水平
|
||||||
边距,骨架被排除在补偿链外,于是左右各宽出一个 gutter。这里补回等宽的
|
边距。补偿同样落在骨架内层的 .posts-item 上(与真实卡片同一元素),
|
||||||
边距,让骨架与真实卡片对齐。tab pane 的 style-list / index-tab 都适用。 */
|
而不是外层 .post_ajax_loader —— 否则骨架内层自带的 .posts-item.card
|
||||||
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader,
|
margin:8px 会与外层补偿叠成 16px,让 card 骨架窄出 8px。
|
||||||
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader,
|
style-card 的卡片由 .posts-item.card 自带 margin:8px,这里只需覆盖骨架内层。 */
|
||||||
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader {
|
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader .posts-item,
|
||||||
|
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader .posts-item,
|
||||||
|
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader .posts-item {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
@@ -11230,9 +11236,9 @@ body.error404 {
|
|||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader,
|
.widget-main-post.style-list .widget-ajaxpager > .post_ajax_loader .posts-item,
|
||||||
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader,
|
.widget-main-post.style-card .widget-ajaxpager > .post_ajax_loader .posts-item,
|
||||||
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader {
|
.widget-tab-post .tab-content > .tab-pane > .widget-ajaxpager > .post_ajax_loader .posts-item {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
@@ -11247,7 +11253,8 @@ body.error404 {
|
|||||||
margin-right: 5px !important;
|
margin-right: 5px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.posts-row > .post_ajax_loader {
|
.posts-row > .post_ajax_loader .posts-item,
|
||||||
|
.posts-row > .post_ajax_loader .posts-mini {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -392,8 +392,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 骨架跟随网格走:容器 margin 已归零、卡片边距已清零,
|
/* 骨架跟随网格走:容器 margin 已归零、卡片边距已清零,
|
||||||
不能再叠加 .posts-row > .post_ajax_loader 的 8px 补偿。 */
|
不能再叠加 .posts-row > .post_ajax_loader 内层的 8px 补偿。 */
|
||||||
.aksrt-user-home .aksrt-user-home__post-list[data-view="list"] > .post_ajax_loader {
|
.aksrt-user-home .aksrt-user-home__post-list[data-view="list"] > .post_ajax_loader .posts-item {
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,26 +97,33 @@ test('skeleton blocks share the card gutter instead of overflowing it', () => {
|
|||||||
// card a matching gutter. The card rule is scoped
|
// card a matching gutter. The card rule is scoped
|
||||||
// `.posts-row > *:not(.post_ajax_loader)`, so the loader is excluded from the
|
// `.posts-row > *:not(.post_ajax_loader)`, so the loader is excluded from the
|
||||||
// compensation chain and ends up wider than the cards by both gutters.
|
// compensation chain and ends up wider than the cards by both gutters.
|
||||||
// Each gutter block therefore needs a sibling rule that gives the loader the
|
//
|
||||||
// same horizontal margin — otherwise 加载更多 shows a skeleton that spills
|
// The relief must land on the skeleton's INNER `.posts-item` — the same
|
||||||
// outside the content column.
|
// element and selector the real card uses — never on the outer
|
||||||
|
// `.post_ajax_loader`. A block skeleton is `> .post_ajax_loader > .mb20 >
|
||||||
|
// .posts-item.card`, and `.posts-item.card` already carries margin:8px of its
|
||||||
|
// own: compensating the outer wrapper too would stack 8px + 8px and make the
|
||||||
|
// card skeleton 8px narrower per side than a real card (the widget card
|
||||||
|
// layout, which skips .posts-row and relies on the .widget-ajaxpager chain).
|
||||||
const blocks = [
|
const blocks = [
|
||||||
{
|
{
|
||||||
file: 'assets/css/main.css',
|
file: 'assets/css/main.css',
|
||||||
selector: '.posts-row > .post_ajax_loader',
|
selector: '.posts-row > .post_ajax_loader .posts-item',
|
||||||
gutters: [8, 5],
|
gutters: [8, 5],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
file: 'assets/css/frontend/post-card.css',
|
file: 'assets/css/frontend/post-card.css',
|
||||||
selector: '.posts-row > .post_ajax_loader',
|
selector: '.posts-row > .post_ajax_loader .posts-item',
|
||||||
gutters: [8, 5],
|
gutters: [8, 5],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
const css = fs.readFileSync(path.join(root, block.file), 'utf8')
|
const css = fs.readFileSync(path.join(root, block.file), 'utf8')
|
||||||
|
// The rule ships a selector group (`.posts-item, .posts-mini`), so match the
|
||||||
|
// whole group up to its `{` instead of assuming the selector is alone.
|
||||||
const rules = (css.match(
|
const rules = (css.match(
|
||||||
new RegExp(`${block.selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\{[^}]*\\}`, 'g')
|
new RegExp(`${block.selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^{]*\\{[^}]*\\}`, 'g')
|
||||||
) || []).map((rule) => rule.replace(/\s+/g, ' '))
|
) || []).map((rule) => rule.replace(/\s+/g, ' '))
|
||||||
|
|
||||||
// One rule per gutter value in that file (desktop 8px, mobile 5px).
|
// One rule per gutter value in that file (desktop 8px, mobile 5px).
|
||||||
@@ -128,26 +135,27 @@ test('skeleton blocks share the card gutter instead of overflowing it', () => {
|
|||||||
)
|
)
|
||||||
assert.ok(
|
assert.ok(
|
||||||
matched,
|
matched,
|
||||||
`${block.file} must give .post_ajax_loader the ${gutter}px gutter in "${block.selector}"`
|
`${block.file} must give .post_ajax_loader .posts-item the ${gutter}px gutter in "${block.selector}"`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The widget pagers apply their own negative-margin compensation chain
|
// The widget pagers apply their own negative-margin compensation chain
|
||||||
// (.widget-ajaxpager / .tab-pane), so the loader needs the same relief there.
|
// (.widget-ajaxpager / .tab-pane), so the skeleton's inner item needs the same
|
||||||
|
// relief there — again on .posts-item, not on the loader wrapper.
|
||||||
for (const file of ['assets/css/main.css', 'assets/css/frontend/post-card.css']) {
|
for (const file of ['assets/css/main.css', 'assets/css/frontend/post-card.css']) {
|
||||||
const css = fs.readFileSync(path.join(root, file), 'utf8')
|
const css = fs.readFileSync(path.join(root, file), 'utf8')
|
||||||
// Cover every style class the renderer can emit (.style-list / .style-card,
|
// Cover every style class the renderer can emit (.style-list / .style-card,
|
||||||
// and the tab pane which carries style-list or index-tab).
|
// and the tab pane which carries style-list or index-tab).
|
||||||
assert.match(css, /\.widget-main-post\.style-list \.widget-ajaxpager > \.post_ajax_loader/)
|
assert.match(css, /\.widget-main-post\.style-list \.widget-ajaxpager > \.post_ajax_loader \.posts-item/)
|
||||||
assert.match(css, /\.widget-main-post\.style-card \.widget-ajaxpager > \.post_ajax_loader/)
|
assert.match(css, /\.widget-main-post\.style-card \.widget-ajaxpager > \.post_ajax_loader \.posts-item/)
|
||||||
assert.match(css, /\.widget-tab-post \.tab-content > \.tab-pane > \.widget-ajaxpager > \.post_ajax_loader/)
|
assert.match(css, /\.widget-tab-post \.tab-content > \.tab-pane > \.widget-ajaxpager > \.post_ajax_loader \.posts-item/)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The user-home panel zeroes the container margin and the card gutters, so the
|
// The user-home panel zeroes the container margin and the card gutters, so the
|
||||||
// loader must not pick up any leftover compensation there.
|
// skeleton's inner item must not pick up any leftover compensation there.
|
||||||
const userHome = fs.readFileSync(path.join(root, 'assets/css/routes/user-home.css'), 'utf8')
|
const userHome = fs.readFileSync(path.join(root, 'assets/css/routes/user-home.css'), 'utf8')
|
||||||
assert.match(userHome, /\.aksrt-user-home__post-list\[data-view="list"\] > \.post_ajax_loader \{\s*margin-right: 0;\s*margin-left: 0;/)
|
assert.match(userHome, /\.aksrt-user-home__post-list\[data-view="list"\] > \.post_ajax_loader \.posts-item \{\s*margin-right: 0;\s*margin-left: 0;/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('asset tables are InnoDB and operation keys are unique', () => {
|
test('asset tables are InnoDB and operation keys are unique', () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"generated_at": "2026-10-03T18:57:46+00:00",
|
"generated_at": "2026-10-03T20:06:15+00:00",
|
||||||
"errors": [],
|
"errors": [],
|
||||||
"warnings": [
|
"warnings": [
|
||||||
"customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)",
|
"customPage tools-update section 'tools/update' not in PHP schema (ok if pure operational page)",
|
||||||
|
|||||||
Reference in New Issue
Block a user