feat(auth): rebuild the sign-in surfaces on the shared sign-card contract

The login/register page and the auth modal had drifted into a parallel
system: floating-input fields, a sign-page-card shell and a bespoke
jio-auth-modal wrapper that none of the theme's own sign CSS targeted.
The reference card rules (.sign, .sign-img, .sign-content, .line-form-input,
.scale-placeholder, .social_loginbar, .title-h-left, #u_sign .modal-dialog,
.qrcode-signin-container) have been in main.css all along, so the fix is to
render the reference DOM instead of re-styling a second one.

auth-content.php now emits one .sign.jio-panel.blur-bg.relative card with a
.tab-content of #tab-sign-in / #tab-sign-up / #tab-resetpassword /
#tab-qrcode-signin / #tab-bind panes. Each pane opens with a
.title-h-left.fa-2x heading plus a .muted-color.px12 link, fields are
.relative.line-form.mb10 > .line-form-input + .scale-placeholder +
.line-form-line, the verification trigger is the .yztx.abs-right captchsubmit
button, and submits are .but.radius.jb-blue|jb-green.padding-lg.btn-block.
Social entries come from the shared render_social_buttons() separator +
.social_loginbar, and the OAuth bind pane uses the oauth-data-box avatar card.

The modal shell is #u_sign.modal > .modal-dialog > .sign-content >
.sign-img.absolute.hide-sm + .sign, and the page shell is the reference
.container.sign-page > .row.sign-row.gutters-5 > .col-md-6(.col-md-offset-6/
.col-md-offset-3).sign-col with a .fixed.lazyload background and a blurred
fixed footer. auth.js drives .in instead of .is-open, toggles the new
password/signin-mode/invite-gate controls and no longer touches the retired
markup. The page also drops two footer-hook removals that targeted functions
which no longer exist, and opts out of the fixed-nav offset.

The sign page's custom theme setting now actually applies: Router answers
jio_forced_theme_mode, jioData.themeForced wins over the stored cookie in
theme.js, theme-toggle positions are suppressed, and the page marks html/body
dark or white accordingly.

sign.css shrinks from ~114 lines of bespoke component styling to a thin
positioning layer, and the retired classes are gone rather than kept.

Verification: php -l on every changed PHP file, node --check on both JS files,
node --test across widget-system, navigation and front-submit (873 passing),
and tools/audit-psr4-paths.php / audit-admin-contracts.php both at 0 errors.
assets/js/runtime/main.min.js was rebuilt because theme.js gained the forced
theme branch.
This commit is contained in:
kate522 committed 2026-11-04 16:04:24 +08:00
1 parent d40906b4a3
commit 41366e7fc4
15 files changed
+664 -281

No files matched your search

