Files
cms-SiteForge/app/responsive.py
T
浅念 8cea7dee20 chore: baseline V8.10.0-dev.1 with v8_workflows type-mapping fix and trade-pro spec
- SiteForge Enterprise V8.10.0-dev.1 upstream source (210 tests)
- Fix: v8_workflows._mapped_input now reads both 类型/分类 columns so WXR
  imports keep post/product types instead of collapsing to page
- Docs: trade-pro theme port spec (R3) and implementation plan

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 16:49:25 +08:00

53 lines
2.7 KiB
Python

"""Validated per-device static CSS for pages and nested layout grids.
Uses exact IDs from validate_blocks, never user supplied CSS selectors/expressions.
Desktop structure is preserved; tablet and mobile may have distinct spans/order.
"""
import re,json,hashlib
from .blueprints import validate_blocks
_ID=re.compile(r'[a-zA-Z0-9_-]{4,50}\Z')
def editor_css(blocks,prefix=""):
tablet=[];mobile=[]
def walk(items):
for b in items:
uid=b['id']
if not _ID.fullmatch(uid):continue
s=b.get('style') or {}
tp=s.get('tablet_padding',50);mp=s.get('mobile_padding',28)
tp=max(0,min(120,int(tp)));mp=max(0,min(120,int(mp)))
ident=prefix+f'.sf-section[data-block="{uid}"]'
tablet.append(f'{ident}{{--sf-pad:{tp}px}}')
mobile.append(f'{ident}{{--sf-pad:{mp}px}}')
if s.get('hide_tablet') is True:tablet.append(f'{ident}{{display:none!important}}')
if s.get('hide_mobile') is True:mobile.append(f'{ident}{{display:none!important}}')
if b['type']=='layout':
selector=prefix+f'.sf-layout-grid[data-layout="{uid}"]'
tablet.append(f'{selector}{{grid-template-columns:repeat({b["tablet_columns"]},minmax(0,1fr))!important}}')
mobile.append(f'{selector}{{grid-template-columns:repeat({b["mobile_columns"]},minmax(0,1fr))!important}}')
for ci,col in enumerate(b['columns'],1):
target=selector+f'>.sf-layout-column[data-col="{ci}"]'
for dev,rules in [('tablet',tablet),('mobile',mobile)]:
rules.append(f'{target}{{order:{col[dev+"_order"]};grid-column:span {col[dev+"_span"]}}}')
if col['hide_'+dev]:rules.append(f'{target}{{display:none!important}}')
walk(col['blocks'])
walk(validate_blocks(blocks))
# Breakpoints are intentionally exclusive: phone has tablet rules plus
# phone overrides. Phone controls are appended after tablet controls.
return '@media(max-width:820px){'+''.join(tablet)+'}@media(max-width:540px){'+''.join(mobile)+'}'
def route_key(route):
return hashlib.sha256(str(route).encode()).hexdigest()[:16]
def site_css(entries):
styles=[]
for entry in entries:
try:meta=json.loads(entry.get('meta_json') or '{}')
except (TypeError,ValueError):continue
if isinstance(meta.get('blocks'),list) and meta['blocks']:
styles.append(editor_css(meta['blocks'],prefix=f'body[data-siteforge-route="{route_key(entry.get("route","/"))}"] '))
# Identical generated IDs may occur on different pages. Styles are scoped
# to block IDs and remain deterministic; validate each page independently.
return ''.join(styles)