Compare commits

...
11 Commits
Author SHA1 Message Date
kate522 5641c40958 fix(header): stop the nav midline chip rule from collapsing the quick-login buttons
The top-nav guest popover rendered its quick-login buttons as 29px circles
with zero gap: github, gitee and qq were touching. The user-card widget, which
uses the same .social_loginbar markup, was fine at 32px with 10px gaps.

header-userbox.css carries an AKSRT-specific rule, absent from the reference
theme, that gives every .toggle-radius inside .navbar-form a 29px midline
circle:

  .navbar-top .navbar-form .toggle-radius { flex: 0 0 29px; margin: 0; }

It exists for the account-row .badg chips, the theme toggle and the language
toggle. The popover social buttons are .toggle-radius too and also sit inside
.navbar-form, so they were caught by it. Two independent mechanisms broke the
geometry: flex-basis outranks width in a flex container, so flex: 0 0 29px beat
the .social_loginbar .social-login-item width: 32px !important; and the rule
(0,3,0) specificity outranked the (0,2,0) margin: 3px 5px.

Exempt the social buttons with :not(). The chips, the theme toggle and the
language toggle still match, and the popover buttons fall back to the
.social_loginbar geometry ported 1:1 from the reference.

Verified on the live site by measuring computed styles in headless Edge: the
popover buttons go from 29px/0px margins/0px gap to 32px/5px margins/10px gap,
the user-card widget is unchanged, and the account-row chips keep 29px (theme
toggle) and 36px (.badg). Rebuilt the public bundle so the exemption survives
the build, and contract-locked the selector plus the bundle copy.

Verification: node --test across widget-system, navigation, admin,
asset-ledger, dev, front-submit and visual (615 tests).
2026-10-17 17:56:56 +08:00
kate522 0bee2c605e refactor(auth): stop pre-escaping the OAuth provider URLs
get_auth_providers_data() returned esc_url()-ed URLs while all four of its
consumers (the modal, the user card, render_social_buttons and the comment
form) escaped again on output. Its sibling get_frontend_bindings_data() stores
the raw URL and lets the binding list escape it, so the class was inconsistent
with itself.

I first reported this as a double-escaping bug that broke ampersand-bearing
URLs. That was wrong: I had verified it with a stub that modelled esc_url() as
htmlspecialchars(). The real esc_url() runs wp_kses_normalize_entities()
first, which explicitly decodes double-encoded character references, so
esc_url() is idempotent on its own output. Re-checked against the WordPress
source with a faithful transcription, and re-ran the popover markup: the
WeChat href is ?tab=oauth&oauth_intent=login, one entity, unchanged.

So the escaping was redundant rather than broken. The producer now hands out
raw URLs and each surface escapes exactly once, which is where WordPress
expects it and matches get_frontend_bindings_data(). Rendered output is
byte-identical.

Extended the auth source contract to pin the invariant: the producer stays
raw and all four surfaces escape on output.

Verification: node --test across widget-system, navigation, admin,
asset-ledger, dev, front-submit and visual (614 tests), plus php -l.
2026-10-17 14:54:10 +08:00
kate522 c472045ce4 fix(auth): align the social-login surfaces with the reference theme
The "other login methods" block drifted from zibll on three axes. Gitee was
mapped to fa-code-branch because FA5 Free ships no Gitee brand glyph, so the
button showed a generic branch icon; Google showed the monochrome FA glyph
instead of the reference's multi-colour mark. The modal also printed an
implicit agreement line under the social buttons, which the reference guest
card does not have. And the top-nav user popover built its own
.social-login-divider / .oauth-row / .oauth-btn markup and then wrapped that
in a second separator and .social_loginbar, so the block rendered nested and
unlike the reference.

aksrt_brand_icon() now routes zibsvg-/customsvg- values through the shared
icon renderer, and the brand map resolves gitee to zibsvg-gitee and google to
zibsvg-google-color. Both sprite keys already exist in the runtime symbol
sheet, which the loader pulls in on every frontend page. Because
get_auth_providers_data() and binding_row() both go through
aksrt_brand_icon(), one map change fixes the icon on the modal, the user card,
the top-nav popover, the user-center binding list and the profile social
links.

render_social_buttons() is rewritten to the reference shape: an optional
.social-separator, a .social_loginbar, and one .social-login-item <type>
toggle-radius anchor per enabled provider with rel="nofollow" and a
"<label>登录" title. oauth_button_lg switches to the button-lg variant with a
label, and the WeChat entry carries qrcode-signin so auth.js opens the QR
panel instead of navigating, matching the reference. The navbar and the link
apply modal now append the block verbatim instead of re-wrapping it, and the
user-card loop adopts the same item attributes. The retired
.social-login-divider / .oauth-row / .oauth-btn rules are dropped from
base-1.css, and the binding-list icon rule gains an svg selector so the
sprite icons keep the 22px the FA glyphs had.

Verification: node --test across widget-system, navigation, admin,
asset-ledger, dev, front-submit and visual (613 tests), plus php -l on every
changed PHP file. Extended the auth source contract with the agreement-line,
brand-icon and popover-structure cases, and pinned the user-card guest
markup.
2026-10-17 14:42:42 +08:00
kate522 6dc9265260 fix(frontend): keep the widget link color off the check-in badge
sidebar.css paints .widget a with --color-ink-2 (specificity 0,1,1), which
out-specifies the .jb-* color group (0,1,0) the badge relies on for its white
ink. The user-card widget carries the .widget class, so the check-in bookmark
rendered dark-gray text on the orange gradient on widget pages. zibll sets no
link color on zib-widget anchors at all.

sidebar.css already restores .but colors for exactly this reason; extend that
restoration to .img-badge so the badge keeps its own --this-color. Rebuild the
homepage bundle and extend the badge contract test.
2026-10-17 13:02:46 +08:00
kate522 008e63524d fix(frontend): pin the check-in badge typography to the reference values
The badge inherited AKSRT's modern system font stack (--font-body) and the
html reset's antialiased/optimizeLegibility rendering, while the reference
theme renders its body in bootstrap's "Helvetica Neue", Helvetica, Arial,
sans-serif with browser defaults. Same 14px size but a different typeface
and smoothing, so the badge text still read as off-theme after the gradient
fix.

Pin font-family, -webkit-font-smoothing and text-rendering to the reference
body values on the existing scoped badge rule (alongside the line-height
pin) and extend the contract test.
2026-10-17 12:40:42 +08:00
kate522 cff3083a91 fix(frontend): align the user-card check-in badge with the reference theme
The legacy .jb-* compat layer in core/buttons.css restated .jb-yellow as #f99d4d -> #f7631d, and because the public bundle loads after main.min.css that value overrode the reference-accurate #f59f54 -> #ff6922 gradient, so the check-in bookmark (and every other .jb-yellow surface) painted the wrong orange. Correct the gradient and rebuild the public bundle.

Also drop the dead is-done class and the attribute-less span wrapper from the signed card badge, which the reference theme does not have. The header dropdown variant keeps its class, locked by the navigation contract.
2026-10-16 17:32:06 +08:00
kate522 4d69aaee50 fix(frontend): scope the credits button skins to the user-center shell
The user-center route rendered a 4px top-nav publish chip while every other
route kept the 50px pill. assets/css/routes/user-center/credits.css restated
border-radius on bare legacy colour modifiers (.but.jb-blue, .but.b-blue-2,
.but.c-blue-2.hollow, .but.c-green.hollow). Those rules are (0,2,0) and load
after public.css, so they out-rank .but.radius's 50px and flatten the publish
chip, which is newadd-btns but jb-blue radius, down to 4px.

The same leak also repainted every site-wide .but.jb-blue outside the panel
with the flat #3b82f6 skin instead of the legacy gradient, so login, submit,
badge and cashier buttons lost their reference surface on that route too.

Scope the four rules to .uc-content, the shell every credits panel and its
modals render inside (templates/pages/page-profile.php wraps
templates/parts/user-center/panel.php). The panel keeps its own button skins
and the top-nav chips get the reference pill back.

The .btn-block restatement that sat in the same block is byte-identical to
the one in core/buttons.css, which loads earlier on every page, so it could
never win; it is dropped rather than scoped.

Verified: node --test across tests/ (604 passing), plus a stubbed navbar
rendered against each route's real stylesheet set measuring the computed
border-radius of the publish chip and of the panel's own buttons. A new
contract test fails on the previous stylesheet and passes on this one.
2026-10-15 23:22:18 +08:00
kate522 ee39024549 fix(header): put the top-nav clusters on one horizontal line
The top-nav account row carried min-height: 50px on top of the 8px
margin-top that Bootstrap base .navbar-form rule keeps at >=768px (the
override only zeroes the horizontal margins). The 8px was counted twice,
so the row ran from 8px to 58px: its icon tiles landed 8px below the 25px
midline that the 50px menu row and the logo define, and the navbar grew
from 50px to 58px, which also broke the body.nav-fixed offset.

Drop the min-height so the row takes its natural 34px height from the
icon tiles, and give the theme/language group the same 34px cross-size so
its 29px toggle shares that midline instead of sitting 2.5px above it.
Measured against the reference theme, the navbar goes from 66px to
50.05px (reference 50.28px) and the logo, menu, publish chip, VIP chip,
theme toggle and account icons all centre on 25px.

The desktop navbar stays visible below 991px (only the menu folds into a
dropdown there), so the same fix applies at the mobile and tablet
breakpoints: the top bar drops from 68px to 52.05px.

Verified: node --test across tests/ (603 passing), and a stubbed navbar
render measured in a headless browser against the reference theme
stylesheet.
2026-10-15 21:03:27 +08:00
kate522 5810693e01 fix(header): restore the payvip surface on the top-nav VIP chip
The 开通会员 chip rendered in the .jb-red orange gradient instead of the
legacy metallic .payvip-icon gradient. The markup always carried
payvip-icon, but core/buttons.css restates the legacy .but block and is
bundled into public.css, which loads after main.css. Same specificity,
later in the cascade, so its background / color / border / text-shadow
declarations outrank .payvip-icon and replace the payvip surface with
whatever .jb-* skin happens to be on the element.

The reference theme has no such compatibility layer, so there its
.payvip-icon declarations are the last word and the chip shows the
metallic gradient with #ece0e0 text, no border and no text shadow.

A previous fix restored the surface only for the user-card CTA, scoped to
.aksrt-header-userbox .pay-vip.but.payvip-icon, which left the top-nav
chip (and the guest CTA, and the user-card status bar) on the wrong
gradient. Re-assert the legacy .payvip-icon declarations unscoped in
header-userbox.css, which loads after core/buttons.css, and complete the
property set with border: none and text-shadow: none so the chip no
longer carries the 1px transparent border the .but block adds.

Verified with a cascade resolver that replays the real stylesheet load
order (bootstrap, main, public, header-userbox) against the reference
theme's own order: background, color, --this-color, border, text-shadow,
border-radius, padding, margin, font-size, display, overflow and position
now resolve identically for both the publish and the VIP chip.
2026-10-15 13:44:01 +08:00
kate522 0722c4f00e fix(header): restore reference VIP chip geometry and cluster split
The VIP chip never received the reference .but geometry. Commit f03d72c8
narrowed the legacy selector from a descendant to a child combinator as a
side effect of an unrelated user-popover fix, and the contract test then
pinned the narrowed form. Because the VIP anchor lives inside the
hover-show wrapper rather than being a direct child of .navbar-but, it
fell back to the bare .but defaults: 15px type instead of 0.8em, no
0.4em margin, and no padding override.

Restore the descendant selector so both chips share one geometry:

- .navbar-top .navbar-but .but is byte-identical to the reference again,
  giving the VIP chip font-size 0.8em, padding 0.3em 1em !important and
  margin 0.4em
- the publish and VIP anchors are plain inline-block .but elements again
  (the merged flex container no longer blockifies them)
- ml10 is back on the VIP anchor; .navbar-top .navbar-but .but overrides
  it exactly as it does in the reference, so it stays markup parity
  without changing the computed margin

The right cluster was also merged into one .aksrt-navbar-actions flex
container with a 10px gap and zero padding, while the reference emits
three floated .navbar-form.navbar-right groups whose 15px padding is what
produces the inter-cluster spacing. Split them again: publish/VIP in
.navbar-but, theme toggle plus language switcher in a flex ac group, and
the account row in its own group. Measured against the reference, the
publish-to-VIP gap was 19.6px instead of 9.6px, and VIP-to-theme-toggle
was 14.8px instead of 34.8px.

The container rules in header-userbox.css are re-anchored onto the
reference structure: .aksrt-navbar-account owns the only remaining flex
context, .toggle-radius and .aksrt-navbar-language are scoped through
.navbar-form, and the hover-show rule targets .navbar-but. Coverage of
those selectors is unchanged, so the theme toggle, language switcher and
message icon keep their current geometry.

The .aksrt-navbar-actions wrapper and its gap are gone; a negative
assertion now guards against reintroducing them.

Verified: node --test across tests/ (603 passing), php -l on the changed
PHP files, and a stubbed render of aksrt_menu_button() for guest,
logged-in and layout 2.
2026-10-15 12:19:24 +08:00
kate522 4f2edebab6 fix(header): align publish and VIP CTAs with reference theme
The top-nav publish chip and open-membership chip carried an AKSRT-only
class layer (navbar-cta / navbar-cta-text / navbar-newposts /
navbar-vip-hover) plus a matching set of CSS overrides that replaced the
legacy .but geometry: margin 0.4em -> 0, an extra min-height and flex gap,
an FA5 pencil glyph that is not the reference pencil, and label text
wrapped in a span. The VIP hover card also had its own positioning
override instead of the reference static position.

Drop that layer and emit the reference class combination verbatim so the
already byte-identical legacy stylesheet owns the geometry again:

- publish chip renders newadd-btns but nowave <skin> [radius] btn-newadd
  with the label echoed as saved (no injected icon, no span)
- VIP chip renders pay-vip|signin-loader but jb-red radius payvip-icon
  with the vip_1 SVG at em12 mr3, inside span.hover-show.inline-block
- user-card CTA uses the vip_1 SVG (em12 mr6) instead of fa-gem and drops
  vip-level, matching zib_get_header_payvip_icon()
- both buttons now honour Settings::close_sign(), matching the reference
  zib_is_close_sign() gate and the floating publish button
- header-userbox.css keeps one rule that restores the hover wrapper to a
  plain block container so .sub-vip-card takes its reference static
  position; every other override is gone
- nav_newposts_text default drops the duplicated fas and uses the FA5
  spelling of the reference pencil (fa-pencil-alt, not fa-pen)

Dead code removed: aksrt_normalize_header_cta_label(), the unreachable
multi-type dropdown branch of aksrt_render_new_add_btns() (only 'post' is
supported and only ['post'] is ever passed), and the
newadd-btns__toggle / newadd-btns__item / drop-newadd markup it emitted.

ml10 is no longer set on the VIP anchor: .aksrt-navbar-actions already
supplies the 10px gap the reference gets from ml10, and keeping both
would double it to 20px.