@@ -28,15 +28,20 @@ test('modal and sign page use one renderer and one state machine', () => {
assert.doesNotMatch(loader, /sign-register/);
});
test('static authentication dimensions and responsive contract are retained', () => {
const css = read('assets/css/routes/sign.css');
assert.match(css, /\.sign\s*\{[\s\S]*?max-width:\s*330px/);
assert.match(css, /#u_sign\.has-sign-img \.modal-dialog\s*\{\s*width:\s*800px/);
assert.match(css, /#u_sign \.sign-img\s*\{[^}]*inset:\s*0 45% 0 0;[^}]*padding:\s*0/);
assert.match(css, /#u_sign \.sign-img \+ \.sign\s*\{[^}]*margin-left:\s*55%/);
assert.match(css, /\.social-login-item\s*\{[\s\S]*?width:\s*32px;[\s\S]*?height:\s*32px/);
assert.match(css, /\.auth-qrcode\s*\{[\s\S]*?width:\s*180px;[\s\S]*?min-height:\s*180px/);
assert.match(css, /@media \(max-width:\s*767px\)[\s\S]*?\.sign-img\s*\{\s*display:\s*none/);
test('authentication shell reuses the shared sign-card CSS contract', () => {
// Card width, the 800px split dialog, the 55% card offset, the 32px social
// items and the QR box all live in the shared theme stylesheet now.
const mainCss = read('assets/css/main.css');
const signCss = read('assets/css/routes/sign.css');
assert.match(mainCss, /\.sign\s*\{[\s\S]*?max-width:\s*330px/);
assert.match(mainCss, /#u_sign \.modal-dialog\s*\{\s*width:\s*800px;/);
assert.match(mainCss, /\.sign-img \+ \.sign\s*\{[\s\S]*?margin-left:\s*55%/);
assert.match(mainCss, /\.social-login-item\s*\{[\s\S]*?width:\s*32px !important;[\s\S]*?height:\s*32px !important/);
assert.match(mainCss, /\.qrcode-signin-container\s*\{[\s\S]*?min-height:\s*254px/);
assert.match(mainCss, /\.signin-qrcode-img\s*\{[\s\S]*?width:\s*180px/);
assert.match(mainCss, /#u_sign \.modal-dialog\s*\{\s*width:\s*800px;/);
assert.match(signCss, /\.sign-page \.sign\s*\{\s*width:\s*350px/);
assert.match(signCss, /@media \(max-width:\s*767px\)[\s\S]*?\.sign-page \.sign\s*\{[\s\S]*?width:\s*340px/);
});
test('authentication settings use reference defaults and canonical keys', () => {
@@ -72,10 +77,10 @@ test('authentication galleries save attachment IDs and are consumed by both surf
assert.match(reader, /auth_gallery_ids\('sign_modal_background'\)/);
assert.match(reader, /auth_gallery_ids\('sign_page_background'\)/);
assert.match(modal, /wp_get_attachment_image\(\$image_id, 'full'/);
assert.match(modal, /<div class="sign-img"><\?php echo \$image_html/);
assert.match(modal, /<div class="sign-img absolute hide-sm"><\?php echo \$image_html/);
assert.match(page, /wp_get_attachment_image_url\(\$background_id, 'full'\)/);
assert.match(page, /<div class="sign-page-background" style="background-image:url/);
assert.match(css, /\.sign-img img\s*\{[\s\S]*?object-fit:\s*cover/);
assert.match(page, /<div class="fixed lazyload" data-bg="<\?php echo esc_url\(\$background_url\)/);
assert.match(css, /#u_sign \.sign-img img\s*\{[\s\S]*?object-fit:\s*cover/);
assert.doesNotMatch(modal + page, /is_string\(\$images\)|explode\(',', \$images\)/);
});
@@ -163,18 +168,19 @@ test('shared renderer contracts every configured authentication state', () => {
const modal = read('templates/parts/modal-auth.php');
const client = read('assets/js/auth.js');
assert.match(shared, /data-auth-password-fields/);
assert.match(shared, /data-auth-code-fields/);
assert.match(shared, /data-login-mode/);
assert.match(shared, /data-signin-panel="nopas"/);
assert.match(shared, /data-signin-panel="pas"/);
assert.match(shared, /data-signin-mode="pas"/);
['login', 'register', 'resetpassword'].forEach((scene) => {
assert.match(shared, new RegExp(`data-send-code="${scene}"`));
});
assert.match(shared, /\$omit_confirm/);
assert.match(shared, /\$ref_mode !== 'hidden'/);
assert.match(shared, /\$ref_mode === 'required'/);
assert.match(shared, /sign-agreement-implicit/);
assert.match(shared, /data-invite-gate-panel/);
assert.match(shared, /data-invite-form-panel/);
assert.match(shared, /name="agree_terms"[\s\S]*?required/);
assert.match(shared, /oauth-button-lg/);
assert.match(shared, /render_social_buttons\(__\('社交账号登录'/);
assert.match(shared, /data-auth-qrcode/);
assert.match(modal, /data-auto=/);
assert.match(client, /jioAuthAutoUntil/);
@@ -220,8 +226,8 @@ test('OAuth frontend is accessible, redirect-aware, and removes retired provider
assert.match(client, /setAttribute\('aria-label', visible \? '隐藏密码' : '显示密码'\)/);
assert.match(client, /setAttribute\('aria-pressed', visible \? 'true' : 'false'\)/);
assert.match(shared, /data-auth-qr-status[^>]*role="status"[^>]*aria-live="polite"/);
assert.match(shared, /data-provider=/);
assert.match(shared, /aria-label="<\?php echo esc_attr\(sprintf/);
assert.match(read('src/Auth/OAuth.php'), /data-provider="' \. esc_attr\(\$name\)/);
assert.match(read('src/Auth/OAuth.php'), /aria-label="' \. esc_attr\(sprintf/);
assert.match(client, /searchParams\.set\('redirect_to', redirect\)/);
assert.match(client, /oauth_action/);
@@ -240,14 +246,15 @@ test('OAuth frontend is accessible, redirect-aware, and removes retired provider
assert.match(mainCss, /\.pay-box \.but\.baidu/);
});
test('the social-login area drops the implicit agreement line', () => {
test('agreement copy sits under each panel, not under the user card', () => {
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, /注册即表示/);
// The sign-in column states the social agreement, the registration panel
// states its own, and the compact user card carries neither line.
assert.match(shared, /使用社交账号登录即表示同意/);
assert.match(shared, /扫码登录即表示同意/);
assert.match(shared, /注册即表示同意/);
assert.match(shared, /name="agree_terms"[\s\S]*?required/);
assert.doesNotMatch(userRenderer, /使用社交账号登录即表示|我已阅读并同意/);
});
@@ -308,7 +315,7 @@ test('provider URLs stay raw in the data layer and are escaped once per surface'
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(oauth, /href="' \. esc_url\(\$url\) \. '" class="' \. esc_attr\(\$class\)/);
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">/);
@@ -68,6 +68,7 @@ test('theme initializes when the removed jio namespace is absent', () => {
getElementById() { return null; },
createElement() { return { setAttribute() {} }; },
dispatchEvent() {},
addEventListener() {},
};
const window = {
document,
@@ -36,6 +36,7 @@ function is_user_logged_in(): bool { return false; }
function get_current_user_id(): int { return 0; }
function wp_max_upload_size(): int { return 1048576; }
function is_page_template(string $template = ''): bool { return false; }
function apply_filters(string $hook, $value) { return $value; }
function jio_is_lazy(string $scope): bool { return false; }
function jio_get_lazy_thumb(string $size = ''): string { return 'https://example.test/lazy.svg'; }
function jio_get_default_avatar_url(): string { return 'https://example.test/avatar.png'; }