feat: SVG icon macro library replaces bare text arrows in trade-pro

Task 9 (new design, not a cms port): _professional_components.html gains
an icon(name, size, cls) macro — 10 inline line-style glyphs (arrow-right,
arrow-left, chevron-down, check, phone, mail, location, download, search,
quote), 24x24 viewBox, stroke=currentColor, stroke-width 1.5, round
caps/joins, decorative aria-hidden + focusable=false, default size
var(--sf-tp-icon-size-md, 20px), unknown names fall back to arrow-right.
Every glyph is a single <path> so the d attribute needs no |safe.

All bare text arrows (->, <-, ↗, ▾, ⌕) in the macro library, pro.html,
site.html and pages/* are replaced with macro calls (site.html keeps a
self-contained copy of the arrow glyph because its rendering envs cannot
import the macro library); the filters macro search box gains an inline
search icon; footer links gain small arrows. theme.css adds .sf-tp-icon
alignment, in-button icon spacing, and search-box icon positioning.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-23 08:35:52 +08:00
parent afecc10aad
commit 22b614118b
13 changed files with 127 additions and 24 deletions
+31
View File
@@ -177,6 +177,37 @@ def test_theme_color_map_covers_trade_pro():
{'title': 'Home', 'route': '/'}, [], mode='home')
assert '<meta name="theme-color" content="#0b3d91"' in html
def test_trade_pro_icon_macro_library():
# Task 9:_professional_components.html 提供 icon(name, size, cls) 内联 SVG 宏。
# 纯服务端输出:无 JS 依赖,装饰性图标 aria-hidden + focusable=false,
# stroke=currentColor 随文字颜色走。
from app import theme_pro
env = theme_pro.env_for('trade-pro')
tpl = env.from_string("{% from '_professional_components.html' import icon %}{{ icon('arrow-right') }}")
out = tpl.render()
assert '<svg' in out and 'aria-hidden="true"' in out and 'currentColor' in out
# 未知图标名回退 arrow-right(不抛错),且回退输出与显式 arrow-right 一致
tpl2 = env.from_string("{% from '_professional_components.html' import icon %}{{ icon('no-such') }}")
fallback = tpl2.render()
assert '<svg' in fallback
assert fallback == tpl.render()
def test_trade_pro_templates_have_no_bare_text_arrows():
# Task 9:交互元素的箭头必须是 SVG 图标,不得残留裸文本 → / ›。
# 判定逻辑:先剔除 Jinja 注释 {# #} 内的说明文字;› 的豁免通道是
# 'chevron' 同现(下拉指示语义)——非图标用途(如版权分隔符)可走该
# 白名单,当前主题模板应一个裸箭头都没有。
from pathlib import Path
import re
root = Path(__file__).resolve().parent.parent / 'themes' / 'trade-pro'
offenders = []
for f in root.rglob('*.html'):
t = f.read_text(encoding='utf-8')
body = re.sub(r'\{#.*?#\}', '', t, flags=re.S)
if '→' in body or ('›' in body and 'chevron' not in body):
offenders.append(str(f.relative_to(root)))
assert offenders == []
def test_theme_blocks_css_overrides_platform(tmp_path):
from app import builder
dist = tmp_path / 'assets'; dist.mkdir()
+31 -7
View File
@@ -1,6 +1,30 @@
{# trade-pro skeleton macro library: passthrough copy of the shared
themes/_professional_components.html with English strings. Phase 3 (Task 9+)
replaces these with the ported cms design and the SVG icon set. #}
{# trade-pro macro library: passthrough copy of the shared
themes/_professional_components.html with English strings, plus the
trade-pro inline SVG icon set (Task 9). Icons are decorative
server-side inlines: no JS dependency, currentColor strokes. #}
{# ── SVG icon set (Task 9) ──────────────────────────────────────────────
Style contract: 24×24 viewBox, stroke=currentColor, stroke-width 1.5,
fill none, round caps/joins — the same modern line language as the
ported cms design. Every glyph is ONE <path> (subpaths allowed) so the
d attribute survives autoescaping without |safe. Unknown names fall
back to arrow-right instead of raising (page code typos must never
take a build down). #}
{% set _tp_icon_paths={
'arrow-right':'M5 12h14M12 5l7 7-7 7',
'arrow-left':'M19 12H5M12 19l-7-7 7-7',
'chevron-down':'M6 9l6 6 6-6',
'check':'M20 6 9 17 4 12',
'phone':'M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.79 19.79 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.36 1.9.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.91.34 1.85.57 2.81.7A2 2 0 0 1 22 16.92z',
'mail':'M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2Zm18 2-10 7L2 6',
'location':'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0ZM12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z',
'download':'M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3',
'search':'M11 3a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM21 21l-4.35-4.35',
'quote':'M8 3a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2 1 1 0 0 0-1 1v1a1 1 0 0 0 1 1 2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-2a2 2 0 0 1 2-2 1 1 0 0 0 1-1v-1a1 1 0 0 0-1-1 2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4ZM16 3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2 1 1 0 0 1 1 1v1a1 1 0 0 1-1 1 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2 1 1 0 0 1-1-1v-1a1 1 0 0 1 1-1 2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-4Z'
} %}
{% macro icon(name, size=None, cls='') -%}
{%- set _d=_tp_icon_paths.get(name) or _tp_icon_paths['arrow-right'] -%}
<svg class="sf-tp-icon{% if cls %} {{ cls }}{% endif %}" width="{{ size if size else 'var(--sf-tp-icon-size-md, 20px)' }}" height="{{ size if size else 'var(--sf-tp-icon-size-md, 20px)' }}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="{{ _d }}"/></svg>
{%- endmacro %}
{% macro cover(item, kind='product') %}
{% if item.cover %}<img class="pro-photo" src="{{ safe_image(item.cover) }}" alt="{{ item.title }}" loading="lazy">
{% else %}<div class="pro-art pro-art-{{ kind }}"><span class="pro-art-ring"></span><span class="pro-art-center">{{ (item.title or 'SF')[:2] }}</span><span class="pro-art-label">{{ kind|upper }}</span></div>{% endif %}
@@ -9,11 +33,11 @@
<a class="pro-card pro-card-{{ kind }}" href="{{ safe_href(item.route) }}" data-category="{{ fields_of(item).get('category','') }}" data-brand="{{ fields_of(item).get('brand','') }}">
<div class="pro-card-cover">{{ cover(item,kind) }}</div>
<div class="pro-card-body"><div class="pro-card-meta"><span>{{ fields_of(item).get('brand') or fields_of(item).get('category') or {'product':'PRODUCT','vehicle':'VEHICLE','post':'NEWS','case':'CASE','brand':'BRAND','service':'SERVICE'}.get(item.type,'CONTENT') }}</span><span>{{ '%02d'|format(position+1) }}</span></div>
<h3>{{ item.title }}</h3><p>{{ (item.summary or '')[:128] }}</p><span class="pro-card-link">View details <span aria-hidden="true">↗</span></span></div>
<h3>{{ item.title }}</h3><p>{{ (item.summary or '')[:128] }}</p><span class="pro-card-link">View details {{ icon('arrow-right') }}</span></div>
</a>
{% 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 <span>↗</span></a>{% endif %}</div>{% endmacro %}
{% macro filters() %}<div class="pro-filter-panel"><label><span>Search</span><input type="search" data-pro-filter-search placeholder="Filter by name or content"></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}}</a></li>{% endfor %}</ul>{% 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 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 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
@@ -1 +1 @@
<section class="pro-article-hero"><div class="pro-wrap pro-article-width"><span class="pro-eyebrow">LATEST NEWS / INSIGHTS</span><h1>{{page.title}}</h1><p>{{page.summary}}</p>{% if fields.get('publish_date') %}<time>{{fields.get('publish_date')}}</time>{% endif %}</div></section><article class="pro-wrap pro-article-content">{% if page.cover %}<img class="pro-article-cover" src="{{safe_image(page.cover)}}" alt="{{page.title}}">{% endif %}{{body_copy(content,page)}}<a class="pro-text-link" href="/news/">← Back to news</a></article>
<section class="pro-article-hero"><div class="pro-wrap pro-article-width"><span class="pro-eyebrow">LATEST NEWS / INSIGHTS</span><h1>{{page.title}}</h1><p>{{page.summary}}</p>{% if fields.get('publish_date') %}<time>{{fields.get('publish_date')}}</time>{% endif %}</div></section><article class="pro-wrap pro-article-content">{% if page.cover %}<img class="pro-article-cover" src="{{safe_image(page.cover)}}" alt="{{page.title}}">{% endif %}{{body_copy(content,page)}}<a class="pro-text-link" href="/news/">{{ icon('arrow-left') }} Back to news</a></article>
+1 -1
View File
@@ -1 +1 @@
<section class="pro-inner-hero"><div class="pro-wrap"><span class="pro-eyebrow">SELECTED WORK / PROJECT</span><h1>{{page.title}}</h1><p>{{page.summary}}</p></div></section><section class="pro-section"><div class="pro-wrap pro-case-detail">{% if page.cover %}<img src="{{safe_image(page.cover)}}" alt="{{page.title}}">{% endif %}<div class="pro-case-meta">{% if fields.get('location') %}<span>Location<br><strong>{{fields.get('location')}}</strong></span>{% endif %}{% if fields.get('industry') %}<span>Industry<br><strong>{{fields.get('industry')}}</strong></span>{% endif %}</div>{{body_copy(content,page)}}<a class="pro-text-link" href="/cases/">Browse more projects →</a></div></section>
<section class="pro-inner-hero"><div class="pro-wrap"><span class="pro-eyebrow">SELECTED WORK / PROJECT</span><h1>{{page.title}}</h1><p>{{page.summary}}</p></div></section><section class="pro-section"><div class="pro-wrap pro-case-detail">{% if page.cover %}<img src="{{safe_image(page.cover)}}" alt="{{page.title}}">{% endif %}<div class="pro-case-meta">{% if fields.get('location') %}<span>Location<br><strong>{{fields.get('location')}}</strong></span>{% endif %}{% if fields.get('industry') %}<span>Industry<br><strong>{{fields.get('industry')}}</strong></span>{% endif %}</div>{{body_copy(content,page)}}<a class="pro-text-link" href="/cases/">Browse more projects {{ icon('arrow-right') }}</a></div></section>
+1 -1
View File
@@ -1,2 +1,2 @@
<section class="pro-inner-hero pro-listing-hero"><div class="pro-wrap"><span class="pro-eyebrow">DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}</span><h1>{{page.title}}</h1><p>{{page.summary or 'Browse all published content in this section.'}}</p><div class="pro-breadcrumb"><a href="/">Home</a><span>/</span><strong>{{page.title}}</strong></div></div></section>
<section class="pro-section pro-list-section"><div class="pro-wrap">{{ filters() }}<div class="pro-filter-scope">{{grid(listing,'catalog')}}</div>{% if pagination and pagination.pages>1 %}<nav class="pro-pages-nav" aria-label="Catalogue pagination">{% if pagination.previous %}<a href="{{pagination.previous}}" rel="prev">← Previous</a>{% endif %}<span>Page {{pagination.page}} of {{pagination.pages}}</span>{% if pagination.next %}<a href="{{pagination.next}}" rel="next">Next →</a>{% endif %}</nav>{% endif %}</div></section>
<section class="pro-section pro-list-section"><div class="pro-wrap">{{ filters() }}<div class="pro-filter-scope">{{grid(listing,'catalog')}}</div>{% if pagination and pagination.pages>1 %}<nav class="pro-pages-nav" aria-label="Catalogue pagination">{% if pagination.previous %}<a href="{{pagination.previous}}" rel="prev">{{ icon('arrow-left') }} Previous</a>{% endif %}<span>Page {{pagination.page}} of {{pagination.pages}}</span>{% if pagination.next %}<a href="{{pagination.next}}" rel="next">Next {{ icon('arrow-right') }}</a>{% endif %}</nav>{% endif %}</div></section>
+2 -2
View File
@@ -1,6 +1,6 @@
<section class="pro-inner-hero pro-info-hero"><div class="pro-wrap"><span class="pro-eyebrow">GET IN TOUCH</span><h1>{{page.title}}</h1><p>{{page.summary}}</p></div></section>
<section class="pro-section pro-info-section"><div class="pro-wrap pro-info-grid"><div><span class="pro-eyebrow">CONNECT / CONTACT</span><h2>Let's start the conversation.</h2>{% if footer_email %}<a class="pro-contact-email" href="mailto:{{footer_email}}">{{footer_email}} ↗</a>{% else %}<p class="pro-muted">Set the official business email in the site admin console first.</p>{% endif %}</div><div class="pro-info-copy">{{body_copy(content,page)}}{% if page.cover %}<img class="pro-info-photo" src="{{safe_image(page.cover)}}" alt="{{page.title}}">{% endif %}</div></div></section>
<section class="pro-section pro-info-section"><div class="pro-wrap pro-info-grid"><div><span class="pro-eyebrow">CONNECT / CONTACT</span><h2>Let's start the conversation.</h2>{% if footer_email %}<a class="pro-contact-email" href="mailto:{{footer_email}}">{{footer_email}} {{ icon('arrow-right') }}</a>{% else %}<p class="pro-muted">Set the official business email in the site admin console first.</p>{% endif %}</div><div class="pro-info-copy">{{body_copy(content,page)}}{% if page.cover %}<img class="pro-info-photo" src="{{safe_image(page.cover)}}" alt="{{page.title}}">{% endif %}</div></div></section>
{% if form_endpoint %}
<section class="pro-section"><div class="pro-wrap pro-inquiry-wrap"><div><span class="pro-eyebrow">CONTACT FORM</span><h2>Send a business enquiry</h2><p>Submissions go directly to the external form service configured by the site administrator.</p></div>
<form class="pro-inquiry" action="{{form_endpoint}}" method="POST"><label>Name <input name="name" autocomplete="name" required maxlength="100"></label><label>Email <input name="email" type="email" autocomplete="email" required maxlength="180"></label><label>Message <textarea name="message" required minlength="6" maxlength="5000"></textarea></label><input name="website" tabindex="-1" autocomplete="off" class="pro-honeypot" aria-hidden="true"><button type="submit" class="sf-tp-btn">Submit enquiry ↗</button><p>Form submissions are handled by the external service; review its privacy and spam-filtering settings.</p></form></div></section>
<form class="pro-inquiry" action="{{form_endpoint}}" method="POST"><label>Name <input name="name" autocomplete="name" required maxlength="100"></label><label>Email <input name="email" type="email" autocomplete="email" required maxlength="180"></label><label>Message <textarea name="message" required minlength="6" maxlength="5000"></textarea></label><input name="website" tabindex="-1" autocomplete="off" class="pro-honeypot" aria-hidden="true"><button type="submit" class="sf-tp-btn">Submit enquiry {{ icon('arrow-right') }}</button><p>Form submissions are handled by the external service; review its privacy and spam-filtering settings.</p></form></div></section>
{% endif %}
+3 -3
View File
@@ -2,7 +2,7 @@
industrial on purpose: the 4-family distinct-markup tripwire in
test_theme_pro_v3 counts distinct pro-home-hero class strings, and the
Phase 3 visual port will give trade-pro its own hero variant. -->
<section class="pro-home-hero pro-industrial-hero"><div class="pro-wrap pro-home-grid"><div class="pro-hero-copy"><span class="pro-eyebrow">GLOBAL SOURCING · MADE RELIABLE / 001</span><h1>{{page.title if page.title not in (site.name,'首页','Home') else site.name}}<em> BUILT FOR GLOBAL TRADE.</em></h1><p>{{page.summary or site.tagline or 'Reliable products, transparent specifications and responsive export service.'}}</p><div class="pro-hero-actions"><a class="sf-tp-btn" href="/products/">Browse products ↗</a><a class="sf-tp-btn sf-tp-btn-outline" href="/contact/">Request a quote</a></div></div><div class="pro-hero-art">{% if page.cover %}<img src="{{safe_image(page.cover)}}" alt="{{page.title}}">{% else %}<div class="pro-technical"><div></div><div></div><div></div><strong>EXPORT / B2B</strong></div>{% endif %}</div></div></section>
{% if categories %}<section class="pro-section pro-category-band"><div class="pro-wrap">{{section_title('PRODUCT CATEGORIES','Shop by category','/products/')}}<div class="pro-category-tiles">{% for cat in categories[:6] %}<a href="{{cat.url}}"><span>0{{loop.index}}</span><strong>{{cat.name}}</strong><b>↗</b></a>{% endfor %}</div></div></section>{% endif %}
<section class="pro-home-hero pro-industrial-hero"><div class="pro-wrap pro-home-grid"><div class="pro-hero-copy"><span class="pro-eyebrow">GLOBAL SOURCING · MADE RELIABLE / 001</span><h1>{{page.title if page.title not in (site.name,'首页','Home') else site.name}}<em> BUILT FOR GLOBAL TRADE.</em></h1><p>{{page.summary or site.tagline or 'Reliable products, transparent specifications and responsive export service.'}}</p><div class="pro-hero-actions"><a class="sf-tp-btn" href="/products/">Browse products {{ icon('arrow-right') }}</a><a class="sf-tp-btn sf-tp-btn-outline" href="/contact/">Request a quote</a></div></div><div class="pro-hero-art">{% if page.cover %}<img src="{{safe_image(page.cover)}}" alt="{{page.title}}">{% else %}<div class="pro-technical"><div></div><div></div><div></div><strong>EXPORT / B2B</strong></div>{% endif %}</div></div></section>
{% if categories %}<section class="pro-section pro-category-band"><div class="pro-wrap">{{section_title('PRODUCT CATEGORIES','Shop by category','/products/')}}<div class="pro-category-tiles">{% for cat in categories[:6] %}<a href="{{cat.url}}"><span>0{{loop.index}}</span><strong>{{cat.name}}</strong><b>{{ icon('arrow-right') }}</b></a>{% endfor %}</div></div></section>{% endif %}
<section class="pro-section pro-featured"><div class="pro-wrap">{{section_title('TRADE CATALOGUE','Featured products','/products/')}}{{grid(showcase,'product')}}</div></section>
<section class="pro-section pro-statement"><div class="pro-wrap pro-statement-layout"><span class="pro-eyebrow">SERVICE &amp; SUPPORT</span><div><h2>From inquiry to delivery.</h2><p>{{site.tagline or 'Product specifications, quality control and export documentation in one place.'}}</p><a class="pro-text-link" href="/about/">About our company ↗</a></div></div></section>
<section class="pro-section pro-statement"><div class="pro-wrap pro-statement-layout"><span class="pro-eyebrow">SERVICE &amp; SUPPORT</span><div><h2>From inquiry to delivery.</h2><p>{{site.tagline or 'Product specifications, quality control and export documentation in one place.'}}</p><a class="pro-text-link" href="/about/">About our company {{ icon('arrow-right') }}</a></div></div></section>
+1 -1
View File
@@ -1,2 +1,2 @@
<section class="pro-inner-hero pro-listing-hero"><div class="pro-wrap"><span class="pro-eyebrow">DISCOVER / {{'CATEGORY' if kind=='category' else 'COLLECTION'}}</span><h1>{{page.title}}</h1><p>{{page.summary or 'Browse all published content in this section.'}}</p><div class="pro-breadcrumb"><a href="/">Home</a><span>/</span><strong>{{page.title}}</strong></div></div></section>
<section class="pro-section pro-list-section"><div class="pro-wrap">{{ filters() }}<div class="pro-filter-scope">{{grid(listing,'catalog')}}</div>{% if pagination and pagination.pages>1 %}<nav class="pro-pages-nav" aria-label="Catalogue pagination">{% if pagination.previous %}<a href="{{pagination.previous}}" rel="prev">← Previous</a>{% endif %}<span>Page {{pagination.page}} of {{pagination.pages}}</span>{% if pagination.next %}<a href="{{pagination.next}}" rel="next">Next →</a>{% endif %}</nav>{% endif %}</div></section>
<section class="pro-section pro-list-section"><div class="pro-wrap">{{ filters() }}<div class="pro-filter-scope">{{grid(listing,'catalog')}}</div>{% if pagination and pagination.pages>1 %}<nav class="pro-pages-nav" aria-label="Catalogue pagination">{% if pagination.previous %}<a href="{{pagination.previous}}" rel="prev">{{ icon('arrow-left') }} Previous</a>{% endif %}<span>Page {{pagination.page}} of {{pagination.pages}}</span>{% if pagination.next %}<a href="{{pagination.next}}" rel="next">Next {{ icon('arrow-right') }}</a>{% endif %}</nav>{% endif %}</div></section>
+1 -1
View File
@@ -1 +1 @@
<section class="pro-404 pro-wrap"><span>ERROR / 404</span><h1>404</h1><p>The page you are looking for does not exist or has been moved.</p><a class="sf-tp-btn" href="/">Back to home ↗</a></section>
<section class="pro-404 pro-wrap"><span>ERROR / 404</span><h1>404</h1><p>The page you are looking for does not exist or has been moved.</p><a class="sf-tp-btn" href="/">Back to home {{ icon('arrow-right') }}</a></section>
+1 -1
View File
@@ -1,3 +1,3 @@
<section class="pro-section pro-product-layout"><div class="pro-wrap"><div class="pro-breadcrumb"><a href="/">Home</a><span>/</span><a href="/products/">Products</a><span>/</span><strong>{{page.title}}</strong></div>
<div class="pro-product-hero"><div class="pro-product-gallery"><div class="pro-product-media">{{cover(page,'product')}}</div>{% if gallery|length>1 %}<div class="pro-gallery-thumbs" aria-label="Product image gallery">{% for photo in gallery %}<button type="button" data-gallery-image="{{photo}}" aria-label="View image {{loop.index}}"><img src="{{photo}}" alt="{{page.title}} · image {{loop.index}}" loading="lazy"></button>{% endfor %}</div>{% endif %}</div><div class="pro-product-summary"><span class="pro-eyebrow">PRODUCT DETAILS / {{fields.get('brand') or 'CATALOGUE'}}</span><h1>{{page.title}}</h1><p>{{page.summary or 'Detailed product information and specifications.'}}</p>{% if fields.get('brand') %}<p class="pro-product-brand">Brand · {{fields.get('brand')}}</p>{% endif %}<a class="sf-tp-btn" href="/contact/">Inquire about this product <span>↗</span></a></div></div>
<div class="pro-product-hero"><div class="pro-product-gallery"><div class="pro-product-media">{{cover(page,'product')}}</div>{% if gallery|length>1 %}<div class="pro-gallery-thumbs" aria-label="Product image gallery">{% for photo in gallery %}<button type="button" data-gallery-image="{{photo}}" aria-label="View image {{loop.index}}"><img src="{{photo}}" alt="{{page.title}} · image {{loop.index}}" loading="lazy"></button>{% endfor %}</div>{% endif %}</div><div class="pro-product-summary"><span class="pro-eyebrow">PRODUCT DETAILS / {{fields.get('brand') or 'CATALOGUE'}}</span><h1>{{page.title}}</h1><p>{{page.summary or 'Detailed product information and specifications.'}}</p>{% if fields.get('brand') %}<p class="pro-product-brand">Brand · {{fields.get('brand')}}</p>{% endif %}<a class="sf-tp-btn" href="/contact/">Inquire about this product <span>{{ icon('arrow-right') }}</span></a></div></div>
<div class="pro-detail-layout"><div><span class="pro-eyebrow">OVERVIEW</span><h2>Product overview</h2>{{body_copy(content,page)}}</div><div><span class="pro-eyebrow">SPECIFICATIONS</span><h2>Specifications</h2><div class="pro-specs">{% for row in specs %}<div><span>{{row.label}}</span><strong>{{row.value}}</strong></div>{% else %}<p class="pro-muted">Specifications have not been filled in yet.</p>{% endfor %}</div></div></div></div></section>
+3 -3
View File
@@ -1,5 +1,5 @@
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><base href="/"><title>{{ seo_title }}</title><meta name="description" content="{{ seo_description }}"><meta property="og:type" content="{{ 'article' if kind in ('article','case') else 'website' }}"><meta property="og:title" content="{{ seo_title }}"><meta property="og:description" content="{{ seo_description }}">{% if canonical %}<meta property="og:url" content="{{canonical}}">{% endif %}<meta name="theme-color" content="{{theme_color}}"><meta name="robots" content="{{ 'noindex,follow' if noindex else page.get('robots','index,follow') }}">{% if canonical and not noindex %}<link rel="canonical" href="{{ canonical }}">{% endif %}<link rel="stylesheet" href="assets/theme.css"><link rel="stylesheet" href="assets/blocks.css"><link rel="stylesheet" href="assets/factory.css"><link rel="stylesheet" href="assets/pro-base.css"><link rel="stylesheet" href="assets/pro.css"><link rel="stylesheet" href="assets/commercial.css">{% if theme_overrides %}<link rel="stylesheet" href="assets/theme-overrides.css">{% endif %}<link rel="stylesheet" href="assets/custom.css"></head><body class="sf-theme-trade-pro pro-trade-pro"><a class="pro-skip" href="#main-content">Skip to content</a>
{% from '_professional_components.html' import card,cover,grid,section_title,filters,footer_links,body_copy with context %}
{% from '_professional_components.html' import card,cover,grid,section_title,filters,footer_links,body_copy,icon with context %}
{# English fallback navigation (skeleton): the platform always fills nav with
Chinese defaults (首页/产品/品牌/新闻/案例/关于/联系) unless the site configured
its own navigation or published a site design. Detect "unconfigured" by
@@ -16,10 +16,10 @@
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 %}
{% 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">▾</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 ⌕</a>{% endif %}</nav><button class="pro-catalog-toggle" type="button" data-pro-catalog aria-controls="pro-mega-content" aria-expanded="false">Products ▾</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>↗</span></a>{% endif %}{% else %}<a href="/contact/" class="sf-tp-btn pro-header-cta">Request a Quote <span>↗</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}} ↗</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', 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>
{% 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 kind not in ('notfound','search') %}<section class="pro-end-cta"><div class="pro-wrap"><div><span class="pro-eyebrow">LET'S CONNECT</span><h2>Ready to source with confidence?</h2><p>{{site.tagline or 'Send your specifications and get a quotation within one business day.'}}</p></div><a class="sf-tp-btn" href="/contact/">Contact us <span>↗</span></a></div></section>{% endif %}
{% if kind not in ('notfound','search') %}<section class="pro-end-cta"><div class="pro-wrap"><div><span class="pro-eyebrow">LET'S CONNECT</span><h2>Ready to source with confidence?</h2><p>{{site.tagline or 'Send your specifications and get a quotation within one business day.'}}</p></div><a class="sf-tp-btn" href="/contact/">Contact us <span>{{ icon('arrow-right') }}</span></a></div></section>{% endif %}
<footer class="sf-site-foot pro-footer"><div class="pro-wrap"><div class="pro-footer-top"><div><span class="pro-eyebrow">TRADE PARTNERSHIP</span><h2>{{chrome.footer.title or site.name}}</h2><p>{{footer_text if (chrome_is_custom or footer_text) else 'Quality products, transparent specifications and dependable worldwide delivery.'}}</p></div><div class="pro-footer-links">{{footer_links(tp_nav if footer_nav else [])}}</div><div><span class="pro-eyebrow">BUSINESS ENQUIRY</span><p>{% if footer_email %}<a href="mailto:{{footer_email}}">{{footer_email}}</a>{% else %}Configure the business email in the admin console.{% endif %}</p></div></div><div class="pro-footer-bottom"><span>© {{year}} {{site.name}}</span><span>ENGINEERED FOR GLOBAL TRADE</span></div></div></footer>
<script defer src="assets/site-runtime.js"></script><script defer src="assets/pro.js"></script></body></html>
+10 -3
View File
@@ -1,12 +1,19 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><base href="/"><title>{{seo_title}}</title><meta name="description" content="{{seo_description}}"><meta name="robots" content="{{'noindex,follow' if mode=='notfound' else (page.robots or 'index,follow')}}">{% if canonical %}<link rel="canonical" href="{{canonical}}">{% endif %}<link rel="stylesheet" href="assets/theme.css"><link rel="stylesheet" href="assets/blocks.css"><link rel="stylesheet" href="assets/custom.css"></head><body>
{# site.html renders in environments where the trade-pro macro library cannot
be imported (theme-package previews load only the package dir), so its arrow
is a self-contained copy of the icon macro's arrow-right glyph. Same style
contract: 24×24 viewBox, currentColor stroke, decorative aria-hidden. #}
{% macro tp_arrow() -%}
<svg class="sf-tp-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
{%- endmacro %}
<div class="top"><div class="wrap"><span>ENGINEERING · MANUFACTURING · SUPPLY</span><span>GLOBAL INDUSTRIAL SOLUTIONS</span></div></div>
<header><div class="wrap header-inner"><a class="brand" href="./"><span class="brand-icon">▰</span><span>{{site.name}}<small>INDUSTRIAL SOLUTIONS</small></span></a><nav><a href="./">Home</a>{% for n in nav if n.route!='/' %}<a href="{{link(n.route)}}">{{n.title}}</a>{% endfor %}{% if entries|selectattr('type','equalto','product')|list %}<a href="product/">Products</a>{% endif %}{% if entries|selectattr('type','equalto','post')|list %}<a href="post/">News</a>{% endif %}</nav><a class="request" href="mailto:{{site.email or "info@example.com"}}">REQUEST A QUOTE ↗</a></div></header>
{% if blueprint_mode %}{% elif mode=='home' %}<section class="hero"><div class="wrap hero-grid"><div class="hero-text"><span class="kicker"><i></i> STRONGER CONNECTIONS. SMARTER SOLUTIONS.</span><h1>{{page.title if page.title!=site.name else 'BUILT TO LAST.'}}</h1><p>{{page.summary or site.tagline or 'Industrial excellence, quality products and dependable worldwide delivery.'}}</p><a class="button" href="product/">EXPLORE PRODUCTS <b>↗</b></a><div class="hero-tags"><span>01 / MATERIALS</span><span>02 / ENGINEERING</span><span>03 / SERVICE</span></div></div><div class="hero-art">{% if page.cover %}<img src="{{asset(page.cover)}}" alt="{{page.title}}">{% else %}<div class="ring ring-1"></div><div class="ring ring-2"></div><div class="ring ring-3"></div><span>INDUSTRY<br>IN MOTION</span>{% endif %}</div></div></section>
<header><div class="wrap header-inner"><a class="brand" href="./"><span class="brand-icon">▰</span><span>{{site.name}}<small>INDUSTRIAL SOLUTIONS</small></span></a><nav><a href="./">Home</a>{% for n in nav if n.route!='/' %}<a href="{{link(n.route)}}">{{n.title}}</a>{% endfor %}{% if entries|selectattr('type','equalto','product')|list %}<a href="product/">Products</a>{% endif %}{% if entries|selectattr('type','equalto','post')|list %}<a href="post/">News</a>{% endif %}</nav><a class="request" href="mailto:{{site.email or "info@example.com"}}">REQUEST A QUOTE {{ tp_arrow() }}</a></div></header>
{% if blueprint_mode %}{% elif mode=='home' %}<section class="hero"><div class="wrap hero-grid"><div class="hero-text"><span class="kicker"><i></i> STRONGER CONNECTIONS. SMARTER SOLUTIONS.</span><h1>{{page.title if page.title!=site.name else 'BUILT TO LAST.'}}</h1><p>{{page.summary or site.tagline or 'Industrial excellence, quality products and dependable worldwide delivery.'}}</p><a class="button" href="product/">EXPLORE PRODUCTS <b>{{ tp_arrow() }}</b></a><div class="hero-tags"><span>01 / MATERIALS</span><span>02 / ENGINEERING</span><span>03 / SERVICE</span></div></div><div class="hero-art">{% if page.cover %}<img src="{{asset(page.cover)}}" alt="{{page.title}}">{% else %}<div class="ring ring-1"></div><div class="ring ring-2"></div><div class="ring ring-3"></div><span>INDUSTRY<br>IN MOTION</span>{% endif %}</div></div></section>
{% elif mode=='notfound' %}<section class="subhero"><div class="wrap"><span class="kicker">ERROR</span><h1>404 — Page not found</h1></div></section>
{% else %}<section class="subhero"><div class="wrap"><span class="kicker">{{type_names.get(page.type,'COMPANY')|upper}} / {{'PRODUCT CATALOG' if mode=='listing' else 'OUR EXPERTISE'}}</span><h1>{{page.title}}</h1><p>{{page.summary}}</p></div></section>{% endif %}
{% if blueprint_mode %}<main class="sf-canvas">{{blocks}}</main>{% else %}<main class="wrap main">{% if mode=='home' %}<div class="metrics"><div><strong>01</strong><span>PRODUCT ENGINEERING</span></div><div><strong>02</strong><span>GLOBAL SOURCING</span></div><div><strong>03</strong><span>QUALITY ASSURANCE</span></div></div>{% endif %}
{% if mode in ['home','detail'] and (content or blocks) %}<article class="rich-content">{{blocks}}{{content}}</article>{% endif %}
{% if mode=='detail' and page.cover %}<figure class="detail-image"><img src="{{asset(page.cover)}}" alt="{{page.title}}"></figure>{% endif %}
{% set cards=listing_items if mode=='listing' else entries|selectattr('type','equalto','product')|list if mode=='home' else [] %}
{% if cards %}<div class="titlebar"><div><span class="kicker">OUR PORTFOLIO</span><h2>{{'Product Catalogue' if mode=='listing' else 'Explore Our Products'}}</h2></div>{% if mode=='home' %}<a href="product/">ALL PRODUCTS ↗</a>{% endif %}</div><div class="product-grid">{% for item in cards[:12] %}<a href="{{link(item.route)}}" class="product"><div class="product-img">{% if item.cover %}<img src="{{asset(item.cover)}}" alt="{{item.title}}">{% else %}<div class="technical-graphic"><span>+</span><span>+</span><span>+</span><span>+</span></div>{% endif %}</div><div class="product-info"><span>INDUSTRIAL / {{type_names.get(item.type,'PRODUCT')|upper}}</span><h3>{{item.title}}</h3><p>{{item.summary|truncate(100)}}</p><div class="arrow">DISCOVER DETAILS <b>→</b></div></div></a>{% endfor %}</div>{% endif %}</main>
{% if cards %}<div class="titlebar"><div><span class="kicker">OUR PORTFOLIO</span><h2>{{'Product Catalogue' if mode=='listing' else 'Explore Our Products'}}</h2></div>{% if mode=='home' %}<a href="product/">ALL PRODUCTS {{ tp_arrow() }}</a>{% endif %}</div><div class="product-grid">{% for item in cards[:12] %}<a href="{{link(item.route)}}" class="product"><div class="product-img">{% if item.cover %}<img src="{{asset(item.cover)}}" alt="{{item.title}}">{% else %}<div class="technical-graphic"><span>+</span><span>+</span><span>+</span><span>+</span></div>{% endif %}</div><div class="product-info"><span>INDUSTRIAL / {{type_names.get(item.type,'PRODUCT')|upper}}</span><h3>{{item.title}}</h3><p>{{item.summary|truncate(100)}}</p><div class="arrow">DISCOVER DETAILS <b>{{ tp_arrow() }}</b></div></div></a>{% endfor %}</div>{% endif %}</main>
{% endif %}<footer><div class="wrap"><div class="footer-cols"><div><h2>{{site.name}}</h2><p>{{site.tagline}}</p></div><div><span>INDUSTRIAL SOLUTIONS</span><p>Quality materials. Dependable service.<br>Worldwide supply.</p></div></div><div class="bottom"><span>© {{site.name}}</span><span>DESIGNED TO DELIVER. BUILT WITH SITEFORGE.</span></div></div></footer></body></html>
+41
View File
@@ -585,6 +585,47 @@ body.sf-theme-trade-pro {
margin: 0;
}
/* ── 图标(Task 9 SVG 宏库:内联线条图标的对齐与间距)── */
/* 基线对齐:inline 上下文垂直居中,flex 上下文不被压缩换行 */
.sf-tp-icon {
display: inline-block;
vertical-align: middle;
flex-shrink: 0;
}
/* 按钮内紧跟文字的图标留出呼吸空间(flex gap 之外的显式间距,
兼容图标包在 <span> 里或直接暴露两种写法) */
.sf-tp-btn svg {
margin-inline-start: var(--sf-tp-space-1);
}
/* 非按钮内联上下文:图标贴着文字时补一点间距 */
.pro-mega a .sf-tp-icon,
.pro-contact-email .sf-tp-icon,
.pro-footer-links a .sf-tp-icon {
margin-inline-start: var(--sf-tp-space-1);
}
/* filters 宏搜索框:放大镜占位在输入框内左侧(不抢焦点) */
.sf-theme-trade-pro .pro-filter-search-box {
position: relative;
display: block;
margin: 0; /* 抵消 pro-base 的 label>span 说明文字样式 */
}
.sf-theme-trade-pro .pro-filter-search-box .sf-tp-icon {
position: absolute;
inset-inline-start: var(--sf-tp-space-3);
inset-block-end: var(--sf-tp-space-3);
pointer-events: none;
color: var(--sf-tp-color-text-muted);
}
.sf-theme-trade-pro .pro-filter-search-box input {
padding-inline-start: calc(var(--sf-tp-icon-size-md, 20px) + var(--sf-tp-space-6));
}
/* ── Phase 2 桥接:骨架期 .pro-* 强调样式改由 cms 令牌驱动(Task 10 布局移植后由组件样式接管)── */
.pro-utility {