No migration for sites that already stored the old nav_newposts_text.

Verified: node --test across tests/ (603 passing), php -l on the changed
PHP files, and a stubbed render of every publish/VIP branch.
2026-10-15 11:54:54 +08:00
26 changed files with 552 additions and 382 deletions

No files matched your search

+3 -2
View File
@@ -709,8 +709,9 @@
color: var(--color-primary, #f04494); color: var(--color-primary, #f04494);
} }
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */ /* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
.widget a.but { .widget a.but,
.widget a.img-badge {
color: var(--this-color, #fff); color: var(--this-color, #fff);
} }
+1 -1
View File
@@ -1 +1 @@
.posts-row{margin-right:-8px;margin-bottom:15px;margin-left:-8px}.posts-row>*:not(.post_ajax_loader){margin-right:8px!important;margin-left:8px!important}.posts-row>.post_ajax_loader .posts-item,.posts-row>.post_ajax_loader .posts-mini{margin-right:8px;margin-left:8px}.posts-item{margin:15px 0;padding:20px;overflow:hidden;background:var(--main-bg-color, #fff);border-radius:var(--main-radius, 8px);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08));transition:.2s}.posts-item.flex,.posts-item .flex,.posts-mini .flex{display:flex}.posts-item .flex.xx,.posts-mini .flex.xx{flex-direction:column}.posts-item .flex.ac,.posts-mini .flex.ac{align-items:center}.posts-item .flex.jsb,.posts-mini .flex.jsb{justify-content:space-between}.posts-item .flex1,.posts-mini .flex1{flex:1;overflow:hidden}.posts-item .post-graphic:not(.order1){margin-right:20px}.posts-item .order1.post-graphic{margin-right:0;margin-left:20px}.posts-item .item-thumbnail{position:relative;display:block;width:190px;height:0;padding-bottom:var(--posts-list-scale, 70%);overflow:hidden;border-radius:var(--main-radius, 8px)}.item-thumbnail.contain img{object-fit:contain}.swiper-slide.gradient-bg .dplayer-poster img,.swiper-slide.gradient-bg>*>img{object-fit:contain!important;border-radius:0!important}.item-thumbnail .swiper-wrapper{height:100%!important}.list .item-thumbnail .aksrt-slider,.list .item-thumbnail.new-swiper{position:absolute;width:100%;height:100%;margin:0!important}.item-thumbnail .dplayer.dplayer-thumb{position:absolute;inset:0;z-index:1;padding-bottom:var(--posts-list-scale, 70%);background:#000;opacity:0;transition:opacity .3s}.card .item-thumbnail .dplayer.dplayer-thumb{padding-bottom:var(--posts-card-scale, 70%)}.item-thumbnail .dplayer-thumb video,.item-thumbnail .swiper-wrapper,.item-thumbnail img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.item-thumbnail .swiper-pagination{right:0!important;bottom:0!important;left:0!important;padding:0 2px!important;text-align:center!important}.posts-item.thumb-dplayer-playing .dplayer-thumb{opacity:1}.posts-item.thumb-dplayer-playing .abs-center{opacity:0}.posts-item .item-heading{display:-webkit-box;max-height:2.8em;margin-top:0;margin-bottom:5px;overflow:hidden;font-size:18px;line-height:1.4em;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2}.font-bold{font-weight:700}.list.style2 .item-thumbnail{width:130px}.list.style2 .item-heading{display:unset;max-height:1.4em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.posts-item .item-heading>a{color:var(--key-color, #333)}.posts-item .item-heading>a:hover{color:var(--focus-color, #f04494)}.posts-item .item-excerpt{margin:0 0 6px;overflow:hidden;color:var(--muted-color, #777);text-overflow:ellipsis;white-space:nowrap}.posts-item .item-meta item{margin-right:8px;font-size:13px}.posts-item .item-meta svg.icon,.posts-mini .item-meta svg.icon{display:inline-block;width:1em;height:1em;margin-right:2px;fill:currentColor;overflow:hidden;vertical-align:-.15em}.posts-item .meta-right item:last-child{margin-right:0}.item-meta .avatar-mini{width:20px;height:20px;border-radius:50%;transform:translateY(-1px)}.item-meta .meta-pay{padding:0 4px}.item-meta .meta-pay img{width:1em;height:1em;object-fit:contain;vertical-align:-.15em}.posts-item .item-meta a,.posts-mini .item-meta a{color:inherit}.posts-item .item-tags a{margin-right:5px;padding:2px 5px;font-size:11px}.posts-item .item-tags a .fa{font-size:1em}.posts-item.card{display:inline-block;width:calc(33.333% - 16px);margin:8px;padding:10px;vertical-align:top;transition:.3s}.posts-item.card:last-child{margin-bottom:20px}.posts-item.card .item-body{width:100%;margin:10px 0 0;padding:0}.posts-item.card .item-meta item{font-size:12px}.posts-mini-con .item-heading,.posts-item.card .item-heading{font-size:14px}.posts-item.card .item-heading{min-height:2.8em}.posts-item.card .item-tags{min-height:23px}.posts-item.card .item-thumbnail{width:100%;padding-bottom:var(--posts-card-scale, 70%)}.posts-item.card.style2{background:transparent;box-shadow:none}.posts-item.card.style2:hover{background:var(--main-bg-color, #fff);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08))}.posts-item.card.style3{padding:0}.posts-item.card.style3 .item-thumbnail,.posts-item.card.style3 .item-thumbnail img{border-radius:var(--main-radius, 8px) var(--main-radius, 8px) 0 0}.posts-item.card.style3 .item-body{margin:0;padding:10px}.posts-item.mult-thumb .thumb-items{margin:0 -3px 6px}.posts-item.mult-thumb .thumb-items>span{position:relative;display:inline-block;width:calc(25% - 6px);height:0;margin:0 3px;padding-bottom:calc(var(--posts-list-scale, 70%) / 4)}.posts-item.mult-thumb .thumb-items>span>img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.posts-item.mult-thumb .thumb-items.contain>span{overflow:hidden;background:var(--main-border-color, rgba(50, 50, 50, .06));border-radius:var(--main-radius, 8px)}.posts-item.mult-thumb .thumb-items.contaiLine truncated .posts-row{margin-right:-8px;margin-bottom:15px;margin-left:-8px}.posts-row>*:not(.post_ajax_loader){margin-right:8px!important;margin-left:8px!important}.posts-row>.post_ajax_loader .posts-item,.posts-row>.post_ajax_loader .posts-mini{margin-right:8px;margin-left:8px}.posts-item{margin:15px 0;padding:20px;overflow:hidden;background:var(--main-bg-color, #fff);border-radius:var(--main-radius, 8px);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08));transition:.2s}.posts-item.flex,.posts-item .flex,.posts-mini .flex{display:flex}.posts-item .flex.xx,.posts-mini .flex.xx{flex-direction:column}.posts-item .flex.ac,.posts-mini .flex.ac{align-items:center}.posts-item .flex.jsb,.posts-mini .flex.jsb{justify-content:space-between}.posts-item .flex1,.posts-mini .flex1{flex:1;overflow:hidden}.posts-item .post-graphic:not(.order1){margin-right:20px}.posts-item .order1.post-graphic{margin-right:0;margin-left:20px}.posts-item .item-thumbnail{position:relative;display:block;width:190px;height:0;padding-bottom:var(--posts-list-scale, 70%);overflow:hidden;border-radius:var(--main-radius, 8px)}.item-thumbnail.contain img{object-fit:contain}.swiper-slide.gradient-bg .dplayer-poster img,.swiper-slide.gradient-bg>*>img{object-fit:contain!important;border-radius:0!important}.item-thumbnail .swiper-wrapper{height:100%!important}.list .item-thumbnail .aksrt-slider,.list .item-thumbnail.new-swiper{position:absolute;width:100%;height:100%;margin:0!important}.item-thumbnail .dplayer.dplayer-thumb{position:absolute;inset:0;z-index:1;padding-bottom:var(--posts-list-scale, 70%);background:#000;opacity:0;transition:opacity .3s}.card .item-thumbnail .dplayer.dplayer-thumb{padding-bottom:var(--posts-card-scale, 70%)}.item-thumbnail .dplayer-thumb video,.item-thumbnail .swiper-wrapper,.item-thumbnail img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.item-thumbnail .swiper-pagination{right:0!important;bottom:0!important;left:0!important;padding:0 2px!important;text-align:center!important}.posts-item.thumb-dplayer-playing .dplayer-thumb{opacity:1}.posts-item.thumb-dplayer-playing .abs-center{opacity:0}.posts-item .item-heading{display:-webkit-box;max-height:2.8em;margin-top:0;margin-bottom:5px;overflow:hidden;font-size:18px;line-height:1.4em;white-space:normal;-webkit-box-orient:vertical;-webkit-line-clamp:2}.font-bold{font-weight:700}.list.style2 .item-thumbnail{width:130px}.list.style2 .item-heading{display:unset;max-height:1.4em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.posts-item .item-heading>a{color:var(--key-color, #333)}.posts-item .item-heading>a:hover{color:var(--focus-color, #f04494)}.posts-item .item-excerpt{margin:0 0 6px;overflow:hidden;color:var(--muted-color, #777);text-overflow:ellipsis;white-space:nowrap}.posts-item .item-meta item{margin-right:8px;font-size:13px}.posts-item .item-meta svg.icon,.posts-mini .item-meta svg.icon{display:inline-block;width:1em;height:1em;margin-right:2px;fill:currentColor;overflow:hidden;vertical-align:-.15em}.posts-item .meta-right item:last-child{margin-right:0}.item-meta .avatar-mini{width:20px;height:20px;border-radius:50%;transform:translateY(-1px)}.item-meta .meta-pay{padding:0 4px}.item-meta .meta-pay img{width:1em;height:1em;object-fit:contain;vertical-align:-.15em}.posts-item .item-meta a,.posts-mini .item-meta a{color:inherit}.posts-item .item-tags a{margin-right:5px;padding:2px 5px;font-size:11px}.posts-item .item-tags a .fa{font-size:1em}.posts-item.card{display:inline-block;width:calc(33.333% - 16px);margin:8px;padding:10px;vertical-align:top;transition:.3s}.posts-item.card:last-child{margin-bottom:20px}.posts-item.card .item-body{width:100%;margin:10px 0 0;padding:0}.posts-item.card .item-meta item{font-size:12px}.posts-mini-con .item-heading,.posts-item.card .item-heading{font-size:14px}.posts-item.card .item-heading{min-height:2.8em}.posts-item.card .item-tags{min-height:23px}.posts-item.card .item-thumbnail{width:100%;padding-bottom:var(--posts-card-scale, 70%)}.posts-item.card.style2{background:transparent;box-shadow:none}.posts-item.card.style2:hover{background:var(--main-bg-color, #fff);box-shadow:0 0 10px var(--main-shadow, rgba(116, 116, 116, .08))}.posts-item.card.style3{padding:0}.posts-item.card.style3 .item-thumbnail,.posts-item.card.style3 .item-thumbnail img{border-radius:var(--main-radius, 8px) var(--main-radius, 8px) 0 0}.posts-item.card.style3 .item-body{margin:0;padding:10px}.posts-item.mult-thumb .thumb-items{margin:0 -3px 6px}.posts-item.mult-thumb .thumb-items>span{position:relative;display:inline-block;width:calc(25% - 6px);height:0;margin:0 3px;padding-bottom:calc(var(--posts-list-scale, 70%) / 4)}.posts-item.mult-thumb .thumb-items>span>img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}.posts-item.mult-thumb .thumb-items.contain>span{overflow:hidden;background:var(--main-border-color, rgba(50, 50, 50, .06));border-radius:var(--main-radius, 8px)}.posts-item.mult-thumb .thumb-items.contaiLine truncated
+48 -70
View File
@@ -1095,7 +1095,7 @@ strong, b {
/* gradient modifiers (legacy .jb-*) */ /* gradient modifiers (legacy .jb-*) */
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); } .jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
.jb-yellow { --this-bg: linear-gradient(135deg, #f99d4d 10%, #f7631d 100%); } .jb-yellow { --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%); }
.jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); } .jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); } .jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); } .jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
@@ -2714,27 +2714,29 @@ main.container,
* shell anchoring plus the account action-row geometry. * shell anchoring plus the account action-row geometry.
*/ */
/* The desktop action cluster owns one horizontal alignment context. */ /* The reference keeps the right cluster as three floated .navbar-form groups.
.navbar-top .aksrt-navbar-actions { * Bootstrap's base .navbar-form rule carries margin-top: 8px and the >=768px
display: flex; * override only zeroes the horizontal margins, so every right-cluster group
align-items: center; * starts 8px below the top of .navbar-collapse. The 50px menu row and the logo
gap: 10px; * put the navbar midline at 25px, so a 34px cross-size lands a group's midline
min-height: 50px; * at 8 + 17 = 25px. The publish/VIP chips already reach that line through their
margin-top: 0; * own 0.4em margin stacked on the group's 8px; the account row reaches it from
margin-bottom: 0; * its 34px icon tiles, and the theme group needs the same cross-size because
padding: 0; * its 29px toggle is otherwise 2.5px high. Nothing here may restate a 50px
} * min-height: it would double-count the 8px and push that group's content 8px
* below the line while inflating the navbar from 50px to 58px. */
.navbar-top .aksrt-navbar-actions > *, .navbar-top .aksrt-navbar-account {
.navbar-top .aksrt-navbar-account,
.navbar-top .aksrt-navbar-account > li {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
flex: 0 0 auto; gap: 4px;
margin: 0;
} }
.navbar-top .aksrt-navbar-account > li { .navbar-top .aksrt-navbar-account > li {
position: relative; position: relative;
display: inline-flex;
align-items: center;
flex: 0 0 auto;
padding: 0; padding: 0;
overflow: visible; overflow: visible;
} }
@@ -2747,39 +2749,6 @@ main.container,
margin: 0; margin: 0;
} }
.navbar-top .aksrt-navbar-account {
gap: 4px;
min-height: 50px;
margin: 0;
}
.navbar-top .aksrt-navbar-account.navbar-text {
float: none;
}
.navbar-top .aksrt-navbar-actions .navbar-cta {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
min-height: 29px;
margin: 0;
padding: .3em 1em !important;
border-radius: var(--radius-btn, 4px);
font-size: .8em;
line-height: 1.44;
}
.navbar-top .aksrt-navbar-actions .navbar-cta.radius {
border-radius: 50px;
}
.navbar-top .aksrt-navbar-actions .navbar-cta > .icon {
width: 1.2em;
height: 1.2em;
flex: 0 0 auto;
}
.navbar-top .aksrt-nav-icon, .navbar-top .aksrt-nav-icon,
.navbar-top .aksrt-nav-avatar { .navbar-top .aksrt-nav-avatar {
display: inline-flex !important; display: inline-flex !important;
@@ -2812,33 +2781,34 @@ main.container,
object-fit: cover; object-fit: cover;
} }
.navbar-top .aksrt-navbar-actions .toggle-radius { /* The account-row chips, the theme and the language toggles all want the 29px
* midline circle. The popover's quick-login buttons are .toggle-radius too and
* also live inside .navbar-form, but they must keep the .social_loginbar
* geometry from main.css: in a flex container flex-basis outranks width, so
* without this exemption the buttons collapse to 29px and lose the 5px side
* margins that space them apart. */
.navbar-top .navbar-form .toggle-radius:not(.social-login-item):not(.xh-social-item) {
flex: 0 0 29px; flex: 0 0 29px;
margin: 0; margin: 0;
vertical-align: middle; vertical-align: middle;
} }
.navbar-top .aksrt-navbar-language { /* Match the account row's 34px cross-size so the 29px toggle shares the 25px
* midline instead of sitting 2.5px above it. */
.navbar-top .navbar-form.flex.ac {
min-height: 34px;
}
.navbar-top .navbar-form .aksrt-navbar-language {
margin: 0; margin: 0;
} }
/* Anchor the VIP preview to the full navigation row instead of relying on /* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
* the absolute panel's static position beside the compact CTA. */ * absolutely positioned .sub-vip-card takes its normal-flow static position.
.navbar-top .aksrt-navbar-actions .navbar-vip-hover { * The shared action cluster otherwise forces inline-flex on every child, which
position: relative; * would move that static position to the flex container's cross-start. */
align-self: stretch; .navbar-top .navbar-but > .hover-show {
align-items: center; display: inline-block;
overflow: visible;
}
.navbar-top .aksrt-navbar-actions .navbar-vip-hover > .sub-vip-card {
top: 100%;
right: 0;
left: auto;
width: max-content;
max-width: calc(100vw - 24px);
margin: 0;
z-index: 1002;
} }
/* Keep the drop-down search panel above a fixed header (z-index: 999). */ /* Keep the drop-down search panel above a fixed header (z-index: 999). */
@@ -2886,11 +2856,19 @@ main.container,
padding: 6px; padding: 6px;
} }
/* The later .but compatibility layer otherwise replaces the legacy payvip surface. */ /* core/buttons.css restates the legacy .but block after main.css, so its
.aksrt-header-userbox .pay-vip.but.payvip-icon { * background / color / border / text-shadow declarations outrank .payvip-icon
* and replace the payvip surface with whatever .jb-* skin is on the element.
* The reference theme has no such compatibility layer, so its .payvip-icon
* declarations are the last word in the cascade. Re-assert them here, which
* loads after core/buttons.css, so every payvip surface (top-nav chip, user
* card status bar and CTA, guest CTA) keeps the legacy metallic gradient. */
.payvip-icon {
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%); background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
color: #ece0e0; color: #ece0e0;
--this-color: #ece0e0; --this-color: #ece0e0;
border: none;
text-shadow: none;
} }
/* Keep the header check-in action flush with the VIP row's padded inner width. */ /* Keep the header check-in action flush with the VIP row's padded inner width. */
+1 -1
View File
@@ -1 +1 @@
:root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated :root{--color-primary: #f04494;--color-primary-hover: #e0307e;--color-primary-active: #c9206d;--color-primary-soft: rgba(240, 68, 148, .1);--color-primary-softer: rgba(240, 68, 148, .05);--color-on-primary: #FFFFFF;--color-ink: #333333;--color-ink-2: #4e5358;--color-ink-3: #777777;--color-ink-4: #999999;--surface-page: #f5f6f7;--surface-1: #FFFFFF;--surface-2: #eeeeee;--surface-3: #e9ecef;--surface-4: #f8f8f8;--color-line: rgba(50, 50, 50, .06);--color-line-soft: rgba(0, 0, 0, .03);--color-line-stronger: rgba(50, 50, 50, .12);--color-success: #10B981;--color-warning: #F59E0B;--color-error: #EF4444;--color-info: #3B82F6;--color-accent: #18a058;--color-accent-soft: rgba(24, 160, 88, .12);--color-accent-shadow: rgba(24, 160, 88, .28);--font-display: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: .875rem;--text-md: 1.125rem;--text-lg: 1.25rem;--text-xl: 1.5rem;--text-2xl: 1.875rem;--text-3xl: 2.25rem;--text-4xl: 3rem;--text-5xl: 3.75rem;--leading-tight: 1.2;--leading-snug: 1.4;--leading-normal: 1.6;--leading-relaxed: 1.6;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-7: 32px;--space-8: 40px;--space-9: 48px;--space-10: 64px;--space-11: 80px;--space-12: 96px;--radius-xs: 3px;--radius-sm: 4px;--radius: 8px;--radius-lg: 8px;--radius-xl: 12px;--radius-2xl: 16px;--radius-pill: 100px;--radius-avatar: 50%;--shadow-xs: 0 1px 2px rgba(0, 0, 0, .03);--shadow-sm: 0 0 10px rgba(116, 116, 116, .08);--shadow: 0 0 10px rgba(116, 116, 116, .12);--shadow-lg: 0 12px 32px rgba(18, 18, 18, .1);--shadow-drawer: 0 18px 60px rgba(0, 0, 0, .2);--nav-height: 60px;--container: 1200px;--container-wide: 1440px;--container-read: 760px;--container-page: 880px;--sidebar-width: 311px;--content-gap: 24px;--section-gap: 24px;--sidebar-bp: 992px;--ease: cubic-bezier(.4, 0, .2, 1);--ease-out: cubic-bezier(.16, 1, .3, 1);--ease-spring: cubic-bezier(.34, 1.56, .64, 1);--dur-fast: .15s;--dur: .25s;--dur-slow: .4s;--focus-ring: 0 0 2px 3px rgba(253, 83, 161, .4);--z-dropdown: 100;--z-sticky: 200;--z-fixed: 300;--z-overlay: 400;--z-modal: 500;--z-popover: 600;--z-toast: 700;--z-tooltip: 800;--radius-btn: 4px;--btn-sm-h: 28px;--btn-md-h: 34px;--btn-lg-h: 44px;--card-padding: 15px;--card-padding-sm: 10px;--card-gap: 16px;--card-gap-lg: 20px;--color-primary-light: var(--color-primary-soft);--color-surface: var(--surface-1);--color-line-strong: var(--color-line);--color-bg-hover: var(--surface-2);--color-border-light: var(--color-line-soft);--color-fill: var(--surface-2);--color-warning-bg: #fffbe6;--color-warning-text: #ad6800;--text-secondary: var(--color-ink-3);--text-tertiary: var(--color-ink-4);--surface-active: var(--surface-2);--surface-card: var(--surface-1);--surface-hover: var(--surface-2);--surface-secondary: var(--surface-2);--radius-code: var(--radius);--radius-full: 100px;--shadow-xl: 0 0 10px rgba(116, 116, 116, .12);--space-16: 64px;--z-floating: 900;--transition-fast: .15s ease;--transition-base: .25s ease;--header-bg: var(--blur-bg);--header-color: var(--color-ink-2);--footer-bg: var(--surface-1);--footer-color: var(--color-ink-4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--font-heading: var(--font-ui);--radius-card: var(--radius);--radius-md: var(--radius);--color-primary-50: var(--color-primary-soft);--color-primary-dark: var(--color-primary-active);--shadow-md: var(--shadow);--mini-radius: calc(var(--radius) / 1.4);--aksrt-avatar-size-xs: 24px;--aksrt-avatar-size-sm: 32px;--aksrt-avatar-size: 40px;--aksrt-avatar-size-lg: 56px;--aksrt-avatar-size-xl: 80px;--aksrt-card-thumb-scale: 56.25%;--aksrt-rose: #f43f5e;--aksrt-rose-soft: rgba(244, 63, 94, .12);--aksrt-blue: #3b82f6;--aksrt-blue-soft: rgba(59, 130, 246, .12);--aksrt-green: #10b981;--aksrt-green-soft: rgba(16, 185, 129, .12);--aksrt-orange: #f97316;--aksrt-orange-soft:rgba(249, 115, 22, .12);--aksrt-purple: #8b5cf6;--aksrt-purple-soft:rgba(139, 92, 246, .12);--aksrt-cyan: #06b6d4;--aksrt-cyan-soft: rgba(6, 182, 212, .12);--aksrt-yellow: #eab308;--aksrt-yellow-soft:rgba(234, 179, 8, .12);--aksrt-red: #ef4444;--aksrt-red-soft: rgba(239, 68, 68, .12);--aksrt-teal: #14b8a6;--aksrt-teal-soft: rgba(20, 184, 166, .12);--aksrt-indigo: #6366f1;--aksrt-indigo-soft:rgba(99, 102, 241, .12);--aksrt-pink: #ec4899;--aksrt-pink-soft: rgba(236, 72, 153, .12);--aksrt-vip-gold-1: #f6d365;--aksrt-vip-gold-2: #fda085;--aksrt-grad-sunset-1: #f093fb;--aksrt-grad-sunset-2: #f5576c;--aksrt-graLine truncated
+1 -1
View File
@@ -316,7 +316,7 @@
/* gradient modifiers (legacy .jb-*) */ /* gradient modifiers (legacy .jb-*) */
.jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); } .jb-red { --this-bg: linear-gradient(135deg, #fd7a64 10%, #fb2d2d 100%); }
.jb-yellow { --this-bg: linear-gradient(135deg, #f99d4d 10%, #f7631d 100%); } .jb-yellow { --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%); }
.jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); } .jb-blue { --this-bg: linear-gradient(135deg, #59c3fb 10%, #268df7 100%); }
.jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); } .jb-green { --this-bg: linear-gradient(135deg, #60e464 10%, #5cb85b 100%); }
.jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); } .jb-purple { --this-bg: linear-gradient(135deg, #f98dfb 10%, #ea00f9 100%); }
+47 -69
View File
@@ -7,27 +7,29 @@
* shell anchoring plus the account action-row geometry. * shell anchoring plus the account action-row geometry.
*/ */
/* The desktop action cluster owns one horizontal alignment context. */ /* The reference keeps the right cluster as three floated .navbar-form groups.
.navbar-top .aksrt-navbar-actions { * Bootstrap's base .navbar-form rule carries margin-top: 8px and the >=768px
display: flex; * override only zeroes the horizontal margins, so every right-cluster group
align-items: center; * starts 8px below the top of .navbar-collapse. The 50px menu row and the logo
gap: 10px; * put the navbar midline at 25px, so a 34px cross-size lands a group's midline
min-height: 50px; * at 8 + 17 = 25px. The publish/VIP chips already reach that line through their
margin-top: 0; * own 0.4em margin stacked on the group's 8px; the account row reaches it from
margin-bottom: 0; * its 34px icon tiles, and the theme group needs the same cross-size because
padding: 0; * its 29px toggle is otherwise 2.5px high. Nothing here may restate a 50px
} * min-height: it would double-count the 8px and push that group's content 8px
* below the line while inflating the navbar from 50px to 58px. */
.navbar-top .aksrt-navbar-actions > *, .navbar-top .aksrt-navbar-account {
.navbar-top .aksrt-navbar-account,
.navbar-top .aksrt-navbar-account > li {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
flex: 0 0 auto; gap: 4px;
margin: 0;
} }
.navbar-top .aksrt-navbar-account > li { .navbar-top .aksrt-navbar-account > li {
position: relative; position: relative;
display: inline-flex;
align-items: center;
flex: 0 0 auto;
padding: 0; padding: 0;
overflow: visible; overflow: visible;
} }
@@ -40,39 +42,6 @@
margin: 0; margin: 0;
} }
.navbar-top .aksrt-navbar-account {
gap: 4px;
min-height: 50px;
margin: 0;
}
.navbar-top .aksrt-navbar-account.navbar-text {
float: none;
}
.navbar-top .aksrt-navbar-actions .navbar-cta {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
min-height: 29px;
margin: 0;
padding: .3em 1em !important;
border-radius: var(--radius-btn, 4px);
font-size: .8em;
line-height: 1.44;
}
.navbar-top .aksrt-navbar-actions .navbar-cta.radius {
border-radius: 50px;
}
.navbar-top .aksrt-navbar-actions .navbar-cta > .icon {
width: 1.2em;
height: 1.2em;
flex: 0 0 auto;
}
.navbar-top .aksrt-nav-icon, .navbar-top .aksrt-nav-icon,
.navbar-top .aksrt-nav-avatar { .navbar-top .aksrt-nav-avatar {
display: inline-flex !important; display: inline-flex !important;
@@ -105,33 +74,34 @@
object-fit: cover; object-fit: cover;
} }
.navbar-top .aksrt-navbar-actions .toggle-radius { /* The account-row chips, the theme and the language toggles all want the 29px
* midline circle. The popover's quick-login buttons are .toggle-radius too and
* also live inside .navbar-form, but they must keep the .social_loginbar
* geometry from main.css: in a flex container flex-basis outranks width, so
* without this exemption the buttons collapse to 29px and lose the 5px side
* margins that space them apart. */
.navbar-top .navbar-form .toggle-radius:not(.social-login-item):not(.xh-social-item) {
flex: 0 0 29px; flex: 0 0 29px;
margin: 0; margin: 0;
vertical-align: middle; vertical-align: middle;
} }
.navbar-top .aksrt-navbar-language { /* Match the account row's 34px cross-size so the 29px toggle shares the 25px
* midline instead of sitting 2.5px above it. */
.navbar-top .navbar-form.flex.ac {
min-height: 34px;
}
.navbar-top .navbar-form .aksrt-navbar-language {
margin: 0; margin: 0;
} }
/* Anchor the VIP preview to the full navigation row instead of relying on /* The reference keeps the publish/VIP hover wrapper a plain inline-block, so the
* the absolute panel's static position beside the compact CTA. */ * absolutely positioned .sub-vip-card takes its normal-flow static position.
.navbar-top .aksrt-navbar-actions .navbar-vip-hover { * The shared action cluster otherwise forces inline-flex on every child, which
position: relative; * would move that static position to the flex container's cross-start. */
align-self: stretch; .navbar-top .navbar-but > .hover-show {
align-items: center; display: inline-block;
overflow: visible;
}
.navbar-top .aksrt-navbar-actions .navbar-vip-hover > .sub-vip-card {
top: 100%;
right: 0;
left: auto;
width: max-content;
max-width: calc(100vw - 24px);
margin: 0;
z-index: 1002;
} }
/* Keep the drop-down search panel above a fixed header (z-index: 999). */ /* Keep the drop-down search panel above a fixed header (z-index: 999). */
@@ -179,11 +149,19 @@
padding: 6px; padding: 6px;
} }
/* The later .but compatibility layer otherwise replaces the legacy payvip surface. */ /* core/buttons.css restates the legacy .but block after main.css, so its
.aksrt-header-userbox .pay-vip.but.payvip-icon { * background / color / border / text-shadow declarations outrank .payvip-icon
* and replace the payvip surface with whatever .jb-* skin is on the element.
* The reference theme has no such compatibility layer, so its .payvip-icon
* declarations are the last word in the cascade. Re-assert them here, which
* loads after core/buttons.css, so every payvip surface (top-nav chip, user
* card status bar and CTA, guest CTA) keeps the legacy metallic gradient. */
.payvip-icon {
background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%); background: linear-gradient(300deg, #4c4d51, #2a2a31 15%, #85858a 40%, #393a3c 60%, #393838 80%, #5e5f62 100%);
color: #ece0e0; color: #ece0e0;
--this-color: #ece0e0; --this-color: #ece0e0;
border: none;
text-shadow: none;
} }
/* Keep the header check-in action flush with the VIP row's padded inner width. */ /* Keep the header check-in action flush with the VIP row's padded inner width. */
-50
View File
@@ -166,56 +166,6 @@ html.dark .archive-timeline__badge {
font-size: 13px; font-size: 13px;
} }
/* 社交登录分隔线 */
.social-login-divider {
display: flex;
align-items: center;
margin: 24px 0 16px;
color: var(--color-ink-3, #94a3b8);
font-size: 13px;
}
.social-login-divider::before,
.social-login-divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--color-line, #e2e8f0);
}
.social-login-divider span {
padding: 0 16px;
white-space: nowrap;
}
.oauth-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 16px;
}
.oauth-btn {
width: 44px;
height: 44px;
border-radius: var(--radius-btn);
border: 1px solid var(--color-line);
display: flex;
align-items: center;
justify-content: center;
color: var(--color-ink-3);
transition: border-color 0.2s, color 0.2s;
}
.oauth-btn i {
font-size: 22px;
line-height: 1;
display: block;
}
.oauth-btn:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
.auth-footer-link { .auth-footer-link {
text-align: center; text-align: center;
margin-top: 20px; margin-top: 20px;
+3 -2
View File
@@ -50,8 +50,9 @@
color: var(--color-primary, #f04494); color: var(--color-primary, #f04494);
} }
/* .widget a 的链接色不得覆盖 .but 按钮自身配色(zibll 的 zib-widget 不设置链接色) */ /* .widget a 的链接色不得覆盖 .but / .img-badge 自身配色(zibll 的 zib-widget 不设置链接色) */
.widget a.but { .widget a.but,
.widget a.img-badge {
color: var(--this-color, #fff); color: var(--this-color, #fff);
} }
+9 -4
View File
@@ -6838,7 +6838,7 @@ badge.top,
padding-right: 5px; padding-right: 5px;
} }
.navbar-top .navbar-but > .but { .navbar-top .navbar-but .but {
font-size: 0.8em; font-size: 0.8em;
padding: 0.3em 1em !important; padding: 0.3em 1em !important;
margin: 0.4em; margin: 0.4em;
@@ -9655,11 +9655,16 @@ img.alignleft {
padding: 15px; padding: 15px;
} }
/* AKSRT 适配:站点行高(--leading-normal: 1.6)与参考主题 bootstrap /* AKSRT 适配:签到书签排版逐字对齐参考主题 bootstrap 的 body 计算值——
* 的 1.42857143 不同,用户卡片内的 img-badge(签到书签)钉住参考行高; * 行高 1.42857143(站点 --leading-normal 为 1.6)、字体族
* 字号已随 --text-base 对齐参考主题(14px),无需单独声明。 */ * "Helvetica Neue", Helvetica, Arial, sans-serif(站点 --font-body 为
* 现代系统栈),并还原浏览器默认的字体平滑与渲染(站点 html 重置为
* antialiased + optimizeLegibility);字号随 --text-base 已是参考的 14px。 */
.user-card .card-content .img-badge { .user-card .card-content .img-badge {
line-height: 1.42857143; line-height: 1.42857143;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: auto;
text-rendering: auto;
} }
.user-card name .display-name { .user-card name .display-name {
+1 -1
View File
@@ -1 +1 @@
body{--mian-max-width: 1200px;--posts-card-scale: 70%;--posts-list-scale: 70%;--theme-color: #f04494;--focus-color: var(--theme-color);--focus-shadow-color: rgba(253, 83, 161, .4);--key-color: #333;--main-color: #4e5358;--main-shadow: rgba(116, 116, 116, .08);--muted-color: #777;--muted-2-color: #999;--muted-3-color: #b1b1b1;--muted-4-color: #d2d2d2;--body-bg-color: #f5f6f7;--main-bg-color: #fff;--muted-bg-color: #eee;--muted-bg-2-color: #f6f7f8;--main-border-color: rgba(50, 50, 50, .06);--muted-border-color: rgba(0, 0, 0, .03);--main-radius: 8px;--mini-radius: calc(var(--main-radius) / 1.4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}.dark-theme{--key-color: #f8fafc;--main-color: #e5eef7;--main-shadow: rgba(24, 24, 24, .1);--muted-color: #b4b6bb;--muted-2-color: #888a8f;--muted-3-color: #636469;--muted-4-color: #43454a;--body-bg-color: #292a2d;--main-bg-color: #323335;--muted-bg-color: #2d2e31;--muted-bg-2-color: #37383a;--main-border-color: rgba(114, 114, 114, .1);--muted-border-color: rgba(184, 184, 184, .02);--blur-bg: rgba(50, 51, 53, .8);--float-btn-bg: rgba(70, 70, 70, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal}.dark::view-transition-old(root){z-index:1}.dark::view-transition-new(root){z-index:2147483646}::view-transition-old(root){z-index:2147483646}::view-transition-new(root){z-index:1}@supports not ((-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(20px))){body{--blur-bg: #ffffff}body.dark-theme{--blur-bg: #37383a}}body:has(.modal.in),body.modal-open{--blur-bg: #ffffff}body.dark-theme:has(.modal.in),body.dark-theme.modal-open{--blur-bg: #37383a}body.fps-accelerat{--blur-bg: #ffffff}body.fps-accelerat.dark-theme{--blur-bg: #37383a}body.fps-accelerat *{-webkit-backdrop-filter:unset!important;backdrop-filter:unset!important}a[href],.relates-thumb li a>.post-info,.more-posts a>span{transition:color .2s,background .3s,opacity .3s,box-shadow .3s,transform .3s}.main-shadow{transition:box-shadow .3s}.but:focus,.but:hover,.progress,.navbar-nav>li:before{transition:.3s}.navbar-top .sub-menu,.theme-popover,.hover-show .hover-show-con{opacity:0;transform:translateY(6px);transition:.4s}.navbar-top li:hover>.sub-menu,.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{opacity:1;transform:translateY(0)}.hover-show{position:relative;cursor:pointer;z-index:10}.hover-show .hover-show-con{position:absolute;visibility:hidden}.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{visibility:unset}.match-ok,.navbar-top .sub-menu li>a,.ic-close{transition:.3s}.ic-close:hover{transform:rotate(270deg);color:inherit}.social_loginbar a,.item-thumbnail img,.dosc-nav .fa{transition:.2s ease-out}.hover-zoom:hover,.hover-zoom-img:hover img,.social_loginbar .xh-social-item:hover,.social_loginbar .social-login-item:hover{transform:scale(1.05)}.hover-zoom-sm:hover,.hover-zoom-img-sm:hover img,.posts-mini:hover img,.posts-item:hover .item-thumbnail img,.posts-item.mult-thumb .thumb-items>span>img:hover{transform:scale(1.02)}.main-min-height{min-height:calc(100vh - 280px)}.main-shadow{box-shadow:0 0 10px var(--main-shadow)}.key-color{color:var(--key-color)}.focus-color{--this-color: var(--focus-color);color:var(--focus-color)}.muted-color{color:var(--muted-color)}.muted-2-color{color:var(--muted-2-color)}.muted-3-color{color:var(--muted-3-color)}.main-bg{background:var(--main-bg-color)}.placeholder{background:linear-gradient(118deg,#7979791a 25%,#79797933 37%,#7979791a 63%);opacity:1;border-radius:4px;-webkit-animation:placeholder 1.5s ease infinite!important;animation:placeholder 1.5s ease infinite!important;background-size:400% 100%!important}.placeholder.img{height:119px}.placeholder.t1{height:24px;width:80%}.placeholder.k1{height:20px;width:60%}.placeholder.k2{height:20px}.placeholder.k3{height:15px}.placeholder.s1{width:70px;height:15px;display:inline-block}@-webkit-keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@-webkit-keyframes fade{0%{opacity:0}to{opacity:1}}@keyframes fade{0%{opacity:0}to{opacity:1}}.ani.shake{-webkit-animation:shake .3s ease-in!important;animation:shake .3s ease-in!important}@-webkit-keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}@keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}.obs-animate{opacity:0;transition:opacity .6sLine truncated body{--mian-max-width: 1200px;--posts-card-scale: 70%;--posts-list-scale: 70%;--theme-color: #f04494;--focus-color: var(--theme-color);--focus-shadow-color: rgba(253, 83, 161, .4);--key-color: #333;--main-color: #4e5358;--main-shadow: rgba(116, 116, 116, .08);--muted-color: #777;--muted-2-color: #999;--muted-3-color: #b1b1b1;--muted-4-color: #d2d2d2;--body-bg-color: #f5f6f7;--main-bg-color: #fff;--muted-bg-color: #eee;--muted-bg-2-color: #f6f7f8;--main-border-color: rgba(50, 50, 50, .06);--muted-border-color: rgba(0, 0, 0, .03);--main-radius: 8px;--mini-radius: calc(var(--main-radius) / 1.4);--blur-bg: rgba(255, 255, 255, .8);--float-btn-bg: rgba(200, 200, 200, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}.dark-theme{--key-color: #f8fafc;--main-color: #e5eef7;--main-shadow: rgba(24, 24, 24, .1);--muted-color: #b4b6bb;--muted-2-color: #888a8f;--muted-3-color: #636469;--muted-4-color: #43454a;--body-bg-color: #292a2d;--main-bg-color: #323335;--muted-bg-color: #2d2e31;--muted-bg-2-color: #37383a;--main-border-color: rgba(114, 114, 114, .1);--muted-border-color: rgba(184, 184, 184, .02);--blur-bg: rgba(50, 51, 53, .8);--float-btn-bg: rgba(70, 70, 70, .4);--header-bg: var(--blur-bg);--header-color: var(--main-color);--footer-bg: var(--main-bg-color);--footer-color: var(--muted-2-color)}::view-transition-old(root),::view-transition-new(root){animation:none;mix-blend-mode:normal}.dark::view-transition-old(root){z-index:1}.dark::view-transition-new(root){z-index:2147483646}::view-transition-old(root){z-index:2147483646}::view-transition-new(root){z-index:1}@supports not ((-webkit-backdrop-filter: blur(20px)) or (backdrop-filter: blur(20px))){body{--blur-bg: #ffffff}body.dark-theme{--blur-bg: #37383a}}body:has(.modal.in),body.modal-open{--blur-bg: #ffffff}body.dark-theme:has(.modal.in),body.dark-theme.modal-open{--blur-bg: #37383a}body.fps-accelerat{--blur-bg: #ffffff}body.fps-accelerat.dark-theme{--blur-bg: #37383a}body.fps-accelerat *{-webkit-backdrop-filter:unset!important;backdrop-filter:unset!important}a[href],.relates-thumb li a>.post-info,.more-posts a>span{transition:color .2s,background .3s,opacity .3s,box-shadow .3s,transform .3s}.main-shadow{transition:box-shadow .3s}.but:focus,.but:hover,.progress,.navbar-nav>li:before{transition:.3s}.navbar-top .sub-menu,.theme-popover,.hover-show .hover-show-con{opacity:0;transform:translateY(6px);transition:.4s}.navbar-top li:hover>.sub-menu,.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{opacity:1;transform:translateY(0)}.hover-show{position:relative;cursor:pointer;z-index:10}.hover-show .hover-show-con{position:absolute;visibility:hidden}.hover-show:hover .hover-show-con,.hover-show:hover~.hover-show-con,.hover-show:hover~.sub-menu{visibility:unset}.match-ok,.navbar-top .sub-menu li>a,.ic-close{transition:.3s}.ic-close:hover{transform:rotate(270deg);color:inherit}.social_loginbar a,.item-thumbnail img,.dosc-nav .fa{transition:.2s ease-out}.hover-zoom:hover,.hover-zoom-img:hover img,.social_loginbar .xh-social-item:hover,.social_loginbar .social-login-item:hover{transform:scale(1.05)}.hover-zoom-sm:hover,.hover-zoom-img-sm:hover img,.posts-mini:hover img,.posts-item:hover .item-thumbnail img,.posts-item.mult-thumb .thumb-items>span>img:hover{transform:scale(1.02)}.main-min-height{min-height:calc(100vh - 280px)}.main-shadow{box-shadow:0 0 10px var(--main-shadow)}.key-color{color:var(--key-color)}.focus-color{--this-color: var(--focus-color);color:var(--focus-color)}.muted-color{color:var(--muted-color)}.muted-2-color{color:var(--muted-2-color)}.muted-3-color{color:var(--muted-3-color)}.main-bg{background:var(--main-bg-color)}.placeholder{background:linear-gradient(118deg,#7979791a 25%,#79797933 37%,#7979791a 63%);opacity:1;border-radius:4px;-webkit-animation:placeholder 1.5s ease infinite!important;animation:placeholder 1.5s ease infinite!important;background-size:400% 100%!important}.placeholder.img{height:119px}.placeholder.t1{height:24px;width:80%}.placeholder.k1{height:20px;width:60%}.placeholder.k2{height:20px}.placeholder.k3{height:15px}.placeholder.s1{width:70px;height:15px;display:inline-block}@-webkit-keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@keyframes placeholder{0%{background-position:100% 50%}to{background-position:0 50%}}@-webkit-keyframes fade{0%{opacity:0}to{opacity:1}}@keyframes fade{0%{opacity:0}to{opacity:1}}.ani.shake{-webkit-animation:shake .3s ease-in!important;animation:shake .3s ease-in!important}@-webkit-keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}@keyframes shake{20%{transform:translate(2px)}40%{transform:translate(-4px)}60%{transform:translate(5px)}80%{transform:translate(3px)}to{transform:translate(0)}}.obs-animate{opacity:0;transition:opacity .6sLine truncated
+22 -10
View File
@@ -304,12 +304,24 @@
body.uc-modal-open { body.uc-modal-open {
overflow: hidden; overflow: hidden;
} }
.btn-block { /* Scoped to the user-center content shell. These are the credits panel's own
display: block; * button skins, and every element they target renders inside .uc-content (the
width: 100%; * panels and their modals are all emitted through templates/parts/user-center/
} * panel.php, which templates/pages/page-profile.php wraps in .uc-content).
.but.jb-blue, *
.but.b-blue-2 { * Left unscoped they are (0,2,0) rules that load after public.css, so they
* out-rank the legacy .but.radius pill and flatten every .but.jb-blue on the
* page down to 4px. The top-nav publish chip is .but.jb-blue.radius, so on the
* user-center route it lost its 50px pill and no longer matched the homepage.
* Site-wide .but.jb-blue callers outside the panel (login, submit, badge and
* cashier buttons) also picked up the flat #3b82f6 skin instead of the legacy
* gradient.
*
* The .btn-block restatement that used to sit here was byte-identical to the
* one in core/buttons.css, which loads earlier on every page, so it could never
* win and is dropped rather than scoped. */
.uc-content .but.jb-blue,
.uc-content .but.b-blue-2 {
background: #3b82f6; background: #3b82f6;
color: #fff; color: #fff;
border: 0; border: 0;
@@ -317,18 +329,18 @@ body.uc-modal-open {
padding: 0.45em 1em; padding: 0.45em 1em;
cursor: pointer; cursor: pointer;
} }
.but.c-blue-2.hollow, .uc-content .but.c-blue-2.hollow,
.but.c-green.hollow { .uc-content .but.c-green.hollow {
background: transparent; background: transparent;
border: 1px solid currentColor; border: 1px solid currentColor;
border-radius: 4px; border-radius: 4px;
padding: 0.4em 0.9em; padding: 0.4em 0.9em;
cursor: pointer; cursor: pointer;
} }
.but.c-blue-2 { .uc-content .but.c-blue-2 {
color: #3b82f6; color: #3b82f6;
} }
.but.c-green { .uc-content .but.c-green {
color: #16a34a; color: #16a34a;
} }
+2 -1
View File
@@ -265,7 +265,8 @@
.aksrt-account-list--social .binding-item { border: 0; } .aksrt-account-list--social .binding-item { border: 0; }
.aksrt-account-list--social .aksrt-account-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto auto; text-decoration: none; } .aksrt-account-list--social .aksrt-account-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto auto; text-decoration: none; }
.aksrt-account-list--social .binding-item__icon { display: grid; place-items: center; width: 24px; height: 24px; color: var(--color-ink, #333); } .aksrt-account-list--social .binding-item__icon { display: grid; place-items: center; width: 24px; height: 24px; color: var(--color-ink, #333); }
.aksrt-account-list--social .binding-item__icon i { font-size: 22px; line-height: 1; } .aksrt-account-list--social .binding-item__icon i,
.aksrt-account-list--social .binding-item__icon svg { font-size: 22px; line-height: 1; }
.aksrt-account-list--social .binding-status { display: inline-flex; align-items: center; gap: 4px; color: var(--color-success, #16a34a); font-size: 11px; white-space: nowrap; } .aksrt-account-list--social .binding-status { display: inline-flex; align-items: center; gap: 4px; color: var(--color-success, #16a34a); font-size: 11px; white-space: nowrap; }
.aksrt-account-list--social .binding-status--idle { color: var(--muted-2-color, var(--color-ink-4, #999)); } .aksrt-account-list--social .binding-status--idle { color: var(--muted-2-color, var(--color-ink-4, #999)); }
.aksrt-account-list--social .binding-unbind-btn { color: var(--this-color, #e4a11b); } .aksrt-account-list--social .binding-unbind-btn { color: var(--this-color, #e4a11b); }
+30 -107
View File
@@ -37,102 +37,33 @@ if (!function_exists('aksrt_get_new_add_actions')) :
} }
endif; endif;
if (!function_exists('aksrt_normalize_header_cta_label')) :
/**
* Normalize CTA label HTML: map FA5 classes and keep a single leading icon.
*/
function aksrt_normalize_header_cta_label(string $label, string $fallback_icon = 'fa fas fa-pen'): string {
$label = trim($label);
if ($label === '') {
return '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i> ' . esc_html__('发布', 'aksrt');
}
if (stripos($label, '<i') === false) {
// No space between icon and label — spacing via CSS margin (legacy .but > .fa).
$label = '<i class="' . esc_attr($fallback_icon) . '" aria-hidden="true"></i><span class="navbar-cta-text">' . esc_html(wp_strip_all_tags($label)) . '</span>';
} else {
$label = preg_replace(
'/(<\/i>)\s*([^<].*)$/u',
'$1<span class="navbar-cta-text">$2</span>',
$label
) ?? $label;
}
return $label;
}
endif;
if (!function_exists('aksrt_render_new_add_btns')) : if (!function_exists('aksrt_render_new_add_btns')) :
/** /**
* Multi-type publish button (legacy newadd-btns). * Publish chip (legacy newadd-btns, single action).
*
* Mirrors the single-button branch of the reference zib_get_new_add_btns():
* the paint classes ride on the anchor itself and the label is emitted
* verbatim. Only the post action exists, so there is no dropdown branch.
* *
* @param string[] $types * @param string[] $types
*/ */
function aksrt_render_new_add_btns(array $types, string $class = '', string $label = ''): string { function aksrt_render_new_add_btns(array $types, string $class = '', string $label = ''): string {
if (class_exists('\AKSRT\Settings\Settings') && \AKSRT\Settings\Settings::close_sign()) {
return '';
}
$actions = aksrt_get_new_add_actions($types); $actions = aksrt_get_new_add_actions($types);
if (!$actions) { if (!$actions) {
// Last resort: keep a write-post entry so the top-nav chip is never blank. return '';
$fallback = function_exists('aksrt_get_new_post_url') ? (string) aksrt_get_new_post_url() : '';
if ($fallback === '') {
$fallback = admin_url('post-new.php');
}
$actions = [[
'type' => 'post',
'url' => $fallback,
'label' => __('写文章', 'aksrt'),
'icon' => 'fa fas fa-file-alt',
]];
} }
$a = $actions[0];
if ($label === '') { if ($label === '') {
$label = __('发布', 'aksrt'); $label = __('发布', 'aksrt');
} }
$label_html = aksrt_normalize_header_cta_label((string) $label, 'fa fas fa-pen');
// Split paint classes vs wrapper tokens. return '<a rel="nofollow" class="newadd-btns ' . esc_attr($class) . ' btn-newadd" href="' . esc_url($a['url']) . '">'
$class_tokens = preg_split('/\s+/', trim($class), -1, PREG_SPLIT_NO_EMPTY) ?: []; . $label . '</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Saved through sanitize_type 'html' (wp_kses_post).
$cta_tokens = [];
$wrap_tokens = ['newadd-btns'];
foreach ($class_tokens as $token) {
if ($token === 'newadd-btns') {
continue;
}
if ($token === 'btn-primary' || $token === 'but' || $token === 'navbar-cta' || $token === 'radius' || $token === 'main-radius' || $token === 'mini-radius' || str_starts_with($token, 'jb-') || str_starts_with($token, 'b-')) {
$cta_tokens[] = $token;
continue;
}
$wrap_tokens[] = $token;
}
if (!$cta_tokens) {
$cta_tokens[] = 'jb-blue';
}
if (!in_array('but', $cta_tokens, true)) {
$cta_tokens[] = 'but';
}
$cta_class = trim(implode(' ', array_unique($cta_tokens)));
if (count($actions) === 1) {
$a = $actions[0];
return '<a rel="nofollow" class="' . esc_attr(trim(implode(' ', $wrap_tokens) . ' ' . $cta_class . ' navbar-cta')) . '" href="' . esc_url($a['url']) . '">'
. $label_html
. '</a>';
}
// legacy: span.newadd-btns.hover-show > toggle chip + .hover-show-con.drop-newadd
// Paint classes (jb-*, but, radius) MUST live on the toggle — wrapper is layout-only
// (background forced transparent). Bare navbar-but has no .navbar-but-cluster shell.
// data-aksrt-newadd: JS may portal menu to body so chip width cannot track menu.
$html = '<span class="' . esc_attr(trim(implode(' ', $wrap_tokens) . ' hover-show')) . '" data-aksrt-newadd="1">';
$html .= '<span class="newadd-btns__toggle navbar-cta ' . esc_attr($cta_class) . '">' . $label_html
. '<i class="fa fas fa-caret-down" aria-hidden="true"></i></span>';
$html .= '<div class="hover-show-con dropdown-menu drop-newadd" role="menu">';
foreach ($actions as $a) {
$html .= '<a rel="nofollow" class="btn-newadd newadd-btns__item" role="menuitem" href="' . esc_url($a['url']) . '">';
$html .= '<i class="' . esc_attr($a['icon']) . '" aria-hidden="true"></i> '
. '<text>' . esc_html($a['label']) . '</text>';
$html .= '</a>';
}
$html .= '</div></span>';
return $html;
} }
endif; endif;
@@ -142,26 +73,19 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
*/ */
function aksrt_render_header_menu_buttons(): string { function aksrt_render_header_menu_buttons(): string {
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::read_domain('page_display', 'header-nav') : []; $settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::read_domain('page_display', 'header-nav') : [];
$cfg = [ $enabled = !isset($settings['nav_newposts']) || !empty($settings['nav_newposts']);
'enabled' => !isset($settings['nav_newposts']) || !empty($settings['nav_newposts']), $text = trim((string) ($settings['nav_newposts_text'] ?? ''));
'text' => trim((string) ($settings['nav_newposts_text'] ?? __('发布', 'aksrt'))), $pill = !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']);
'radius_pill' => !isset($settings['nav_newposts_radius']) || !empty($settings['nav_newposts_radius']), $skin = sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue'));
'class' => sanitize_html_class((string) (($settings['nav_newposts_class'] ?? '') ?: 'jb-blue')),
];
$html = ''; $html = '';
if (!empty($cfg['enabled'])) { if ($enabled) {
// legacy default: but nowave jb-blue (+ radius). // legacy default: but nowave jb-blue (+ radius).
$class = 'but nowave navbar-newposts'; $class = 'but nowave ' . $skin;
$skin = trim((string) ($cfg['class'] ?? ''));
if ($skin === '') {
$skin = 'jb-blue';
}
$class .= ' ' . $skin;
$pill = !empty($cfg['radius_pill']);
if ($pill && !str_contains($class, 'radius')) { if ($pill && !str_contains($class, 'radius')) {
$class .= ' radius'; $class .= ' radius';
} }
$label = (string) ($cfg['text'] ?? __('发布', 'aksrt')); $label = $text !== '' ? $text : __('发布', 'aksrt');
$btn = aksrt_render_new_add_btns(['post'], $class, $label); $btn = aksrt_render_new_add_btns(['post'], $class, $label);
if ($btn !== '') { if ($btn !== '') {
$html .= $btn; $html .= $btn;
@@ -173,7 +97,8 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
$show_vip = false; $show_vip = false;
if (class_exists('\AKSRT\Settings\Settings')) { if (class_exists('\AKSRT\Settings\Settings')) {
$vs = \AKSRT\Settings\Settings::get_payment_vip_settings(); $vs = \AKSRT\Settings\Settings::get_payment_vip_settings();
$show_vip = !empty($vs['enabled']) && !empty($vs['nav_pay_vip']); $show_vip = !empty($vs['enabled']) && !empty($vs['nav_pay_vip'])
&& !\AKSRT\Settings\Settings::close_sign();
} }
if ($show_vip) { if ($show_vip) {
$is_vip = false; $is_vip = false;
@@ -188,17 +113,15 @@ if (!function_exists('aksrt_render_header_menu_buttons')) :
if (method_exists($svc, 'render_vip_card')) { if (method_exists($svc, 'render_vip_card')) {
$cards = $svc->render_vip_card(1, $user_id) . $svc->render_vip_card(2, $user_id); $cards = $svc->render_vip_card(1, $user_id) . $svc->render_vip_card(2, $user_id);
if ($cards !== '') { if ($cards !== '') {
$hover = '<div class="hover-show-con sub-menu sub-vip-card">' . $cards . '</div>'; $hover = '<div class="sub-menu hover-show-con sub-vip-card">' . $cards . '</div>';
} }
} }
} }
// legacy: span.hover-show wraps button + .hover-show-con.sub-vip-card // legacy: span.hover-show wraps button + .sub-menu.sub-vip-card
$html .= '<span class="hover-show inline-block navbar-vip-hover">'; $html .= '<span class="hover-show inline-block">';
$html .= '<a class="but jb-red radius payvip-icon ml10 navbar-cta ' . esc_attr($cls) . '" href="javascript:;"' $html .= '<a class="' . esc_attr($cls) . ' but jb-red radius payvip-icon ml10" href="javascript:;">'
. ($user_id > 0 ? ' vip-level="1"' : '') . '>'; . '<svg class="em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
$html .= '<svg class="icon em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>' . esc_html__('开通会员', 'aksrt') . '</a>' . $hover . '</span>';
. '<span class="navbar-cta-text">' . esc_html__('开通会员', 'aksrt') . '</span>';
$html .= '</a>' . $hover . '</span>';
} }
} }
+7 -7
View File
@@ -56,11 +56,11 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
// 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。 // 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。
if ($signed) { if ($signed) {
if ($class !== null) { if ($class !== null) {
// 用户卡片签到书签:保留书签类与 is-done 视觉,仅将行为切换为打开详情弹窗。 // 用户卡片签到书签:与参考主题一致,已签到态仅切换为详情弹窗触发链接,书签类名与文本不变。
return aksrt_get_user_checkin_details_link( return aksrt_get_user_checkin_details_link(
$class . ' is-done', $class,
'<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>' '<i class="fa far fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
. '<span>' . esc_html__('已签到', 'aksrt') . '</span>' . esc_html__('已签到', 'aksrt')
); );
} }
return aksrt_get_user_checkin_details_link( return aksrt_get_user_checkin_details_link(
@@ -230,11 +230,11 @@ if (!function_exists('aksrt_header_payvip_bar')) :
return ''; return '';
} }
// 几何对齐参考主题 CTA(通栏、radius4)——不是顶导 navbar-cta chips。 // 1:1 对齐参考主题 zib_get_header_payvip_icon():通栏、radius4、vip_1 SVG 图标、裸文案。
return '<div class="mt10 em09" style="padding:2px;">' return '<div class="mt10 em09" style="padding:2px;">'
. '<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;" vip-level="1">' . '<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'
. '<i class="fa fas fa-gem em12 mr6" aria-hidden="true"></i>' . '<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"></use></svg>'
. '<span>' . esc_html__('开通会员 尊享会员权益', 'aksrt') . '</span>' . esc_html__('开通会员 尊享会员权益', 'aksrt')
. '</a></div>'; . '</a></div>';
} }
endif; endif;
+24 -9
View File
@@ -42,7 +42,7 @@ endif;
if (!function_exists('aksrt_brand_icon_map')) : if (!function_exists('aksrt_brand_icon_map')) :
/** /**
* Short key → FA5 class string (allowlisted). * Short key → FA5 class string or built-in SVG sprite key (allowlisted).
* *
* @return array<string, string> * @return array<string, string>
*/ */
@@ -52,10 +52,12 @@ function aksrt_brand_icon_map(): array {
return $map; return $map;
} }
// FA5 Free has no bilibili/gitee brands — use solid stand-ins. // FA5 Free ships no Gitee brand glyph and no multi-colour Google mark, so
// those two resolve to the theme's built-in SVG sprite (zibsvg-*) instead of
// a solid stand-in; every other key stays on FA5.
$map = [ $map = [
'github' => 'fa fab fa-github', 'github' => 'fa fab fa-github',
'google' => 'fa fab fa-google', 'google' => 'zibsvg-google-color',
'x' => 'fa fab fa-twitter', 'x' => 'fa fab fa-twitter',
'twitter' => 'fa fab fa-twitter', 'twitter' => 'fa fab fa-twitter',
'email' => 'fa fas fa-envelope', 'email' => 'fa fas fa-envelope',
@@ -73,7 +75,7 @@ function aksrt_brand_icon_map(): array {
'instagram' => 'fa fab fa-instagram', 'instagram' => 'fa fab fa-instagram',
'web' => 'fa fas fa-globe', 'web' => 'fa fas fa-globe',
'globe' => 'fa fas fa-globe', 'globe' => 'fa fas fa-globe',
'gitee' => 'fa fas fa-code-branch', 'gitee' => 'zibsvg-gitee',
'baidu' => 'fa fas fa-paw', 'baidu' => 'fa fas fa-paw',
'alipay' => 'fa fab fa-alipay', 'alipay' => 'fa fab fa-alipay',
]; ];
@@ -127,10 +129,10 @@ endif;
if (!function_exists('aksrt_brand_icon_class')) : if (!function_exists('aksrt_brand_icon_class')) :
/** /**
* Resolve a short key or FA class string to an allowlisted FA5 class. * Resolve a short key, FA class string or sprite key to its icon value.
* *
* @param string $name Short key or full FA class string. * @param string $name Short key, full FA class string or zibsvg-/customsvg- value.
* @return string e.g. 'fa fab fa-github', or '' if unknown. * @return string e.g. 'fa fab fa-github' or 'zibsvg-gitee', or '' if unknown.
*/ */
function aksrt_brand_icon_class(string $name): string { function aksrt_brand_icon_class(string $name): string {
$raw = trim($name); $raw = trim($name);
@@ -138,6 +140,12 @@ function aksrt_brand_icon_class(string $name): string {
return ''; return '';
} }
// Built-in SVG sprite and inline SVG values pass straight through; they are
// not FA class strings and must survive to the markup renderer untouched.
if (preg_match('/^(?:zibsvg|customsvg)-/i', $raw)) {
return $raw;
}
// Pass through already-valid full FA class strings (footer contact_more). // Pass through already-valid full FA class strings (footer contact_more).
if (preg_match('/^fa\s+(?:fas|far|fab)\s+fa-[\w-]+(?:\s+fa-[\w-]+)*$/i', $raw)) { if (preg_match('/^fa\s+(?:fas|far|fab)\s+fa-[\w-]+(?:\s+fa-[\w-]+)*$/i', $raw)) {
// Prefer mapped short key when strip matches our catalog (normalizes weixin etc.). // Prefer mapped short key when strip matches our catalog (normalizes weixin etc.).
@@ -164,14 +172,21 @@ endif;
if (!function_exists('aksrt_brand_icon')) : if (!function_exists('aksrt_brand_icon')) :
/** /**
* Escaped FA <i> markup for a brand/contact icon. * Escaped markup for a brand/contact icon (FA5 <i> or built-in SVG sprite).
* *
* @param string $name Short key or FA class string. * @param string $name Short key, FA class string or zibsvg-/customsvg- value.
* @param array|string $extra_classes Optional extra classes. * @param array|string $extra_classes Optional extra classes.
* @return string Safe HTML or empty string when unknown. * @return string Safe HTML or empty string when unknown.
*/ */
function aksrt_brand_icon(string $name, $extra_classes = []): string { function aksrt_brand_icon(string $name, $extra_classes = []): string {
$class = aksrt_brand_icon_class($name); $class = aksrt_brand_icon_class($name);
// Sprite and inline-SVG values carry their own element, so the shared icon
// renderer owns the markup (it emits <svg class="icon"><use …>).
if (strpos($class, 'zibsvg-') === 0 || strpos($class, 'customsvg-') === 0) {
return aksrt_icon_markup($class, $extra_classes);
}
if ($class === '') { if ($class === '') {
return ''; return '';
} }
+1 -4
View File
@@ -428,10 +428,7 @@ if (!function_exists('aksrt_submit_links_modal')) :
$body .= '<p><a href="' . esc_url($sign_url) . '" class="signin-loader but c-blue padding-lg">' $body .= '<p><a href="' . esc_url($sign_url) . '" class="signin-loader but c-blue padding-lg">'
. '<i class="fa fas fa-fw fa-sign-in-alt mr10" aria-hidden="true"></i>' . esc_html__('登录', 'aksrt') . '</a></p>'; . '<i class="fa fas fa-fw fa-sign-in-alt mr10" aria-hidden="true"></i>' . esc_html__('登录', 'aksrt') . '</a></p>';
if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) { if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) {
$social = (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('使用以下方式快速登录', 'aksrt')); $body .= (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('使用以下方式快速登录', 'aksrt'));
if ($social !== '') {
$body .= '<div class="social_loginbar">' . $social . '</div>';
}
} }
$body .= '</div>'; $body .= '</div>';
} else { } else {
+20 -8
View File
@@ -286,11 +286,7 @@ if (!function_exists('aksrt_header_user_box_html')) :
$con = '<div class="text-center">' . $href . '</div>'; $con = '<div class="text-center">' . $href . '</div>';
if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) { if (class_exists('\AKSRT\Auth\OAuth') && method_exists(\AKSRT\Auth\OAuth::get_instance(), 'render_social_buttons')) {
$oauth_html = (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('快速登录', 'aksrt')); $con .= (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('快速登录', 'aksrt'));
if ($oauth_html !== '') {
$con .= '<p class="social-separator separator muted-3-color em09 mt10">' . esc_html__('快速登录', 'aksrt') . '</p>';
$con .= '<div class="social_loginbar">' . $oauth_html . '</div>';
}
} }
} }
@@ -303,6 +299,12 @@ if (!function_exists('aksrt_menu_button')) :
/** /**
* Right cluster: fold, publish/VIP, theme, user, search (legacy). * Right cluster: fold, publish/VIP, theme, user, search (legacy).
* *
* The reference splits the cluster into three floated .navbar-form
* groups (publish/VIP, theme + language, account row). Each group
* keeps its own .navbar-form padding, which is what produces the
* reference inter-cluster spacing, so the groups are emitted
* separately instead of being merged into one flex container.
*
* @param array<string,mixed> $ctx * @param array<string,mixed> $ctx
*/ */
function aksrt_menu_button(array $ctx): void { function aksrt_menu_button(array $ctx): void {
@@ -371,14 +373,24 @@ if (!function_exists('aksrt_menu_button')) :
$account = '<ul class="list-inline splitters relative aksrt-navbar-account' $account = '<ul class="list-inline splitters relative aksrt-navbar-account'
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">' . ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
. $notification_but . $sign_but . $search_but . '</ul>'; . $notification_but . $sign_but . $search_but . '</ul>';
$button = '<div class="aksrt-navbar-actions navbar-form navbar-right navbar-but">'
. $actions . $utility_buttons . $account . '</div>'; // Reference cluster split: publish/VIP, theme + language, account row.
$button = '';
if ($actions !== '') {
$button = '<div class="navbar-form navbar-right navbar-but">' . $actions . '</div>';
}
if ($utility_buttons !== '') {
$button .= '<div class="navbar-form navbar-right flex ac">' . $utility_buttons . '</div>';
}
$right_but = '<div class="navbar-form navbar-right'
. ($user_id <= 0 && '2' === $layout ? ' navbar-text' : '') . '">'
. $account . '</div>';
$fold = '<div class="navbar-form navbar-right hide show-nav-but" style="margin-right:-10px;">' $fold = '<div class="navbar-form navbar-right hide show-nav-but" style="margin-right:-10px;">'
. '<a data-toggle-class data-target=".nav.navbar-nav" href="javascript:;" class="aksrt-nav-icon" aria-label="' . '<a data-toggle-class data-target=".nav.navbar-nav" href="javascript:;" class="aksrt-nav-icon" aria-label="'
. esc_attr__('展开菜单', 'aksrt') . '"><svg class="icon" viewBox="0 0 1024 1024" aria-hidden="true"><use xlink:href="#icon-menu_2"></use></svg></a></div>'; . esc_attr__('展开菜单', 'aksrt') . '"><svg class="icon" viewBox="0 0 1024 1024" aria-hidden="true"><use xlink:href="#icon-menu_2"></use></svg></a></div>';
$html = $fold . $button; $html = $fold . $button . $right_but;
echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} }
+1 -1
View File
@@ -53,7 +53,7 @@ class SchemaPageDisplay {
self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']), self::field('nav_newposts', 'Add发布按钮', 'switcher', true, ['description' => '顶部导航显示文章投稿按钮。']),
self::field('nav_newposts_radius', '按钮圆角', 'switcher', true, ['dependsOn' => self::enabled('nav_newposts')]), self::field('nav_newposts_radius', '按钮圆角', 'switcher', true, ['dependsOn' => self::enabled('nav_newposts')]),
self::field('nav_newposts_class', '按钮颜色', 'palette', 'jb-blue', ['dependsOn' => self::enabled('nav_newposts'), 'options' => self::aksrt_palette()]), self::field('nav_newposts_class', '按钮颜色', 'palette', 'jb-blue', ['dependsOn' => self::enabled('nav_newposts'), 'options' => self::aksrt_palette()]),
self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fas fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]), self::field('nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fa-pencil-alt"></i>发布', ['dependsOn' => self::enabled('nav_newposts'), 'sanitize_type' => 'html', 'rows' => 1]),
]); ]);
} }
+41 -19
View File
@@ -219,35 +219,57 @@ final class OAuth {
'name' => $provider, 'name' => $provider,
'label' => OAuthProviderCatalog::label($provider), 'label' => OAuthProviderCatalog::label($provider),
'icon' => function_exists('aksrt_brand_icon') ? aksrt_brand_icon(OAuthProviderCatalog::icon_key($provider)) : '', 'icon' => function_exists('aksrt_brand_icon') ? aksrt_brand_icon(OAuthProviderCatalog::icon_key($provider)) : '',
'url' => esc_url($url), // Raw, matching get_frontend_bindings_data(): every surface escapes
// once on output. esc_url() is idempotent on its own output, so
// escaping here too was redundant rather than broken.
'url' => $url,
]; ];
} }
return $result; return $result;
} }
/**
* Reference-parity social login block (legacy zib_social_login()).
*
* Emits the muted separator plus a .social_loginbar of .social-login-item
* buttons, one per enabled provider. Returns '' when nothing is enabled.
*
* @param string $title Separator label; an empty title drops the separator.
*/
public function render_social_buttons(string $title = ''): string { public function render_social_buttons(string $title = ''): string {
$items = $this->get_auth_providers_data(); $items = $this->get_auth_providers_data();
if ($items === []) { if ($items === []) {
return ''; return '';
} }
ob_start();
?> $large = (bool) \AKSRT\Settings\Settings::get_value('oauth_button_lg', false);
<div class="social-login-divider"> $html = $title !== ''
<span><?php echo esc_html($title) ?: esc_html__('or', 'aksrt'); ?></span> ? '<p class="social-separator separator muted-3-color em09 mt10">' . esc_html($title) . '</p>'
</div> : '';
<div class="oauth-row"> $html .= '<div class="social_loginbar">';
<?php foreach ($items as $item) : ?> foreach ($items as $item) {
<a href="<?php echo esc_url((string) $item['url']); ?>" $name = sanitize_html_class((string) ($item['name'] ?? ''));
class="oauth-btn oauth-btn--<?php echo esc_attr((string) $item['name']); ?>" $type = str_starts_with($name, 'clogin_') ? substr($name, 7) : $name;
data-provider="<?php echo esc_attr((string) $item['name']); ?>" $label = trim((string) ($item['label'] ?? ''));
aria-label="<?php echo esc_attr((string) $item['label']); ?>" $url = (string) ($item['url'] ?? '');
title="<?php echo esc_attr((string) $item['label']); ?>"> if ($name === '' || $type === '' || $label === '' || $url === '') {
<?php echo $item['icon']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> continue;
</a> }
<?php endforeach; ?> $class = 'social-login-item ' . $type . ($large ? ' button-lg' : ' toggle-radius');
</div> if ($name === 'weixingzh') {
<?php // Reference parity: the WeChat entry opens the QR panel instead of
return (string) ob_get_clean(); // navigating, so auth.js routes it to the modal's oauth tab.
$class .= ' qrcode-signin';
}
$html .= '<a rel="nofollow" href="' . esc_url($url) . '" class="' . esc_attr($class) . '"'
. ' data-provider="' . esc_attr($name) . '"'
. ' aria-label="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '"'
. ' title="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '">'
. $item['icon'] // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
. ($large ? '<span>' . esc_html(sprintf(__('%s登录', 'aksrt'), $label)) . '</span>' : '')
. '</a>';
}
return $html . '</div>';
} }
public function get_frontend_bindings_data(int $user_id): array { public function get_frontend_bindings_data(int $user_id): array {
+1 -1
View File
@@ -125,7 +125,7 @@ final class UserRenderer {
if ($name === '' || $style_name === '' || $label === '' || $url === '') { if ($name === '' || $style_name === '' || $label === '' || $url === '') {
continue; continue;
} }
$html .= '<a rel="nofollow" href="' . esc_url($url) . '" class="social-login-item ' . esc_attr($style_name) . '" data-provider="' . esc_attr($name) . '" aria-label="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '" title="' . esc_attr($label) . '">' . (string) ($item['icon'] ?? '') . '</a>'; $html .= '<a rel="nofollow" href="' . esc_url($url) . '" class="social-login-item ' . esc_attr($style_name) . ' toggle-radius" data-provider="' . esc_attr($name) . '" aria-label="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '" title="' . esc_attr(sprintf(__('%s登录', 'aksrt'), $label)) . '">' . (string) ($item['icon'] ?? '') . '</a>';
} }
return $html . '</div>'; return $html . '</div>';
} }
-1
View File
@@ -147,6 +147,5 @@ $oauth_bind_pass = $oauth_intent === 'bind'
<?php foreach ($oauth_providers as $provider) : ?><?php if (($provider['name'] ?? '') === 'weixingzh') continue; ?><?php $provider_label = (string) ($provider['label'] ?? $provider['name'] ?? ''); ?><a class="social-login-item <?php echo esc_attr((string) ($provider['name'] ?? '')); ?>" href="<?php echo esc_url((string) ($provider['url'] ?? '#')); ?>" data-provider="<?php echo esc_attr((string) ($provider['name'] ?? '')); ?>" aria-label="<?php echo esc_attr(sprintf(__('%s登录', 'aksrt'), $provider_label)); ?>" title="<?php echo esc_attr($provider_label); ?>"><?php echo $provider['icon'] ?? ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php if ($oauth_large) : ?><span><?php printf(esc_html__('%s登录', 'aksrt'), esc_html($provider_label)); ?></span><?php endif; ?></a><?php endforeach; ?> <?php foreach ($oauth_providers as $provider) : ?><?php if (($provider['name'] ?? '') === 'weixingzh') continue; ?><?php $provider_label = (string) ($provider['label'] ?? $provider['name'] ?? ''); ?><a class="social-login-item <?php echo esc_attr((string) ($provider['name'] ?? '')); ?>" href="<?php echo esc_url((string) ($provider['url'] ?? '#')); ?>" data-provider="<?php echo esc_attr((string) ($provider['name'] ?? '')); ?>" aria-label="<?php echo esc_attr(sprintf(__('%s登录', 'aksrt'), $provider_label)); ?>" title="<?php echo esc_attr($provider_label); ?>"><?php echo $provider['icon'] ?? ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php if ($oauth_large) : ?><span><?php printf(esc_html__('%s登录', 'aksrt'), esc_html($provider_label)); ?></span><?php endif; ?></a><?php endforeach; ?>
<?php if ($wechat_qr) : ?><button type="button" class="social-login-item weixingzh" data-auth-tab="oauth" aria-label="<?php esc_attr_e('微信扫码登录', 'aksrt'); ?>" title="<?php esc_attr_e('微信扫码登录', 'aksrt'); ?>"><i class="fa fab fa-weixin" aria-hidden="true"></i><?php if ($oauth_large) : ?><span><?php esc_html_e('微信登录', 'aksrt'); ?></span><?php endif; ?></button><?php endif; ?> <?php if ($wechat_qr) : ?><button type="button" class="social-login-item weixingzh" data-auth-tab="oauth" aria-label="<?php esc_attr_e('微信扫码登录', 'aksrt'); ?>" title="<?php esc_attr_e('微信扫码登录', 'aksrt'); ?>"><i class="fa fab fa-weixin" aria-hidden="true"></i><?php if ($oauth_large) : ?><span><?php esc_html_e('微信登录', 'aksrt'); ?></span><?php endif; ?></button><?php endif; ?>
</div></div> </div></div>
<?php if (!empty($agreement['enabled'])) : ?><div class="sign-agreement sign-agreement-implicit"><?php esc_html_e('使用社交账号登录即表示', 'aksrt'); ?><?php echo $agreement['label_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div><?php endif; ?>
<?php endif; ?> <?php endif; ?>
</div> </div>
@@ -180,6 +180,52 @@ test('guest account member VIP and publish contracts share reference header DOM'
assert.match(userboxCss, /\.aksrt-header-userbox\s*>\s*\.mt6\.em09\s*>\s*\.but\.block\s*\{[^}]*display:\s*block;[^}]*box-sizing:\s*border-box;[^}]*width:\s*100%;/s); assert.match(userboxCss, /\.aksrt-header-userbox\s*>\s*\.mt6\.em09\s*>\s*\.but\.block\s*\{[^}]*display:\s*block;[^}]*box-sizing:\s*border-box;[^}]*width:\s*100%;/s);
}); });
test('the top-nav midline chip rule leaves the popover social buttons alone', () => {
const userboxCss = read('assets/css/frontend/header-userbox.css');
const publicCss = read('assets/css/build/public.css');
// The account-row chips, the theme toggle and the language toggle all want a
// 29px midline circle. The popover's quick-login buttons are .toggle-radius
// too and also live inside .navbar-form, but they must keep the
// .social_loginbar geometry from main.css. flex-basis outranks width in a
// flex container and the bare rule is (0,3,0) against (0,2,0), so an
// unscoped selector collapses the buttons to 29px and zeroes their margins.
assert.match(userboxCss, /\.navbar-top \.navbar-form \.toggle-radius:not\(\.social-login-item\):not\(\.xh-social-item\)\s*\{[^}]*flex:\s*0 0 29px;[^}]*margin:\s*0;/s);
assert.doesNotMatch(userboxCss, /\.navbar-top \.navbar-form \.toggle-radius\s*\{/);
// header-userbox.css is also folded into the public bundle, so the exemption
// has to survive the build.
assert.match(publicCss, /\.navbar-top \.navbar-form \.toggle-radius:not\(\.social-login-item\):not\(\.xh-social-item\)/);
});
test('route and feature modules do not restyle the legacy top-nav chips', () => {
// Route and feature modules may not restyle the legacy .but colour modifiers
// without an enclosing scope. The top-nav chips ride the legacy geometry
// verbatim (newadd-btns but <skin> radius and pay-vip but jb-red radius
// payvip-icon), so their pill comes from .but.radius at (0,2,0). A later,
// equally specific rule that restates border-radius on a bare .but.<colour>
// out-ranks it and flattens the pill: user-center/credits.css did exactly that
// with .but.jb-blue, so the user-center route rendered a 4px publish chip while
// every other route kept the 50px one. Such rules must be scoped to their own
// shell, as credits.css now is with .uc-content.
const moduleCss = [
...listFiles('assets/css/routes', new Set(['.css'])),
...listFiles('assets/css/frontend', new Set(['.css'])),
].filter((file) => file !== 'assets/css/frontend/header-userbox.css');
for (const file of moduleCss) {
const css = read(file);
for (const [, selector, declarations] of css.matchAll(/([^{}]+)\{([^{}]*)\}/g)) {
if (!/border-radius/.test(declarations)) continue;
for (const part of selector.split(',')) {
assert.doesNotMatch(
part.trim(),
/^\.but\./,
`${file} restates border-radius on an unscoped legacy .but modifier (${part.trim()}); scope it to the module's own shell so the top-nav chips keep the reference pill`
);
}
}
}
})
test('the account popover rows match the reference user box structure', () => { test('the account popover rows match the reference user box structure', () => {
const identity = read('inc/helpers/aksrt-identity.php'); const identity = read('inc/helpers/aksrt-identity.php');
const legacyMainCss = read('assets/css/main.css'); const legacyMainCss = read('assets/css/main.css');
@@ -217,7 +263,12 @@ test('the account popover rows match the reference user box structure', () => {
assert.match(header, /radius4 payvip-icon btn-block text-center/); assert.match(header, /radius4 payvip-icon btn-block text-center/);
assert.match(header, /vip-expdate-tag/); assert.match(header, /vip-expdate-tag/);
assert.match(header, /pay-vip but radius4 payvip-icon btn-block/); assert.match(header, /pay-vip but radius4 payvip-icon btn-block/);
assert.match(userboxCss, /\.aksrt-header-userbox \.pay-vip\.but\.payvip-icon\s*\{[\s\S]*?background:\s*linear-gradient\(300deg,[\s\S]*?color:\s*#ece0e0;[\s\S]*?--this-color:\s*#ece0e0;/s); // core/buttons.css restates .but after main.css, which would otherwise replace
// the legacy .payvip-icon surface with the .jb-* skin. The restoration must
// cover background, color, border and text-shadow, and must not be scoped to
// one call site or the top-nav chip keeps the wrong gradient.
assert.match(userboxCss, /\.payvip-icon\s*\{[\s\S]*?background:\s*linear-gradient\(300deg,[\s\S]*?color:\s*#ece0e0;[\s\S]*?--this-color:\s*#ece0e0;[\s\S]*?border:\s*none;[\s\S]*?text-shadow:\s*none;/s);
assert.doesNotMatch(userboxCss, /\.aksrt-header-userbox \.pay-vip\.but\.payvip-icon/)
assert.doesNotMatch(header, /vipbg-v2/); assert.doesNotMatch(header, /vipbg-v2/);
// Check-in entry is an anchor with the reference button classes. // Check-in entry is an anchor with the reference button classes.
@@ -225,8 +276,11 @@ test('the account popover rows match the reference user box structure', () => {
assert.match(header, /but block c-yellow is-done/); assert.match(header, /but block c-yellow is-done/);
assert.match(buttonsCss, /\.but\.block\s*\{\s*display:\s*block;\s*\}/); assert.match(buttonsCss, /\.but\.block\s*\{\s*display:\s*block;\s*\}/);
assert.match(buttonsCss, /\.btn-block\s*\{\s*display:\s*block;\s*width:\s*100%;\s*\}/); assert.match(buttonsCss, /\.btn-block\s*\{\s*display:\s*block;\s*width:\s*100%;\s*\}/);
assert.match(legacyMainCss, /\.navbar-top \.navbar-but > \.but\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.3em 1em !important;[^}]*margin:\s*0\.4em;/s); // The reference selector is a descendant, so the VIP chip inside its hover-show
assert.doesNotMatch(legacyMainCss, /\.navbar-top \.navbar-but \.but\s*\{/); // wrapper receives the same .but geometry (font-size, padding, margin) as the
// publish chip instead of falling back to the bare .but defaults.
assert.match(legacyMainCss, /\.navbar-top \.navbar-but \.but\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.3em 1em !important;[^}]*margin:\s*0\.4em;/s);
assert.doesNotMatch(legacyMainCss, /\.navbar-top \.navbar-but > \.but\s*\{/);
assert.doesNotMatch(header, /but block c-yellow[^"']*btn-block/); assert.doesNotMatch(header, /but block c-yellow[^"']*btn-block/);
assert.match(header, /<div class="mt6 em09" style="padding:2px;">' \. \$checkin \. '<\/div>/); assert.match(header, /<div class="mt6 em09" style="padding:2px;">' \. \$checkin \. '<\/div>/);
assert.match(header, /data-signin-done-label[\s\S]{0,120}?今日已签到/); assert.match(header, /data-signin-done-label[\s\S]{0,120}?今日已签到/);
@@ -239,3 +239,77 @@ test('OAuth frontend is accessible, redirect-aware, and removes retired provider
assert.doesNotMatch(signCss + mainCss, /social-login-item[^\n{]*(?:baidu|alipay)|social-login-item\.(?:baidu|alipay)/i); assert.doesNotMatch(signCss + mainCss, /social-login-item[^\n{]*(?:baidu|alipay)|social-login-item\.(?:baidu|alipay)/i);
assert.match(mainCss, /\.pay-box \.but\.baidu/); assert.match(mainCss, /\.pay-box \.but\.baidu/);
}); });
test('the social-login area drops the implicit agreement line', () => {
const shared = read('templates/parts/auth-content.php');
const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php');
// The reference shows no agreement copy under the social buttons; only the
// registration panel keeps its own, and it stays reachable.
assert.doesNotMatch(shared, /使用社交账号登录即表示/);
assert.match(shared, /注册即表示/);
assert.match(shared, /name="agree_terms"[\s\S]*?required/);
assert.doesNotMatch(userRenderer, /使用社交账号登录即表示|我已阅读并同意/);
});
test('brand icons resolve Gitee and Google to the reference SVG sprite', () => {
const icons = read('inc/helpers/aksrt-icons.php');
const sprites = read('assets/js/runtime/svg-icon.js');
// FA5 Free ships no Gitee brand glyph and no multi-colour Google mark, so both
// resolve to the theme's own symbol sheet exactly like the reference does.
assert.match(icons, /'gitee'\s*=>\s*'zibsvg-gitee'/);
assert.match(icons, /'google'\s*=>\s*'zibsvg-google-color'/);
assert.match(icons, /'github'\s*=>\s*'fa fab fa-github'/);
assert.match(icons, /'wechat'\s*=>\s*'fa fab fa-weixin'/);
assert.match(sprites, /aksrt_svgs\['gitee'\]/);
assert.match(sprites, /aksrt_svgs\['google-color'\]/);
// Sprite values must reach the shared icon renderer instead of being wrapped
// in a bogus <i class="zibsvg-…">.
assert.match(icons, /strpos\(\$class, 'zibsvg-'\) === 0[\s\S]*?aksrt_icon_markup\(\$class, \$extra_classes\)/);
});
test('the top-nav user popover reproduces the reference quick-login block', () => {
const oauth = read('src/Auth/OAuth.php');
const header = read('inc/helpers/header/aksrt-header-navbar.php');
const links = read('inc/helpers/aksrt-links.php');
const legacyCss = read('assets/css/frontend/pages/base-1.css');
assert.match(oauth, /social-separator separator muted-3-color em09 mt10/);
assert.match(oauth, /'<div class="social_loginbar">'/);
assert.match(oauth, /'social-login-item ' \. \$type/);
assert.match(oauth, /' button-lg' : ' toggle-radius'/);
assert.match(oauth, /rel="nofollow"/);
assert.match(oauth, /qrcode-signin/);
assert.match(oauth, /oauth_button_lg/);
// Callers append the block verbatim — no hand-rolled separator or wrapper.
assert.match(header, /render_social_buttons\(__\('快速登录', 'aksrt'\)\)/);
assert.doesNotMatch(header, /social-separator|social_loginbar/);
assert.match(links, /render_social_buttons\(__\('使用以下方式快速登录', 'aksrt'\)\)/);
assert.doesNotMatch(links, /social_loginbar/);
// The retired .social-login-divider / .oauth-row / .oauth-btn markup and its
// stylesheet are gone.
assert.doesNotMatch(oauth, /oauth-row|oauth-btn|social-login-divider/);
assert.doesNotMatch(legacyCss, /\.oauth-row|\.oauth-btn|\.social-login-divider/);
});
test('provider URLs stay raw in the data layer and are escaped once per surface', () => {
const oauth = read('src/Auth/OAuth.php');
const shared = read('templates/parts/auth-content.php');
const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php');
const comments = read('inc/helpers/aksrt-comments.php');
// get_auth_providers_data() hands out raw URLs, exactly like its sibling
// get_frontend_bindings_data() does. esc_url() is idempotent on its own
// output, so escaping here as well was redundant rather than broken.
assert.doesNotMatch(oauth, /'url'\s*=>\s*esc_url\(\$url\)/);
assert.match(oauth, /'url'\s*=>\s*\$url,/);
// Every consumer still escapes on output, so the rendered href is unchanged.
assert.match(shared, /esc_url\(\(string\) \(\$provider\['url'\] \?\? '#'\)\)/);
assert.match(userRenderer, /href="' \. esc_url\(\$url\)/);
assert.match(oauth, /'<a rel="nofollow" href="' \. esc_url\(\$url\)/);
assert.match(comments, /<a href="<\?php echo esc_url\(\$url\); \?\>" class="but hollow social-login-item ml10">/);
});
@@ -120,6 +120,10 @@ test('defaults, nested structures, dependencies, and excluded product families a
} }
assert.equal(headerMenuButtons.includes('nav_newposts_btns'), false) assert.equal(headerMenuButtons.includes('nav_newposts_btns'), false)
assert.match(headerMenuButtons, /aksrt_render_new_add_btns\(\['post'\], \$class, \$label\)/) assert.match(headerMenuButtons, /aksrt_render_new_add_btns\(\['post'\], \$class, \$label\)/)
// FA5 spelling of the reference '<i class="fa fa-fw fa-pencil"></i>发布'
// default: fa-pencil does not exist in the bundled FA 5.15.4 free set.
assert.match(schema, /'nav_newposts_text', '按钮文案', 'textarea', '<i class="fa fas fa-fw fa-pencil-alt"><\/i>发布'/)
assert.doesNotMatch(schema, /fa fas fa-fw fas /)
}) })
test('VIP navigation purchase switches are saved and consumed end to end', () => { test('VIP navigation purchase switches are saved and consumed end to end', () => {
@@ -136,8 +140,18 @@ test('VIP navigation purchase switches are saved and consumed end to end', () =>
}) })
test('header VIP hover cards match the legacy markup and style contract', () => { test('header VIP hover cards match the legacy markup and style contract', () => {
assert.match(headerMenuButtons, /<span class="hover-show inline-block navbar-vip-hover">/) assert.match(headerMenuButtons, /<span class="hover-show inline-block">/)
assert.match(headerMenuButtons, /<div class="hover-show-con sub-menu sub-vip-card">/) assert.match(headerMenuButtons, /<div class="sub-menu hover-show-con sub-vip-card">/)
assert.match(headerMenuButtons, /'<a class="' \. esc_attr\(\$cls\) \. ' but jb-red radius payvip-icon ml10" href="javascript:;">'/)
// The payvip surface must survive the .but compatibility layer for the top-nav
// chip too, not just the user-card CTA.
assert.match(headerCss, /\.payvip-icon\s*\{[\s\S]*?background:\s*linear-gradient\(300deg,[\s\S]*?--this-color:\s*#ece0e0;/s)
assert.match(headerMenuButtons, /<svg class="em12 mr3" aria-hidden="true"><use xlink:href="#icon-vip_1"><\/use><\/svg>'/)
assert.match(headerMenuButtons, /'<a rel="nofollow" class="newadd-btns ' \. esc_attr\(\$class\) \. ' btn-newadd"/)
assert.equal((headerMenuButtons.match(/Settings::close_sign\(\)/g) || []).length, 2)
for (const dead of ['navbar-cta', 'navbar-cta-text', 'navbar-newposts', 'navbar-vip-hover', 'newadd-btns__', 'vip-level', 'fa-gem']) {
assert.equal(headerMenuButtons.includes(dead), false, `AKSRT-only header CTA token remains: ${dead}`)
}
assert.match(vipCardRenderer, /aksrt_get_lazy_attr\('lazy_other', \$icon_url, 'img-icon'\)/) assert.match(vipCardRenderer, /aksrt_get_lazy_attr\('lazy_other', \$icon_url, 'img-icon'\)/)
assert.match(vipCardRenderer, /<div class=\"abs-center vip-baicon\"[^>]*>/) assert.match(vipCardRenderer, /<div class=\"abs-center vip-baicon\"[^>]*>/)
assert.match(vipCardRenderer, /<div class=\"vip-img abs-right\">/) assert.match(vipCardRenderer, /<div class=\"vip-img abs-right\">/)
@@ -154,8 +168,21 @@ test('header VIP hover cards match the legacy markup and style contract', () =>
assert.match(legacyMainCss, /\.sub-vip-card \.vip-card\s*\{[^}]*display:\s*inline-block;[^}]*width:\s*240px;[^}]*margin:\s*5px;/s) assert.match(legacyMainCss, /\.sub-vip-card \.vip-card\s*\{[^}]*display:\s*inline-block;[^}]*width:\s*240px;[^}]*margin:\s*5px;/s)
assert.match(legacyMainCss, /\.sub-vip-card \.vip-baicon\.abs-center\s*\{[^}]*font-size:\s*10em;[^}]*left:\s*55%;/s) assert.match(legacyMainCss, /\.sub-vip-card \.vip-baicon\.abs-center\s*\{[^}]*font-size:\s*10em;[^}]*left:\s*55%;/s)
assert.match(legacyMainCss, /\.sub-vip-card \.vip-img\s*\{[^}]*display:\s*none;/s) assert.match(legacyMainCss, /\.sub-vip-card \.vip-img\s*\{[^}]*display:\s*none;/s)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-vip-hover\s*\{[^}]*align-self:\s*stretch;[^}]*overflow:\s*visible;/s) // The hover wrapper must stay a plain block container so the legacy static
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-vip-hover > \.sub-vip-card\s*\{[^}]*top:\s*100%;[^}]*right:\s*0;[^}]*left:\s*auto;[^}]*max-width:\s*calc\(100vw - 24px\);/s) // position of .sub-vip-card matches the reference theme.
assert.match(headerCss, /\.navbar-top \.navbar-but > \.hover-show\s*\{[^}]*display:\s*inline-block;/s)
assert.doesNotMatch(headerCss, /\.navbar-cta|\.navbar-vip-hover/)
assert.match(legacyMainCss, /\.navbar-top \.navbar-right \.sub-menu\s*\{[^}]*right:\s*0;/s)
})
test('user card VIP CTA matches the reference zib_get_header_payvip_icon()', () => {
assert.match(headerUserbox, /'<a class="pay-vip but radius4 payvip-icon btn-block" href="javascript:;">'/)
assert.match(headerUserbox, /<svg class="em12 mr6" aria-hidden="true"><use xlink:href="#icon-vip_1"><\/use><\/svg>'/)
assert.match(headerUserbox, /esc_html__\('开通会员 尊享会员权益', 'aksrt'\)/)
for (const dead of ['fa-gem', 'vip-level']) {
assert.equal(headerUserbox.includes(dead), false, `AKSRT-only user card CTA token remains: ${dead}`)
}
assert.doesNotMatch(headerUserbox, /<span>' \. esc_html__\('开通会员 尊享会员权益'/)
}) })
test('VIP purchase modal matches legacy desktop-center and mobile-bottom positioning', () => { test('VIP purchase modal matches legacy desktop-center and mobile-bottom positioning', () => {
@@ -322,13 +349,30 @@ test('user profile and author widgets render the reference user-card contract',
}) })
test('desktop header actions share one row without generic button masks', () => { test('desktop header actions share one row without generic button masks', () => {
assert.match(headerNavbar, /aksrt-navbar-actions navbar-form navbar-right/) assert.match(headerNavbar, /navbar-form navbar-right navbar-but/)
assert.match(headerNavbar, /navbar-form navbar-right flex ac/)
assert.match(headerNavbar, /\$fold \. \$button \. \$right_but/)
assert.match(headerNavbar, /class="aksrt-nav-icon nav-search-btn main-search-btn"/) assert.match(headerNavbar, /class="aksrt-nav-icon nav-search-btn main-search-btn"/)
assert.doesNotMatch(headerNavbar, /class="btn nav-search-btn main-search-btn"/) assert.doesNotMatch(headerNavbar, /class="btn nav-search-btn main-search-btn"/)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions\s*\{[^}]*display:\s*flex;[^}]*align-items:\s*center;/s) assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account\s*\{[^}]*display:\s*inline-flex;[^}]*align-items:\s*center;/s)
assert.match(headerCss, /\.navbar-top \.aksrt-nav-icon,[\s\S]*?background:\s*transparent !important;/) assert.match(headerCss, /\.navbar-top \.aksrt-nav-icon,[\s\S]*?background:\s*transparent !important;/)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-cta\.radius\s*\{[^}]*border-radius:\s*50px;/s) // Bootstrap's base .navbar-form rule keeps margin-top: 8px at >=768px (the
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-cta > \.icon\s*\{[^}]*width:\s*1\.2em;[^}]*height:\s*1\.2em;[^}]*flex:\s*0 0 auto;/s) // override only zeroes the horizontal margins), so every right-cluster group
// starts 8px below the top of .navbar-collapse. A 34px cross-size therefore
// puts each group's midline on the navbar's 25px line, where the 50px menu row
// and the logo already sit. A 50px min-height double-counts that 8px: the
// group's content drops 8px below the line and the navbar grows from 50px to
// 58px, which also breaks the body.nav-fixed offset.
assert.doesNotMatch(headerCss, /\.navbar-top \.aksrt-navbar-account\s*\{[^}]*min-height/s)
assert.match(headerCss, /\.navbar-top \.navbar-form\.flex\.ac\s*\{[^}]*min-height:\s*34px;/s)
// The publish and VIP chips ride the legacy .but geometry verbatim; nothing in
// the header module may restate their padding, margin, radius or icon size.
// Descendant, not child: the VIP chip lives inside the hover-show wrapper.
assert.match(legacyMainCss, /\.navbar-top \.navbar-but \.but\s*\{[^}]*font-size:\s*0\.8em;[^}]*padding:\s*0\.3em 1em !important;[^}]*margin:\s*0\.4em;/s)
assert.doesNotMatch(headerCss, /\.navbar-cta|\.navbar-vip-hover/)
// No merged flex container: the three groups keep their own .navbar-form
// padding, which is what produces the reference inter-cluster spacing.
assert.doesNotMatch(headerCss, /\.aksrt-navbar-actions/)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li\s*\{[^}]*position:\s*relative;/s) assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li\s*\{[^}]*position:\s*relative;/s)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li > \.sub-menu\s*\{[^}]*top:\s*calc\(100% \+ 5px\);[^}]*right:\s*0;/s) assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li > \.sub-menu\s*\{[^}]*top:\s*calc\(100% \+ 5px\);[^}]*right:\s*0;/s)
}) })
@@ -0,0 +1,100 @@
'use strict'
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const test = require('node:test')
const root = path.resolve(__dirname, '..', '..')
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
// Reference theme (zibll css/main.css): .jb-yellow sets --this-bg-b: #f59f54 and
// --this-bg: linear-gradient(135deg, #f59f54 10%, #ff6922 100%). build/public.min.css
// is enqueued after main.min.css, so the legacy .jb-* compat layer in core/buttons.css
// overrides main.css there; the two must agree or every .jb-yellow surface (the
// user-card check-in badge first of all) paints the wrong gradient.
const referenceGradient = /--this-bg:\s*linear-gradient\(135deg,\s*#f59f54 10%,\s*#ff6922 100%\)/
const staleHexes = /#f99d4d|#f7631d/
test('buttons.css legacy .jb-yellow gradient matches the reference theme', () => {
const buttons = read('assets/css/core/buttons.css')
const start = buttons.indexOf('.jb-yellow {')
assert.notEqual(start, -1, 'missing the legacy .jb-yellow rule')
const block = buttons.slice(start, buttons.indexOf('}', start))
assert.match(block, referenceGradient)
assert.doesNotMatch(block, staleHexes)
})
test('the built public bundle carries the reference .jb-yellow gradient', () => {
const bundle = read('assets/css/build/public.min.css')
assert.match(
bundle,
/\.jb-yellow\{--this-bg: linear-gradient\(135deg, #f59f54 10%, #ff6922 100%\)\}/
)
assert.doesNotMatch(
bundle,
/\.jb-yellow\{--this-bg: linear-gradient\(135deg, #f99d4d 10%, #f7631d 100%\)\}/
)
})
test('main.css still owns the reference .jb-yellow gradient', () => {
const main = read('assets/css/main.css')
assert.match(main, referenceGradient)
assert.doesNotMatch(main, staleHexes)
})
test('the badge typography is pinned to the reference bootstrap body values', () => {
const main = read('assets/css/main.css')
const start = main.indexOf('.user-card .card-content .img-badge')
assert.notEqual(start, -1, 'missing the user-card img-badge adaptation rule')
const block = main.slice(start, main.indexOf('}', start))
assert.match(block, /line-height:\s*1\.42857143/)
// Reference body font stack (zibll bootstrap.min.css; zibll main.css does not
// override it), while AKSRT's reset imposes the modern --font-body stack.
assert.match(block, /font-family:\s*"Helvetica Neue", Helvetica, Arial, sans-serif/)
// The AKSRT html reset forces antialiased + optimizeLegibility; the badge
// restores the browser defaults the reference renders with.
assert.match(block, /-webkit-font-smoothing:\s*auto/)
assert.match(block, /text-rendering:\s*auto/)
const min = read('assets/css/main.min.css')
assert.match(
min,
/\.user-card \.card-content \.img-badge\{line-height:1\.42857143;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;-webkit-font-smoothing:auto;text-rendering:auto\}/
)
})
test('the sidebar widget link color does not recolor the check-in badge', () => {
// .widget a (sidebar.css, shipped in the homepage bundle) out-specifies the
// .jb-* color group, so the badge would paint --color-ink-2 instead of the
// white --this-color; the established .but restoration must cover img-badge.
const sidebar = read('assets/css/frontend/sidebar.css')
const start = sidebar.indexOf('.widget a.but')
assert.notEqual(start, -1, 'missing the .widget a color restoration')
const block = sidebar.slice(start, sidebar.indexOf('}', start))
assert.match(block, /\.widget a\.but,/)
assert.match(block, /\.widget a\.img-badge/)
assert.match(block, /color:\s*var\(--this-color/)
const bundle = read('assets/css/build/homepage.min.css')
assert.match(bundle, /\.widget a\.but,\.widget a\.img-badge\{color:var\(--this-color, #fff\)\}/)
})
test('the user-card check-in badge renders without the dead is-done class and bare span', () => {
const userbox = read('inc/helpers/aksrt-header-userbox.php')
const fnStart = userbox.indexOf('function aksrt_header_user_checkin_btn')
assert.notEqual(fnStart, -1, 'missing aksrt_header_user_checkin_btn')
const fnEnd = userbox.indexOf('if (!function_exists', fnStart)
assert.notEqual(fnEnd, -1)
const fn = userbox.slice(fnStart, fnEnd)
// Signed card badge: the badge class passes through untouched and the done
// label is bare text after the icon, matching the reference markup.
assert.match(fn, /aksrt_get_user_checkin_details_link\(\s*\$class,/)
assert.doesNotMatch(fn, /\$class \. ' is-done'/)
assert.doesNotMatch(fn, /'<span>'/)
// The idle badge keeps the data-signin-label hook the sign-in controller
// uses to swap labels in place.
assert.match(fn, /<span data-signin-label>/)
})
@@ -23,9 +23,13 @@ test('user and author widgets render the reference state contract', () => {
assert.match(data.guest, /signup-loader ml10 but jb-yellow padding-lg/) assert.match(data.guest, /signup-loader ml10 but jb-yellow padding-lg/)
assert.match(data.guest, /social-separator separator muted-3-color em09 mt20 mb20/) assert.match(data.guest, /social-separator separator muted-3-color em09 mt20 mb20/)
assert.match(data.guest, /social_loginbar/) assert.match(data.guest, /social_loginbar/)
assert.match(data.guest, /class="social-login-item github"/) assert.match(data.guest, /class="social-login-item github toggle-radius"/)
assert.match(data.guest, /class="social-login-item qq" data-provider="clogin_qq"/) assert.match(data.guest, /class="social-login-item qq toggle-radius" data-provider="clogin_qq"/)
assert.match(data.guest, /title="GitHub登录"/)
assert.doesNotMatch(data.guest, /aksrt-widget|aksrt-panel-wrap|user-card__auth-btns|oauth-row|oauth-btn/) assert.doesNotMatch(data.guest, /aksrt-widget|aksrt-panel-wrap|user-card__auth-btns|oauth-row|oauth-btn/)
// The social-login area carries no agreement copy; the reference guest card
// only shows the separator, the .social_loginbar and the provider buttons.
assert.doesNotMatch(data.guest, /我已阅读并同意|使用社交账号登录即表示|注册即表示/)
assert.match(data.guest_no_registration, /signin-loader but jb-blue padding-lg/) assert.match(data.guest_no_registration, /signin-loader but jb-blue padding-lg/)
assert.doesNotMatch(data.guest_no_registration, /signup-loader/) assert.doesNotMatch(data.guest_no_registration, /signup-loader/)