From c472045ce430488b8c7e66c215bdfb491e32330b Mon Sep 17 00:00:00 2001
From: unknown
Date: Sat, 17 Oct 2026 14:42:42 +0800
Subject: [PATCH] fix(auth): align the social-login surfaces with the reference
theme
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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
toggle-radius anchor per enabled provider with rel="nofollow" and a
"登录" 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.
---
assets/css/frontend/pages/base-1.css | 50 -----------------
assets/css/routes/user-center/settings.css | 3 +-
inc/helpers/aksrt-icons.php | 33 ++++++++---
inc/helpers/aksrt-links.php | 5 +-
inc/helpers/header/aksrt-header-navbar.php | 6 +-
src/Auth/OAuth.php | 55 +++++++++++++------
src/WidgetSystem/Renderers/UserRenderer.php | 2 +-
templates/parts/auth-content.php | 1 -
.../auth-source-contract.test.cjs | 55 +++++++++++++++++++
.../user-card-runtime-contract.test.cjs | 8 ++-
10 files changed, 127 insertions(+), 91 deletions(-)
diff --git a/assets/css/frontend/pages/base-1.css b/assets/css/frontend/pages/base-1.css
index 2cccdec1..51b25726 100644
--- a/assets/css/frontend/pages/base-1.css
+++ b/assets/css/frontend/pages/base-1.css
@@ -166,56 +166,6 @@ html.dark .archive-timeline__badge {
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 {
text-align: center;
margin-top: 20px;
diff --git a/assets/css/routes/user-center/settings.css b/assets/css/routes/user-center/settings.css
index 9dafdad8..fbdc466f 100644
--- a/assets/css/routes/user-center/settings.css
+++ b/assets/css/routes/user-center/settings.css
@@ -265,7 +265,8 @@
.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 .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--idle { color: var(--muted-2-color, var(--color-ink-4, #999)); }
.aksrt-account-list--social .binding-unbind-btn { color: var(--this-color, #e4a11b); }
diff --git a/inc/helpers/aksrt-icons.php b/inc/helpers/aksrt-icons.php
index 828498b8..2d2d1b8d 100644
--- a/inc/helpers/aksrt-icons.php
+++ b/inc/helpers/aksrt-icons.php
@@ -42,7 +42,7 @@ endif;
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
*/
@@ -52,10 +52,12 @@ function aksrt_brand_icon_map(): array {
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 = [
'github' => 'fa fab fa-github',
- 'google' => 'fa fab fa-google',
+ 'google' => 'zibsvg-google-color',
'x' => 'fa fab fa-twitter',
'twitter' => 'fa fab fa-twitter',
'email' => 'fa fas fa-envelope',
@@ -73,7 +75,7 @@ function aksrt_brand_icon_map(): array {
'instagram' => 'fa fab fa-instagram',
'web' => 'fa fas fa-globe',
'globe' => 'fa fas fa-globe',
- 'gitee' => 'fa fas fa-code-branch',
+ 'gitee' => 'zibsvg-gitee',
'baidu' => 'fa fas fa-paw',
'alipay' => 'fa fab fa-alipay',
];
@@ -127,10 +129,10 @@ endif;
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.
- * @return string e.g. 'fa fab fa-github', or '' if unknown.
+ * @param string $name Short key, full FA class string or zibsvg-/customsvg- value.
+ * @return string e.g. 'fa fab fa-github' or 'zibsvg-gitee', or '' if unknown.
*/
function aksrt_brand_icon_class(string $name): string {
$raw = trim($name);
@@ -138,6 +140,12 @@ function aksrt_brand_icon_class(string $name): string {
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).
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.).
@@ -164,14 +172,21 @@ endif;
if (!function_exists('aksrt_brand_icon')) :
/**
- * Escaped FA markup for a brand/contact icon.
+ * Escaped markup for a brand/contact icon (FA5 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.
* @return string Safe HTML or empty string when unknown.
*/
function aksrt_brand_icon(string $name, $extra_classes = []): string {
$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 ).
+ if (strpos($class, 'zibsvg-') === 0 || strpos($class, 'customsvg-') === 0) {
+ return aksrt_icon_markup($class, $extra_classes);
+ }
+
if ($class === '') {
return '';
}
diff --git a/inc/helpers/aksrt-links.php b/inc/helpers/aksrt-links.php
index 6e553f7e..a425a332 100644
--- a/inc/helpers/aksrt-links.php
+++ b/inc/helpers/aksrt-links.php
@@ -428,10 +428,7 @@ if (!function_exists('aksrt_submit_links_modal')) :
$body .= ''
. ' ' . esc_html__('登录', 'aksrt') . '
';
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'));
- if ($social !== '') {
- $body .= '' . $social . '
';
- }
+ $body .= (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('使用以下方式快速登录', 'aksrt'));
}
$body .= '';
} else {
diff --git a/inc/helpers/header/aksrt-header-navbar.php b/inc/helpers/header/aksrt-header-navbar.php
index 4c96ce6a..aab7fd4f 100644
--- a/inc/helpers/header/aksrt-header-navbar.php
+++ b/inc/helpers/header/aksrt-header-navbar.php
@@ -286,11 +286,7 @@ if (!function_exists('aksrt_header_user_box_html')) :
$con = '' . $href . '
';
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'));
- if ($oauth_html !== '') {
- $con .= '' . esc_html__('快速登录', 'aksrt') . '
';
- $con .= '' . $oauth_html . '
';
- }
+ $con .= (string) \AKSRT\Auth\OAuth::get_instance()->render_social_buttons(__('快速登录', 'aksrt'));
}
}
diff --git a/src/Auth/OAuth.php b/src/Auth/OAuth.php
index a0bcde1f..afe21ca5 100644
--- a/src/Auth/OAuth.php
+++ b/src/Auth/OAuth.php
@@ -225,29 +225,48 @@ final class OAuth {
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 {
$items = $this->get_auth_providers_data();
if ($items === []) {
return '';
}
- ob_start();
- ?>
-
-
-
-
- ' . esc_html($title) . '
'
+ : '';
+ $html .= '';
}
public function get_frontend_bindings_data(int $user_id): array {
diff --git a/src/WidgetSystem/Renderers/UserRenderer.php b/src/WidgetSystem/Renderers/UserRenderer.php
index 2bc8bd40..131e6c64 100644
--- a/src/WidgetSystem/Renderers/UserRenderer.php
+++ b/src/WidgetSystem/Renderers/UserRenderer.php
@@ -125,7 +125,7 @@ final class UserRenderer {
if ($name === '' || $style_name === '' || $label === '' || $url === '') {
continue;
}
- $html .= '' . (string) ($item['icon'] ?? '') . ' ';
+ $html .= '' . (string) ($item['icon'] ?? '') . ' ';
}
return $html . '';
}
diff --git a/templates/parts/auth-content.php b/templates/parts/auth-content.php
index 0bbcf171..87999052 100644
--- a/templates/parts/auth-content.php
+++ b/templates/parts/auth-content.php
@@ -147,6 +147,5 @@ $oauth_bind_pass = $oauth_intent === 'bind'
-
diff --git a/tests/widget-system/auth-source-contract.test.cjs b/tests/widget-system/auth-source-contract.test.cjs
index 9f7c2031..0e915734 100644
--- a/tests/widget-system/auth-source-contract.test.cjs
+++ b/tests/widget-system/auth-source-contract.test.cjs
@@ -239,3 +239,58 @@ 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.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 .
+ 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, /' '/);
+ 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/);
+});
diff --git a/tests/widget-system/user-card-runtime-contract.test.cjs b/tests/widget-system/user-card-runtime-contract.test.cjs
index 1d5b77c0..6413e52b 100644
--- a/tests/widget-system/user-card-runtime-contract.test.cjs
+++ b/tests/widget-system/user-card-runtime-contract.test.cjs
@@ -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, /social-separator separator muted-3-color em09 mt20 mb20/)
assert.match(data.guest, /social_loginbar/)
- assert.match(data.guest, /class="social-login-item github"/)
- assert.match(data.guest, /class="social-login-item qq" data-provider="clogin_qq"/)
+ assert.match(data.guest, /class="social-login-item github toggle-radius"/)
+ 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/)
+ // 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.doesNotMatch(data.guest_no_registration, /signup-loader/)