polish: icon sizing hardened for cross-browser (review I-1/M2/M3/M4)

- .sf-tp-icon CSS width/height (Safari doesn't support var() in SVG
  presentation attributes); --sm variant class replaces size= call sites
- search icon centered via translateY, decoupled from input height
- bare-arrow test: full charset (→←↗↘⌕▾) + line-level › whitelist
- new test: site.html tp_arrow must stay in sync with macro library path

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-23 09:00:50 +08:00
parent 22b614118b
commit 1dd510285a
4 changed files with 31 additions and 5 deletions
+17 -1
View File
@@ -204,10 +204,26 @@ def test_trade_pro_templates_have_no_bare_text_arrows():
for f in root.rglob('*.html'): for f in root.rglob('*.html'):
t = f.read_text(encoding='utf-8') t = f.read_text(encoding='utf-8')
body = re.sub(r'\{#.*?#\}', '', t, flags=re.S) body = re.sub(r'\{#.*?#\}', '', t, flags=re.S)
if '→' in body or ('›' in body and 'chevron' not in body): if any(ch in body for ch in '→←↗↘⌕▾'):
offenders.append(str(f.relative_to(root))) offenders.append(str(f.relative_to(root)))
# › 豁免通道收紧为行级:同行的 chevron 语义才放行
for line in body.splitlines():
if '›' in line and 'chevron' not in line:
offenders.append(f'{f.relative_to(root)}:›')
assert offenders == [] assert offenders == []
def test_site_html_arrow_matches_macro_library():
# 双源防护:site.html 的自包含 tp_arrow 必须与宏库 arrow-right 的 path 一致
from pathlib import Path
import re
root = Path(__file__).resolve().parent.parent / 'themes' / 'trade-pro'
macro = (root / '_professional_components.html').read_text(encoding='utf-8')
site = (root / 'site.html').read_text(encoding='utf-8')
m = re.search(r"'arrow-right'\s*:\s*'([^']+)'", macro)
s = re.search(r"tp_arrow.*?d=\"([^\"]+)\"", site, flags=re.S)
assert m and s, 'icon path sources not found'
assert m.group(1) == s.group(1), 'site.html tp_arrow drifted from macro library arrow-right'
def test_theme_blocks_css_overrides_platform(tmp_path): def test_theme_blocks_css_overrides_platform(tmp_path):
from app import builder from app import builder
dist = tmp_path / 'assets'; dist.mkdir() dist = tmp_path / 'assets'; dist.mkdir()
@@ -39,5 +39,5 @@
{% macro grid(items,kind='product') %}<div class="pro-grid pro-grid-{{ kind }}">{% for item in items %}{{ card(item,kind,loop.index0) }}{% else %}<div class="pro-empty">No published content in this section yet.</div>{% endfor %}</div>{% endmacro %} {% macro grid(items,kind='product') %}<div class="pro-grid pro-grid-{{ kind }}">{% for item in items %}{{ card(item,kind,loop.index0) }}{% else %}<div class="pro-empty">No published content in this section yet.</div>{% endfor %}</div>{% endmacro %}
{% macro section_title(eyebrow,heading,href='') %}<div class="pro-section-head"><div><span class="pro-eyebrow">{{ eyebrow }}</span><h2>{{ heading }}</h2></div>{% if href %}<a class="pro-text-link" href="{{ href }}">Browse all {{ icon('arrow-right') }}</a>{% endif %}</div>{% endmacro %} {% macro section_title(eyebrow,heading,href='') %}<div class="pro-section-head"><div><span class="pro-eyebrow">{{ eyebrow }}</span><h2>{{ heading }}</h2></div>{% if href %}<a class="pro-text-link" href="{{ href }}">Browse all {{ icon('arrow-right') }}</a>{% endif %}</div>{% endmacro %}
{% macro filters() %}<div class="pro-filter-panel"><label><span>Search</span><span class="pro-filter-search-box">{{ icon('search') }}<input type="search" data-pro-filter-search placeholder="Filter by name or content"></span></label><label><span>Brand</span><select data-pro-filter-brand><option value="">All brands</option></select></label><label><span>Category</span><select data-pro-filter-category><option value="">All categories</option></select></label><span class="pro-filter-count" data-pro-filter-count role="status"></span></div>{% endmacro %} {% macro filters() %}<div class="pro-filter-panel"><label><span>Search</span><span class="pro-filter-search-box">{{ icon('search') }}<input type="search" data-pro-filter-search placeholder="Filter by name or content"></span></label><label><span>Brand</span><select data-pro-filter-brand><option value="">All brands</option></select></label><label><span>Category</span><select data-pro-filter-category><option value="">All categories</option></select></label><span class="pro-filter-count" data-pro-filter-count role="status"></span></div>{% endmacro %}
{% macro footer_links(nav) %}<ul>{% for item in nav[:7] %}<li><a href="{{item.route}}">{{item.title}} {{ icon('arrow-right', size='var(--sf-tp-icon-size-sm, 16px)') }}</a></li>{% endfor %}</ul>{% endmacro %} {% macro footer_links(nav) %}<ul>{% for item in nav[:7] %}<li><a href="{{item.route}}">{{item.title}} {{ icon('arrow-right', cls='sf-tp-icon--sm') }}</a></li>{% endfor %}</ul>{% endmacro %}
{% macro body_copy(content,page) %}{% if content %}<div class="pro-rich">{{ content }}</div>{% elif page.summary %}<p class="pro-lead">{{ page.summary }}</p>{% else %}<p class="pro-muted">Complete this page's content in the admin console.</p>{% endif %}{% endmacro %} {% macro body_copy(content,page) %}{% if content %}<div class="pro-rich">{{ content }}</div>{% elif page.summary %}<p class="pro-lead">{{ page.summary }}</p>{% else %}<p class="pro-muted">Complete this page's content in the admin console.</p>{% endif %}{% endmacro %}
+1 -1
View File
@@ -16,7 +16,7 @@
keeping all 7 default sections so only the labels change, never coverage. #} keeping all 7 default sections so only the labels change, never coverage. #}
{% set tp_nav=[{'title':'Home','route':'/'},{'title':'Products','route':'/products/'},{'title':'Brands','route':'/brands/'},{'title':'News','route':'/news/'},{'title':'Cases','route':'/cases/'},{'title':'About','route':'/about/'},{'title':'Contact','route':'/contact/'}] if tp_needs_en_nav else nav %} {% set tp_nav=[{'title':'Home','route':'/'},{'title':'Products','route':'/products/'},{'title':'Brands','route':'/brands/'},{'title':'News','route':'/news/'},{'title':'Cases','route':'/cases/'},{'title':'About','route':'/about/'},{'title':'Contact','route':'/contact/'}] if tp_needs_en_nav else nav %}
{% if chrome_is_custom %}<div class="pro-utility"{% if not chrome.header.show_announcement %} hidden{% endif %}><div class="pro-wrap"><span>{{ chrome.header.announcement }}</span><span>{{ site.tagline or site.name }}</span></div></div>{% else %}<div class="pro-utility"><div class="pro-wrap"><span>GLOBAL SOURCING · QUALITY · DELIVERY</span><span>INTERNATIONAL B2B SOLUTIONS</span></div></div>{% endif %} {% if chrome_is_custom %}<div class="pro-utility"{% if not chrome.header.show_announcement %} hidden{% endif %}><div class="pro-wrap"><span>{{ chrome.header.announcement }}</span><span>{{ site.tagline or site.name }}</span></div></div>{% else %}<div class="pro-utility"><div class="pro-wrap"><span>GLOBAL SOURCING · QUALITY · DELIVERY</span><span>INTERNATIONAL B2B SOLUTIONS</span></div></div>{% endif %}
<header class="sf-site-head pro-header"><div class="pro-wrap pro-head-inner"><a class="pro-logo" href="/" aria-label="{{site.name}} home">{% if safe_image(site.logo) %}<img class="pro-brand-logo" src="{{safe_image(site.logo)}}" alt="{{site.name}} Logo">{% else %}<span class="pro-logo-symbol">▣</span>{% endif %}<span class="pro-logo-name">{{site.name}}<small> GLOBAL B2B</small></span></a><button class="pro-mobile-toggle" type="button" data-pro-menu aria-controls="pro-navigation" aria-expanded="false" aria-label="Open navigation">☰</button><nav id="pro-navigation" class="pro-navigation" aria-label="Main navigation">{% for link in tp_nav %}{% if link.children %}<div class="pro-nav-group"><a href="{{link.route}}">{{link.title}}</a><button type="button" class="pro-nav-disclose" aria-expanded="false" aria-label="Expand {{link.title}} submenu">{{ icon('chevron-down', size='var(--sf-tp-icon-size-sm, 16px)') }}</button><div class="pro-nav-children" hidden>{% for child in link.children %}<a href="{{child.route}}">{{child.title}}</a>{% for leaf in child.children %}<a class="pro-nav-leaf" href="{{leaf.route}}">— {{leaf.title}}</a>{% endfor %}{% endfor %}</div></div>{% else %}<a href="{{link.route}}">{{link.title}}</a>{% endif %}{% endfor %}{% if chrome.header.show_search %}<a class="pro-search-link" href="/search/">Search {{ icon('search', size='var(--sf-tp-icon-size-sm, 16px)') }}</a>{% endif %}</nav><button class="pro-catalog-toggle" type="button" data-pro-catalog aria-controls="pro-mega-content" aria-expanded="false">Products {{ icon('chevron-down', size='var(--sf-tp-icon-size-sm, 16px)') }}</button>{% if chrome_is_custom %}{% if chrome.header.show_cta %}<a href="{{chrome.header.cta_href}}" class="sf-tp-btn pro-header-cta">{{chrome.header.cta_text}} <span>{{ icon('arrow-right') }}</span></a>{% endif %}{% else %}<a href="/contact/" class="sf-tp-btn pro-header-cta">Request a Quote <span>{{ icon('arrow-right') }}</span></a>{% endif %}</div>{% if categories %}<div class="pro-mega pro-wrap" data-pro-mega id="pro-mega-content" hidden><span>Product categories</span><div>{% for cat in categories[:12] %}<a href="{{cat.url}}">{{cat.name}} {{ icon('arrow-right') }}</a>{% endfor %}</div></div>{% endif %}</header> <header class="sf-site-head pro-header"><div class="pro-wrap pro-head-inner"><a class="pro-logo" href="/" aria-label="{{site.name}} home">{% if safe_image(site.logo) %}<img class="pro-brand-logo" src="{{safe_image(site.logo)}}" alt="{{site.name}} Logo">{% else %}<span class="pro-logo-symbol">▣</span>{% endif %}<span class="pro-logo-name">{{site.name}}<small> GLOBAL B2B</small></span></a><button class="pro-mobile-toggle" type="button" data-pro-menu aria-controls="pro-navigation" aria-expanded="false" aria-label="Open navigation">☰</button><nav id="pro-navigation" class="pro-navigation" aria-label="Main navigation">{% for link in tp_nav %}{% if link.children %}<div class="pro-nav-group"><a href="{{link.route}}">{{link.title}}</a><button type="button" class="pro-nav-disclose" aria-expanded="false" aria-label="Expand {{link.title}} submenu">{{ icon('chevron-down', cls='sf-tp-icon--sm') }}</button><div class="pro-nav-children" hidden>{% for child in link.children %}<a href="{{child.route}}">{{child.title}}</a>{% for leaf in child.children %}<a class="pro-nav-leaf" href="{{leaf.route}}">— {{leaf.title}}</a>{% endfor %}{% endfor %}</div></div>{% else %}<a href="{{link.route}}">{{link.title}}</a>{% endif %}{% endfor %}{% if chrome.header.show_search %}<a class="pro-search-link" href="/search/">Search {{ icon('search', cls='sf-tp-icon--sm') }}</a>{% endif %}</nav><button class="pro-catalog-toggle" type="button" data-pro-catalog aria-controls="pro-mega-content" aria-expanded="false">Products {{ icon('chevron-down', cls='sf-tp-icon--sm') }}</button>{% if chrome_is_custom %}{% if chrome.header.show_cta %}<a href="{{chrome.header.cta_href}}" class="sf-tp-btn pro-header-cta">{{chrome.header.cta_text}} <span>{{ icon('arrow-right') }}</span></a>{% endif %}{% else %}<a href="/contact/" class="sf-tp-btn pro-header-cta">Request a Quote <span>{{ icon('arrow-right') }}</span></a>{% endif %}</div>{% if categories %}<div class="pro-mega pro-wrap" data-pro-mega id="pro-mega-content" hidden><span>Product categories</span><div>{% for cat in categories[:12] %}<a href="{{cat.url}}">{{cat.name}} {{ icon('arrow-right') }}</a>{% endfor %}</div></div>{% endif %}</header>
{% if not designer_replace %}{% include 'trade-pro/pages/' ~ kind ~ '.html' %}{% endif %} {% if not designer_replace %}{% include 'trade-pro/pages/' ~ kind ~ '.html' %}{% endif %}
{% if has_custom and kind!='notfound' %}<main id="main-content" class="sf-canvas pro-custom-content">{{custom_blocks}}</main>{% else %}<main id="main-content" class="sf-canvas pro-custom-content" aria-hidden="true"></main>{% endif %} {% if has_custom and kind!='notfound' %}<main id="main-content" class="sf-canvas pro-custom-content">{{custom_blocks}}</main>{% else %}<main id="main-content" class="sf-canvas pro-custom-content" aria-hidden="true"></main>{% endif %}
+12 -2
View File
@@ -587,11 +587,20 @@ body.sf-theme-trade-pro {
/* ── 图标(Task 9 SVG 宏库:内联线条图标的对齐与间距)── */ /* ── 图标(Task 9 SVG 宏库:内联线条图标的对齐与间距)── */
/* 基线对齐:inline 上下文垂直居中,flex 上下文不被压缩换行 */ /* 基线对齐:inline 上下文垂直居中,flex 上下文不被压缩换行。
尺寸在 CSS 层固定(Safari 等引擎不支持 width/height 属性中的 var(),
属性值仅作无 CSS 场景的渐进增强;CSS 覆盖属性保证跨引擎一致) */
.sf-tp-icon { .sf-tp-icon {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
flex-shrink: 0; flex-shrink: 0;
width: var(--sf-tp-icon-size-md, 20px);
height: var(--sf-tp-icon-size-md, 20px);
}
.sf-tp-icon--sm {
width: var(--sf-tp-icon-size-sm, 16px);
height: var(--sf-tp-icon-size-sm, 16px);
} }
/* 按钮内紧跟文字的图标留出呼吸空间(flex gap 之外的显式间距, /* 按钮内紧跟文字的图标留出呼吸空间(flex gap 之外的显式间距,
@@ -617,7 +626,8 @@ body.sf-theme-trade-pro {
.sf-theme-trade-pro .pro-filter-search-box .sf-tp-icon { .sf-theme-trade-pro .pro-filter-search-box .sf-tp-icon {
position: absolute; position: absolute;
inset-inline-start: var(--sf-tp-space-3); inset-inline-start: var(--sf-tp-space-3);
inset-block-end: var(--sf-tp-space-3); inset-block-start: 50%;
transform: translateY(-50%);
pointer-events: none; pointer-events: none;
color: var(--sf-tp-color-text-muted); color: var(--sf-tp-color-text-muted);
} }