feat: per-block font size/color controls — title_size, text_size, text_color, accent_color, button_color
- blueprints.py: validate + render 5 new style properties as CSS vars - editor.js: slider controls for title/text size, color pickers for text/accent/button colors with reset buttons - blocks.css: CSS variable consumption rules (--sf-title-size, etc.) Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+9
-2
@@ -148,12 +148,15 @@ def validate_blocks(blocks):
|
||||
if not isinstance(style,dict):raise ValueError('样式必须为对象')
|
||||
normalized['style']={}
|
||||
for key,val in style.items():
|
||||
if key in ('background','text_color'):
|
||||
if key in ('background','text_color','accent_color','button_color'):
|
||||
if not isinstance(val,str) or not re.fullmatch(r'#[0-9a-fA-F]{6}',val):raise ValueError('颜色必须为六位十六进制')
|
||||
normalized['style'][key]=val
|
||||
elif key in ('padding','tablet_padding','mobile_padding'):
|
||||
if type(val) is not int or not 0<=val<=120:raise ValueError('区块间距必须是0-120整数')
|
||||
normalized['style'][key]=val
|
||||
elif key in ('title_size','text_size'):
|
||||
if type(val) is not int or not 10<=val<=80:raise ValueError('字号必须是10-80整数')
|
||||
normalized['style'][key]=val
|
||||
elif key in ('hide_mobile','hide_tablet'):
|
||||
if type(val) is not bool:raise ValueError('设备显隐必须是布尔值')
|
||||
normalized['style'][key]=val
|
||||
@@ -267,12 +270,16 @@ def render_blocks(blocks, site, page, entries):
|
||||
typ=block['type']; resolve=lambda key,default='':resolve_text(block.get(key,default),site,page)
|
||||
title=escape(resolve('title')); subtitle=escape(resolve('subtitle'));desc=escape(resolve('description'))
|
||||
raw_style=block.get('style') or {}; s=[]
|
||||
for key,var in [('background','--sf-bg'),('text_color','--sf-fg')]:
|
||||
for key,var in [('background','--sf-bg'),('text_color','--sf-fg'),('accent_color','--sf-accent-c'),('button_color','--sf-btn-c')]:
|
||||
val=str(raw_style.get(key,'')).strip()
|
||||
if re.fullmatch(r'#[a-fA-F0-9]{6}',val):s.append(f'{var}:{val}')
|
||||
if 'padding' in raw_style:
|
||||
try:pad=max(0,min(120,int(raw_style['padding'])));s.append(f'--sf-pad:{pad}px')
|
||||
except (ValueError,TypeError):pass
|
||||
for key,var,lo,hi in [('title_size','--sf-title-size',10,80),('text_size','--sf-text-size',10,24)]:
|
||||
if key in raw_style:
|
||||
try:size=max(lo,min(hi,int(raw_style[key])));s.append(f'{var}:{size}px')
|
||||
except (ValueError,TypeError):pass
|
||||
style=' style="'+escape(';'.join(s),quote=True)+'"' if s else ''
|
||||
start=f'<section class="sf-section sf-{typ}" data-block="{escape(block["id"])}"{style}><div class="sf-container">'
|
||||
end='</div></section>'
|
||||
|
||||
@@ -89,3 +89,11 @@
|
||||
.sf-app-tile h3{font-size:20px;margin:0 0 6px}
|
||||
.sf-app-tile p{font-size:14px;line-height:1.65;opacity:.75;margin:0}
|
||||
@media(max-width:540px){.sf-applications-band{grid-template-columns:minmax(0,1fr)}}
|
||||
|
||||
/* ── 区块级字号/颜色自定义(CSS 变量消费)── */
|
||||
.sf-section h1,.sf-section h2{font-size:var(--sf-title-size,clamp(38px,5vw,76px))!important}
|
||||
.sf-section h3{font-size:var(--sf-title-size,clamp(22px,2vw,28px))!important}
|
||||
.sf-section p,.sf-section li,.sf-section .sf-prose{font-size:var(--sf-text-size,16px)!important}
|
||||
.sf-section .sf-kicker,.sf-section .sf-feature>span,.sf-section .sf-item-copy small{color:var(--sf-accent-c,var(--accent,#3569d3))!important}
|
||||
.sf-section .sf-button{background:var(--sf-btn-c,var(--accent,#315ee2))!important}
|
||||
.sf-section .sf-link,.sf-section .sf-section-head>a{color:var(--sf-accent-c,var(--accent,#3569d3))!important}
|
||||
|
||||
@@ -407,7 +407,7 @@ const Studio={
|
||||
out+='<button class="ui-btn ghost" onclick="Studio.addColumn()">+ 添加列</button>';
|
||||
}
|
||||
out+=`<div class="subheading">响应式样式</div><div class="prop"><label>平板上下间距(px)</label><input type="number" min="0" max="120" value="${html(b.style?.tablet_padding??50)}" onchange="Studio.styleField('tablet_padding',Number(this.value))"></div><div class="prop"><label>手机上下间距(px)</label><input type="number" min="0" max="120" value="${html(b.style?.mobile_padding??28)}" onchange="Studio.styleField('mobile_padding',Number(this.value))"></div><div class="prop"><label><input type="checkbox" ${b.style?.hide_mobile?'checked':''} onchange="Studio.styleField('hide_mobile',this.checked)"> 手机上隐藏区块</label></div>`;
|
||||
out+=`</div><div data-inspector-section="style"><div class="subheading">快速设置外观</div><p class="prop-tip">不必先调色值:选择样式后会立即更新预览,也可以撤销。</p><div class="sf-style-presets"><button type="button" onclick="Studio.applyStylePreset('clean')">清爽白底</button><button type="button" onclick="Studio.applyStylePreset('soft')">柔和浅底</button><button type="button" onclick="Studio.applyStylePreset('feature')">重点深底</button></div><div class="subheading">自定义外观</div><div class="prop"><label>背景颜色</label><input type="color" value="${html(b.style?.background||'#ffffff')}" onchange="Studio.styleField('background',this.value)"></div><div class="prop"><label>上下间距(px)</label><input type="number" min="0" max="120" value="${html(b.style?.padding??82)}" onchange="Studio.styleField('padding',Number(this.value))"></div><div class="prop"><label>从图库选择图片</label><button type="button" class="ui-btn ghost" onclick="Studio.chooseMedia()">打开本站素材库</button></div><div class="prop"><label>上传图片</label><input type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/avif" onchange="Studio.uploadImage(this.files[0])"></div></div>`;
|
||||
out+=`</div><div data-inspector-section="style"><div class="subheading">快速设置外观</div><p class="prop-tip">不必先调色值:选择样式后会立即更新预览,也可以撤销。</p><div class="sf-style-presets"><button type="button" onclick="Studio.applyStylePreset('clean')">清爽白底</button><button type="button" onclick="Studio.applyStylePreset('soft')">柔和浅底</button><button type="button" onclick="Studio.applyStylePreset('feature')">重点深底</button></div><div class="subheading">字体与颜色</div><div class="prop"><label>标题字号 (px) <b id="titleSizeVal">${b.style?.title_size||'默认'}</b></label><input type="range" min="16" max="80" value="${b.style?.title_size||48}" oninput="document.getElementById('titleSizeVal').textContent=this.value+'px'" onchange="Studio.styleField('title_size',Number(this.value))" style="width:100%"></div><div class="prop"><label>正文字号 (px) <b id="textSizeVal">${b.style?.text_size||'默认'}</b></label><input type="range" min="10" max="24" value="${b.style?.text_size||16}" oninput="document.getElementById('textSizeVal').textContent=this.value+'px'" onchange="Studio.styleField('text_size',Number(this.value))" style="width:100%"></div><div class="prop"><label>文字颜色</label><div style="display:flex;gap:8px;align-items:center"><input type="color" value="${b.style?.text_color||'#253247'}" onchange="Studio.styleField('text_color',this.value)"><button type="button" class="ui-btn ghost" style="font-size:11px;padding:4px 8px" onclick="Studio.styleField('text_color','');Studio.refreshPreview();Studio.draw()" title="恢复默认">重置</button></div></div><div class="prop"><label>强调色(链接/标签)</label><div style="display:flex;gap:8px;align-items:center"><input type="color" value="${b.style?.accent_color||'#3569d3'}" onchange="Studio.styleField('accent_color',this.value)"><button type="button" class="ui-btn ghost" style="font-size:11px;padding:4px 8px" onclick="Studio.styleField('accent_color','');Studio.refreshPreview();Studio.draw()">重置</button></div></div><div class="prop"><label>按钮颜色</label><div style="display:flex;gap:8px;align-items:center"><input type="color" value="${b.style?.button_color||'#0874df'}" onchange="Studio.styleField('button_color',this.value)"><button type="button" class="ui-btn ghost" style="font-size:11px;padding:4px 8px" onclick="Studio.styleField('button_color','');Studio.refreshPreview();Studio.draw()">重置</button></div></div><div class="subheading">背景与间距</div><div class="prop"><label>背景颜色</label><div style="display:flex;gap:8px;align-items:center"><input type="color" value="${b.style?.background||'#ffffff'}" onchange="Studio.styleField('background',this.value)"><button type="button" class="ui-btn ghost" style="font-size:11px;padding:4px 8px" onclick="Studio.styleField('background','');Studio.refreshPreview();Studio.draw()">重置</button></div></div><div class="prop"><label>上下间距(px)</label><input type="range" min="0" max="120" value="${b.style?.padding??82}" onchange="Studio.styleField('padding',Number(this.value))" style="width:100%"></div><div class="prop"><label>从图库选择图片</label><button type="button" class="ui-btn ghost" onclick="Studio.chooseMedia()">打开本站素材库</button></div><div class="prop"><label>上传图片</label><input type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/avif" onchange="Studio.uploadImage(this.files[0])"></div></div>`;
|
||||
const sharing=selectedChildId?editorTools.findChildInRoot(blocks,b.id,selectedChildId)?.child?.shared_ref:b.shared_ref;
|
||||
if(sharing)out+=`<div class="sf-share-info"><strong>已关联公共组件 · v${html(sharing.version)}</strong><p>此区块将来可以按需同步新版。直接修改后会被视为本地定制。</p><button type="button" class="ui-btn ghost" onclick="Studio.detachShared()">解除关联,保留当前内容</button></div>`;
|
||||
out+='<div style="display:flex;gap:8px;margin-bottom:22px"><button class="small-btn" onclick="Studio.duplicate('+selected+')">复制区块</button><button class="small-btn red-btn" onclick="Studio.remove('+selected+')">删除区块</button></div>';
|
||||
|
||||
Reference in New Issue
Block a user