Files
blog/public/admin/preview-style.js
T
wait4xandGLM-5.3 5c0e6ed900 chore(spec): 归档 blog-5n——八任务收敛(后台深度优化全家桶)
Spec: 2026-11-05_blog-5n-admin-deep-optimization

Co-authored-by: GLM-5.3
2026-11-05 20:13:09 +08:00

126 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 后台预览一致性(5l T1 / 5n T7):把站点排版效果注入 Sveltia 预览窗格。
*
* 现状问题:markdown 里的排版类(font- 字体、c-/ink- 颜色)在预览里是素文本——
* 类名随 DOM 保留(typography-editor 的 toPreview 输出带 class),但预览
* 文档没有站点 CSS,颜色字体全部不可见。
*
* 方案(官方口子,勿改成 iframe hack——见 spec 5l §2.5):
* 1. 基础富文本排版 + 全部排版类 CSS 从 window.TYPOGRAPHY_CLASSES
* **运行时生成**(清单单一事实源:typography-classes.js 改一处,
* 选择面板与预览自动同步),raw 模式注入;
* 2. 自托管艺术字体挂切片 CSS(public/fonts/<family>/fonts.css,
* unicode-range 按需取片——预览里只拉用到的字所落在的切片,不整包)。
*
* 暗色(5n T7):所有色值走 CSS 自定义属性,变量缺省值 = 亮态(与改造前同一批
* 字面量,无暗色命中时逐字节等价),末尾一段 @media (prefers-color-scheme: dark)
* 统一换档——后台切暗色时预览不再用亮态色值配暗底。
*
* serif/kai/kuaile 的 CSS 经 Vite ?url 打包成哈希名,后台引不到稳定 URL,
* 预览走字体栈里的系统回退(Kaiti SC/Songti)——近似可见,文档已注明。
* 加载顺序:须在 typography-classes.js 之后、CMS init 之前(index.html 固定序)。
*/
(function () {
const CLASSES = window.TYPOGRAPHY_CLASSES;
if (!CLASSES || !window.CMS || !window.CMS.registerPreviewStyle) return;
/**
* 基础排版的暗态值(亮态值不在这里登记,直接作各条 var() 的回落值)。
* 出处:src/styles/tokens.css `[data-theme="dark"]` 段,勿凭观感改。
*/
const BASE_DARK = {
'pv-accent': '#8ea2ff', // --accent
'pv-line': '#262b33', // --line(站点暗色分隔线就是这个档)
'pv-quote-ink': '#8b8f98', // --ink-3
'pv-code-bg': '#1f242b', // --bg-soft
};
/**
* 颜色类暗态值,键 = typography-classes.js 里的 cls。同样取自暗色 tokens 段:
* 站点语义色板暗态是独立取值(不是亮态加亮),硬套亮态在 #191d23 底上不达 AA。
*/
const CLASS_DARK = {
'ink-1': '#f5f5f7', // --ink-1
'ink-2': '#c9ccd4', // --ink-2
'ink-3': '#8b8f98', // --ink-3
// 站点 CSS 里 ink-muted 映射 --ink-4,而暗态 --ink-4 (#5c6066) 是装饰档,
// 暗底上近乎不可见;亮态 swatch 本就取的是 --ink-faint 的值,暗态随之一致
'ink-muted': '#84888f', // --ink-faint
'ink-warm': '#d6cbbd', // --ink-warm
'ink-cool': '#b9c6d6', // --ink-cool
'ink-faint': '#84888f', // --ink-faint
'ink-accent': '#8ea2ff', // --accent
'ink-accent-soft': '#a8b7f7', // --accent-soft-text
'c-blue': '#7fb0ff',
'c-blue-soft': '#3381ff',
'c-teal': '#6fd6c8',
'c-teal-soft': '#2a9384',
'c-green': '#7ddf99',
'c-green-soft': '#269a47',
'c-yellow': '#ffd166',
'c-yellow-soft': '#af7a00',
'c-orange': '#ffb163',
'c-orange-soft': '#d16800',
'c-red': '#ff8b8b',
'c-red-soft': '#ff2828',
'c-pink': '#ff9ecb',
'c-pink-soft': '#ff0d7e',
'c-purple': '#c4a6ff',
'c-purple-soft': '#9a67ff',
};
/**
* 未登记暗态值的类(清单后续新增)用 color-mix 提亮近似——宁可偏亮,
* 也不要暗底上跑亮态深色(那是这次要修的问题本身)。
*/
const approxDark = (swatch) => `color-mix(in oklab, ${swatch} 82%, #fff)`;
/** 预览基调:近似站点 .rich 的通用排版(目标是类效果可见,非像素级还原) */
const baseCss = [
'p{margin:.6em 0;line-height:1.75}',
'h1,h2,h3,h4{font-weight:600;line-height:1.4;margin:1.2em 0 .5em}',
'h2{font-size:1.3em}h3{font-size:1.15em}',
'ul,ol{padding-left:1.5em;margin:.6em 0}li{margin:.25em 0}',
'a{color:var(--pv-accent,#3454d1);text-decoration:underline}',
'blockquote{margin:.8em 0;padding:.2em 1em;border-left:3px solid var(--pv-line,#e6e7eb);color:var(--pv-quote-ink,#6b6e78)}',
"code{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:.92em;background:var(--pv-code-bg,#f2f3f5);padding:.1em .35em;border-radius:4px}",
'img{max-width:100%}',
'hr{border:none;border-top:1px solid var(--pv-line,#e6e7eb);margin:1.5em 0}',
'strong{font-weight:700}',
].join('');
/** 排版类规则:清单驱动(swatch/family 亮态值作 var() 回落 = 面板所见) */
const rules = [];
const darkDecls = [];
for (const f of CLASSES.font || []) {
if (f.family) rules.push(`.${f.cls}{font-family:${f.family}}`);
}
for (const c of CLASSES.color || []) {
if (!c.swatch) continue;
const name = `--pv-${c.cls}`;
rules.push(`.${c.cls}{color:var(${name},${c.swatch})}`);
darkDecls.push(`${name}:${CLASS_DARK[c.cls] || approxDark(c.swatch)}`);
}
for (const [name, value] of Object.entries(BASE_DARK)) darkDecls.push(`--${name}:${value}`);
/**
* 为什么用媒体查询而不是监听「后台切主题」:Sveltia 的 registerPreviewStyle 是
* 一次性注入(无更新/移除接口),registerEventListener 只支持 pre/postSave 与
* pre/postPublish 这类保存事件,没有任何主题切换回调;后台主题默认跟随系统,
* 预览文档的 prefers-color-scheme 与后台同源,浏览器自己换档即可,零重注入。
* 代价:用户在后台显式选 light 而系统是 dark 时预览偏暗——已知取舍,比监听
* Sveltia 内部 DOM(非公开契约)稳妥。
*/
const darkCss = darkDecls.length ? `@media (prefers-color-scheme: dark){:root{${darkDecls.join(';')}}}` : '';
window.CMS.registerPreviewStyle(baseCss + rules.join('') + darkCss, { raw: true });
// 字体切片(URL 模式):预览文档自己的 @font-face;去重后逐个挂
const seen = new Set();
for (const f of CLASSES.font || []) {
if (!f.css || seen.has(f.css)) continue;
seen.add(f.css);
window.CMS.registerPreviewStyle(f.css);
}
})();