fix(ui): render user level icon inline beside the name

The level (等级) badge was wrapping to its own line below the username and
rendering at the intrinsic PNG size in most surfaces. Centralize the badge
sizing after zibll's `.img-icon` (height: 1.1em, inline-block) in base.css so
every route gets the same compact inline badge, and keep the name row from
flex-wrapping.

- Add shared `.aksrt-user-level-icon / .aksrt-identity-medal` rule + nowrap name
  rows in base.css (loaded on all routes).
- Header dropdown: append the level icon next to the display name (was a
  separate badges row); constrain the name to ellipsis so the badge never wraps.
- User center profile header & author hero: render the level icon inside the
  <h1> name row; drop the duplicate level chip from the signals row.
- Single article author bar / after-box author card: nowrap the name row and
  size the level icon to 1.2em (was unstyled → intrinsic PNG size).
- Comments: inner <name> nowrap with a name clamp; medal sized to 1em.
- Consolidate the per-route `.aksrt-user-level-icon` duplicates in
  user-center/level.css and profile/layout.css onto the shared rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-07-30 18:13:41 +08:00
parent bdb01fa4a2
commit 4325ce0c1c
13 changed files with 134 additions and 34 deletions
+47
View File
@@ -162,6 +162,53 @@ main.container,
} }
} }
/* ========== User identity inline icons ==========
* Level / medal icons ride inline to the right of a username. Centralized here
* so every route (header panel, user center, author hero, single, comments,
* widgets) renders the same compact badge instead of the raw intrinsic PNG size.
*
* Mirrors zibll `.img-icon { height: 1.1em; display: inline-block; vertical-align: -.15em }`
* — sizes to the surrounding line height, never dominates the row. Localized
* exceptions (UC level hero, admin column) override `height` per context.
*/
.aksrt-user-level-icon,
.aksrt-identity-medal {
display: inline-block;
width: auto;
height: 1.1em;
max-width: 6em;
object-fit: contain;
vertical-align: -.15em;
flex: 0 0 auto;
}
/* Comment context: a touch smaller to fit the 14px author line. */
.aksrt-identity-medal--comment,
.aksrt-user-level-icon--comment {
height: 1em;
vertical-align: -.12em;
}
/* No-wrap inline name rows — keep the level icon on the same line as the name.
* The non-standard <name> element (zibll compat) plus .user-name / author-hero /
* uc-header name rows must not flex-wrap, or the badge drops to a new line.
* The comment header may still wrap (avatar + name + like can overflow on
* narrow viewports), so it keeps flex-wrap but the inner <name> never does. */
.article-avatar name,
.article-avatar .name,
.user-name,
.aksrt-author-hero__name,
.uc-header__name,
.aksrt-header-badges {
flex-wrap: nowrap;
white-space: nowrap;
}
.aksrt-author-hero__name,
.uc-header__name {
display: inline-flex;
align-items: center;
gap: 4px;
}
/* ========== 通用空状态 ========== */ /* ========== 通用空状态 ========== */
.no-results { .no-results {
padding: 48px 20px; padding: 48px 20px;
@@ -30,6 +30,9 @@
.comment-header .author-box { display: flex; align-items: center; gap: 8px; width: 100%; } .comment-header .author-box { display: flex; align-items: center; gap: 8px; width: 100%; }
.comment-header .author-box name, .comment-header .author-box name,
.comment-header .author-box .flex1 { flex: 1; min-width: 0; } .comment-header .author-box .flex1 { flex: 1; min-width: 0; }
/* Keep name + level badge on one line; clamp the name so the badge never wraps. */
.comment-header .author-box name { flex-wrap: nowrap; }
.comment-header .author-box .aksrt-comment__author { max-width: 14em; }
.comment-header .aksrt-comment__like, .comment-header .aksrt-comment__like,
.comment-header .action-comment-like { .comment-header .action-comment-like {
margin-left: auto; display: inline-flex; align-items: center; gap: 4px; margin-left: auto; display: inline-flex; align-items: center; gap: 4px;
+12 -5
View File
@@ -754,15 +754,16 @@ body.dark-theme .comment-login-prompt__inner {
.medal-icon.img-icon, .medal-icon.img-icon,
.aksrt-identity-medal { .aksrt-identity-medal {
display: inline-block; display: inline-block;
width: 1.35em; width: auto;
height: 1.35em; height: 1.1em;
max-width: 6em;
object-fit: contain; object-fit: contain;
vertical-align: middle; vertical-align: -.15em;
margin-left: 4px; margin-left: 4px;
} }
.aksrt-identity-medal--comment { .aksrt-identity-medal--comment {
width: 1.15em; width: auto;
height: 1.15em; height: 1em;
} }
.aksrt-identity-medal.is-wear { .aksrt-identity-medal.is-wear {
outline: 1px solid rgba(245, 158, 11, 0.45); outline: 1px solid rgba(245, 158, 11, 0.45);
@@ -782,3 +783,9 @@ body.dark-theme .comment-login-prompt__inner {
align-items: center; align-items: center;
gap: 4px; gap: 4px;
} }
/* Inline name + level badge stay on one line; only the outer author-box wraps. */
.aksrt-comment__header name,
.aksrt-comment__header .author-box > name {
flex-wrap: nowrap;
white-space: nowrap;
}
@@ -210,6 +210,32 @@
flex-wrap: wrap; flex-wrap: wrap;
gap: 4px; gap: 4px;
} }
/* Inline name + level badge in the dropdown profile row — one line, no wrap. */
.sub-user-box .user-right > .flex1 {
min-width: 0;
}
.sub-user-box .user-right b {
display: inline-flex;
align-items: center;
gap: 4px;
flex-wrap: nowrap;
white-space: nowrap;
max-width: 100%;
overflow: hidden;
}
.sub-user-box .user-right b .display-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
max-width: 100%;
}
.sub-user-box .user-right .aksrt-user-level-icon,
.sub-user-box .user-right .aksrt-identity-medal {
height: 1em;
vertical-align: -.12em;
flex: 0 0 auto;
}
/* User dropdown light-theme text contrast */ /* User dropdown light-theme text contrast */
.user-dropdown.sub-user-box, .user-dropdown.sub-user-box,
@@ -640,7 +640,7 @@ html.dark .post-drop-meta__item:hover {
.user-card.author .user-avatar { display: flex; justify-content: center; } .user-card.author .user-avatar { display: flex; justify-content: center; }
.user-card.author .user-avatar .avatar, .user-card.author .user-avatar .avatar,
.user-card.author .user-avatar img { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; } .user-card.author .user-avatar img { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; }
.user-card.author .user-name { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 4px; } .user-card.author .user-name { display: flex; flex-wrap: nowrap; align-items: center; justify-content: center; gap: 4px; }
.user-card.author .user-name .display-name { font-weight: 700; color: var(--key-color, inherit); } .user-card.author .user-name .display-name { font-weight: 700; color: var(--key-color, inherit); }
.user-card.author .user-desc { text-align: center; } .user-card.author .user-desc { text-align: center; }
.user-card.author .user-btns { text-align: center; } .user-card.author .user-btns { text-align: center; }
+7 -5
View File
@@ -277,7 +277,7 @@
} }
.article-avatar name.flex { .article-avatar name.flex {
gap: 4px; gap: 4px;
flex-wrap: wrap; flex-wrap: nowrap;
} }
.article-avatar .aksrt-vip-badge, .article-avatar .aksrt-vip-badge,
.article-avatar .aksrt-identity-chip { .article-avatar .aksrt-identity-chip {
@@ -286,11 +286,13 @@
vertical-align: middle; vertical-align: middle;
} }
.article-avatar .medal-icon.img-icon, .article-avatar .medal-icon.img-icon,
.article-avatar .aksrt-identity-medal { .article-avatar .aksrt-identity-medal,
width: 1.4em; .article-avatar .aksrt-user-level-icon {
height: 1.4em; width: auto;
height: 1.2em;
max-width: 6em;
object-fit: contain; object-fit: contain;
vertical-align: middle; vertical-align: -.15em;
margin-left: 4px; margin-left: 4px;
} }
.article-avatar .display-name { .article-avatar .display-name {
+1 -8
View File
@@ -535,14 +535,7 @@
color: #fff; color: #fff;
} }
.aksrt-user-level-icon { /* Level icon base sizing is centralized in assets/css/frontend/base.css. */
display: inline-block;
width: auto;
height: 1.15em;
max-width: 6em;
object-fit: contain;
vertical-align: -.18em;
}
/* 用户中心 layout 间距;双栏 / sticky 见 user-center/shell.css(body.aksrt-user-center)。 */ /* 用户中心 layout 间距;双栏 / sticky 见 user-center/shell.css(body.aksrt-user-center)。 */
.aksrt-profile__layout { .aksrt-profile__layout {
+3 -9
View File
@@ -1,12 +1,6 @@
/* User level — dedicated center panel */ /* User level — dedicated center panel.
.aksrt-user-level-icon { * Base level icon sizing lives in assets/css/frontend/base.css (1.1em, inline).
display: inline-block; * This file keeps only the UC-level-specific hero/road overrides + admin column. */
width: auto;
height: 1.35em;
max-width: 6em;
object-fit: contain;
vertical-align: -.2em;
}
.aksrt-user-level-icon--admin { height: 22px; } .aksrt-user-level-icon--admin { height: 22px; }
.uc-level-page { display: grid; gap: 16px; } .uc-level-page { display: grid; gap: 16px; }
+2 -2
View File
@@ -91,8 +91,8 @@ if (!function_exists('aksrt_header_user_badges_row')) :
if ($user_id <= 0 || !function_exists('aksrt_render_user_identity_chips')) { if ($user_id <= 0 || !function_exists('aksrt_render_user_identity_chips')) {
return ''; return '';
} }
// VIP already shown next to display name in the header profile row. // VIP + level already shown next to display name in the header profile row.
$chips = aksrt_render_user_identity_chips($user_id, 'card', ['include_vip' => false]); $chips = aksrt_render_user_identity_chips($user_id, 'card', ['include_vip' => false, 'skip_level' => true]);
if ($chips === '') { if ($chips === '') {
return ''; return '';
} }
+9 -2
View File
@@ -35,6 +35,7 @@ if (!function_exists('aksrt_render_user_identity_chips')) {
* Optional. Additional options. * Optional. Additional options.
* *
* @type bool $include_vip Whether to prepend VIP badge. Default true. * @type bool $include_vip Whether to prepend VIP badge. Default true.
* @type bool $skip_level Whether to omit the level chip (rendered inline elsewhere). Default false.
* } * }
*/ */
function aksrt_render_user_identity_chips(int $user_id, string $context = 'author', array $args = []): string { function aksrt_render_user_identity_chips(int $user_id, string $context = 'author', array $args = []): string {
@@ -43,6 +44,7 @@ if (!function_exists('aksrt_render_user_identity_chips')) {
} }
$args = wp_parse_args($args, [ $args = wp_parse_args($args, [
'include_vip' => true, 'include_vip' => true,
'skip_level' => false,
]); ]);
$html = ''; $html = '';
if (!empty($args['include_vip']) && function_exists('aksrt_render_vip_badge')) { if (!empty($args['include_vip']) && function_exists('aksrt_render_vip_badge')) {
@@ -63,12 +65,16 @@ if (!function_exists('aksrt_render_user_identity_chips')) {
if ($label === '') { if ($label === '') {
continue; continue;
} }
$type = sanitize_html_class((string) ($signal['type'] ?? ''));
// Level is rendered inline next to the name by the caller; skip it here.
if (!empty($args['skip_level']) && $type === 'level') {
continue;
}
$tone = sanitize_html_class((string) ($signal['tone'] ?? 'info')); $tone = sanitize_html_class((string) ($signal['tone'] ?? 'info'));
$icon = sanitize_html_class((string) ($signal['icon'] ?? 'check')); $icon = sanitize_html_class((string) ($signal['icon'] ?? 'check'));
$icon_url = esc_url((string) ($signal['icon_url'] ?? '')); $icon_url = esc_url((string) ($signal['icon_url'] ?? ''));
$title = (string) ($signal['title'] ?? $label); $title = (string) ($signal['title'] ?? $label);
$is_wear = !empty($signal['wear']); $is_wear = !empty($signal['wear']);
$type = sanitize_html_class((string) ($signal['type'] ?? ''));
// Image medal when dictionary provides icon_url. // Image medal when dictionary provides icon_url.
if ($icon_url !== '') { if ($icon_url !== '') {
@@ -162,7 +168,8 @@ if (!function_exists('aksrt_get_user_name')) {
if ($icon_url !== '') { if ($icon_url !== '') {
$level_html = '<img class="aksrt-user-level-icon img-icon ml3" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($title) . '" loading="lazy" decoding="async" />'; $level_html = '<img class="aksrt-user-level-icon img-icon ml3" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($title) . '" loading="lazy" decoding="async" />';
} else { } else {
$level_html = '<span class="badg badg-sm ml3 aksrt-identity-chip aksrt-identity-chip--info" title="' . esc_attr($title) . '"><i class="fa fa-layer-group" aria-hidden="true"></i> ' . esc_html($label) . '</span>'; // Text fallback mirrors zibll LV badge: small pill, never oversized.
$level_html = '<span class="badg badg-sm ml3 aksrt-user-level-badge" title="' . esc_attr($title) . '">' . esc_html($label) . '</span>';
} }
} elseif ($type === 'badge' && !empty($args['medal']) && $medal_html === '' && !empty($signal['wear'])) { } elseif ($type === 'badge' && !empty($args['medal']) && $medal_html === '' && !empty($signal['wear'])) {
if ($icon_url !== '') { if ($icon_url !== '') {
+5 -1
View File
@@ -151,6 +151,10 @@ if (!function_exists('aksrt_header_user_box_html')) :
$avatar = get_avatar($user_id, 44, '', esc_attr($display_name), ['loading' => 'lazy']); $avatar = get_avatar($user_id, 44, '', esc_attr($display_name), ['loading' => 'lazy']);
$desc = (string) ($ctx['user_desc'] ?? ''); $desc = (string) ($ctx['user_desc'] ?? '');
$vip_badge = function_exists('aksrt_render_vip_badge') ? aksrt_render_vip_badge($user_id, 'sm') : ''; $vip_badge = function_exists('aksrt_render_vip_badge') ? aksrt_render_vip_badge($user_id, 'sm') : '';
// Level icon rides inline to the right of the display name (zibll parity).
$level_badge = (class_exists('\AKSRT\User\Level') && method_exists('\AKSRT\User\Level', 'get_badge_html'))
? \AKSRT\User\Level::get_badge_html($user_id, 'aksrt-user-level-icon--header')
: '';
$payvip = function_exists('aksrt_header_payvip_bar') ? aksrt_header_payvip_bar($user_id) : ''; $payvip = function_exists('aksrt_header_payvip_bar') ? aksrt_header_payvip_bar($user_id) : '';
// nav_user_pay_vip gate: empty payvip when switch off for non-VIP CTA only — helper already handles VIP status. // nav_user_pay_vip gate: empty payvip when switch off for non-VIP CTA only — helper already handles VIP status.
if (empty($ctx['nav_user_pay_vip']) && $payvip !== '' && strpos($payvip, 'is-vip') === false) { if (empty($ctx['nav_user_pay_vip']) && $payvip !== '' && strpos($payvip, 'is-vip') === false) {
@@ -239,7 +243,7 @@ if (!function_exists('aksrt_header_user_box_html')) :
$con .= $avatar; $con .= $avatar;
$con .= '<div class="user-right flex flex1 ac jsb ml10">'; $con .= '<div class="user-right flex flex1 ac jsb ml10">';
$con .= '<div class="flex1" style="max-width: calc(100% - 40px);"><b>' $con .= '<div class="flex1" style="max-width: calc(100% - 40px);"><b>'
. esc_html($display_name) . $vip_badge . '<span class="display-name text-ellipsis">' . esc_html($display_name) . '</span>' . $vip_badge . $level_badge
. '</b><div class="px12 muted-2-color text-ellipsis">' . esc_html($desc) . '</div></div>'; . '</b><div class="px12 muted-2-color text-ellipsis">' . esc_html($desc) . '</div></div>';
$con .= '</div>'; $con .= '</div>';
$con .= $msg_icon; $con .= $msg_icon;
@@ -41,6 +41,10 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
if ($vip_badge) { if ($vip_badge) {
echo $vip_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $vip_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} }
// Level icon inline to the right of the name (zibll parity).
if (class_exists('\AKSRT\User\Level') && method_exists('\AKSRT\User\Level', 'get_badge_html')) {
echo \AKSRT\User\Level::get_badge_html((int) $user_id); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
?> ?>
</h1> </h1>
<?php if ($target_user->description) : ?> <?php if ($target_user->description) : ?>
@@ -49,6 +53,12 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
<?php if (!empty($profile_signals)) : ?> <?php if (!empty($profile_signals)) : ?>
<div class="aksrt-identity-chips"> <div class="aksrt-identity-chips">
<?php foreach ($profile_signals as $signal) : ?> <?php foreach ($profile_signals as $signal) : ?>
<?php
// Level is rendered inline next to the name above; skip it here.
if ((string) ($signal['type'] ?? '') === 'level') {
continue;
}
?>
<span class="aksrt-identity-chip aksrt-identity-chip--<?php echo esc_attr($signal['tone']); ?>"> <span class="aksrt-identity-chip aksrt-identity-chip--<?php echo esc_attr($signal['tone']); ?>">
<i class="fas fa-<?php echo esc_attr($signal['icon']); ?>" aria-hidden="true"></i> <i class="fas fa-<?php echo esc_attr($signal['icon']); ?>" aria-hidden="true"></i>
<span><?php echo esc_html($signal['label']); ?></span> <span><?php echo esc_html($signal['label']); ?></span>
+8 -1
View File
@@ -98,6 +98,10 @@ $pm_url = ($pm_enabled && $messages_url !== '')
if ($vip_badge) { if ($vip_badge) {
echo $vip_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped echo $vip_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
} }
// Level icon inline to the right of the name (zibll parity).
if (class_exists('\AKSRT\User\Level') && method_exists('\AKSRT\User\Level', 'get_badge_html')) {
echo \AKSRT\User\Level::get_badge_html($user_id); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
?> ?>
</h1> </h1>
<?php endif; ?> <?php endif; ?>
@@ -124,7 +128,10 @@ $pm_url = ($pm_enabled && $messages_url !== '')
<?php echo $city_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <?php echo $city_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php endif; ?> <?php endif; ?>
<?php if (function_exists('aksrt_render_user_identity_chips')) : ?> <?php if (function_exists('aksrt_render_user_identity_chips')) : ?>
<?php echo aksrt_render_user_identity_chips($user_id, 'author', ['include_vip' => false]); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> <?php
// Level already rendered inline next to the name above; skip it here.
echo aksrt_render_user_identity_chips($user_id, 'author', ['include_vip' => false, 'skip_level' => true]); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
?>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php endif; ?> <?php endif; ?>