## 发布说明 **v1.13.0 Theme V3 主题系统版本** ### 关联 Issue - 关联 #99(「能不能直接在网页加个密码?」):本版本实现其中**主题苹果风**部分——新增 Apple 内置主题(macOS 风格浅色)并落地 Theme V3 主题系统;**网页访问密码**部分未包含在本版本,issue 保持开启、留待后续版本。 ### 版本亮点 - **内置主题 5 → 7 款**:新增 CRT Amber(CRT 磷光深色:径向暗角背景 + 扫描线/闪烁/辉光/噪点全开 + 方形 mono 造型)与 Glass(蓝紫浅色玻璃态:mesh 粉彩漂移背景 + 强表面模糊 + 1.4× 圆角 + 分段控件);Glacier 替换为 Apple(macOS 风格浅色,完整 ANSI 16 色无障碍变体) - **Theme V3 声明式令牌系统**(schemaVersion 3):`background`(solid/linear/radial/mesh 四类型、停靠点白名单、读性遮罩下限 0.25/0.35、drift 动画)、`effects`(scanline/flicker/glow/noise 强度钳制 0-1、data-fx-* 门控、Cyberpunk 默认视觉不变)、`typography`(字号/圆角缩放)、`appearance.blur` 独立表面模糊档位 - **零 DOM 操作、前后端同源校验**:背景/效果全部 CSS 变量 + 属性驱动;V2 主题数据自动升级、glacier baseTheme 优雅降级;导入/导出双向兼容 - **在线主题编辑器支持 V3**:背景层/效果/版式三组面板 + blur 档位,导出 schemaVersion 3 - **全量文档对齐**:README/tests README/AGENTS/CHANGELOG 与当前实现同步 ### 提交列表 | 提交 | 说明 | | ---- | ---- | | 268a892 | feat: 内置主题 Glacier 替换为 Apple 浅色主题 | | 86e3ddf | feat: Theme V3 主题系统落地——背景层、效果注册表与版式缩放 | | 9092e2a | release: 发布 v1.13.0(版本号 + CHANGELOG) | | c098a7c | docs: 全量文档与当前实现对齐(测试清单 + 措辞修正) | ### 验证结果 - ✅ typecheck(worker + frontend + tests 三工程) - ✅ vitest 54 文件 / 649 用例 - ✅ build:frontend 可复现内联构建(html.ts 再生成) - ✅ 主题 e2e 6/6(含 CRT/Glass 切换、drift 属性、强模糊与 body::before 背景计算值断言) --------- Co-authored-by: TanXin <tanxin@example.com>
119 lines
4.7 KiB
JavaScript
119 lines
4.7 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const rootDir = path.resolve(__dirname, '..');
|
|
const themeSourcePath = path.join(rootDir, 'frontend', 'src', 'theme.ts');
|
|
const editorPath = path.join(rootDir, 'docs', 'theme-editor', 'index.html');
|
|
|
|
const themeSource = fs.readFileSync(themeSourcePath, 'utf8');
|
|
let editorHtml = fs.readFileSync(editorPath, 'utf8');
|
|
|
|
const terminalThemes = readObjectLiteral(themeSource, 'export const THEMES =');
|
|
const uiThemes = readObjectLiteral(themeSource, 'export const UI_THEMES:');
|
|
const stylePresets = readObjectLiteral(themeSource, 'export const UI_STYLE_PRESETS:');
|
|
const builtInAppearances = readObjectLiteral(themeSource, 'export const BUILT_IN_APPEARANCE:');
|
|
const colorSchemes = readObjectLiteral(themeSource, 'const COLOR_SCHEMES:');
|
|
const builtInBackgrounds = readObjectLiteral(themeSource, 'export const BUILT_IN_BACKGROUND:');
|
|
const builtInEffects = readObjectLiteral(themeSource, 'export const BUILT_IN_EFFECTS:');
|
|
const builtInTypography = readObjectLiteral(themeSource, 'export const BUILT_IN_TYPOGRAPHY:');
|
|
const labels = {
|
|
'standard-dark': 'Standard Dark',
|
|
'standard-light': 'Standard Light',
|
|
cyberpunk: 'Cyberpunk',
|
|
apple: 'Apple',
|
|
gruvbox: 'Gruvbox',
|
|
crt: 'CRT Amber',
|
|
glass: 'Glass',
|
|
};
|
|
|
|
const presets = Object.fromEntries(
|
|
Object.keys(terminalThemes).map((themeName) => {
|
|
const appearance = builtInAppearances[themeName] || {};
|
|
const style = appearance.style || 'cyberpunk';
|
|
const stylePreset = stylePresets[style] || stylePresets.cyberpunk;
|
|
return [
|
|
themeName,
|
|
{
|
|
name: labels[themeName] || themeName,
|
|
colorScheme: colorSchemes[themeName],
|
|
appearance: {
|
|
...stylePreset,
|
|
...appearance,
|
|
components: {
|
|
...stylePreset.components,
|
|
...(appearance.components || {}),
|
|
},
|
|
},
|
|
...(builtInBackgrounds[themeName] ? { background: builtInBackgrounds[themeName] } : {}),
|
|
...(Object.keys(builtInEffects[themeName] || {}).length
|
|
? { effects: builtInEffects[themeName] }
|
|
: {}),
|
|
...(builtInTypography[themeName] ? { typography: builtInTypography[themeName] } : {}),
|
|
terminal: terminalThemes[themeName],
|
|
ui: uiThemes[themeName],
|
|
},
|
|
];
|
|
}),
|
|
);
|
|
|
|
const generatedBlock = `const PRESETS = /* THEME_PRESETS_START */ ${JSON.stringify(presets, null, 2)} /* THEME_PRESETS_END */;`;
|
|
const markerStart = editorHtml.indexOf('/* THEME_PRESETS_START */');
|
|
const markerEnd = editorHtml.indexOf('/* THEME_PRESETS_END */');
|
|
|
|
if (markerStart >= 0 && markerEnd > markerStart) {
|
|
const declarationStart = editorHtml.lastIndexOf('const PRESETS =', markerStart);
|
|
const declarationEnd = editorHtml.indexOf(';', markerEnd);
|
|
editorHtml = `${editorHtml.slice(0, declarationStart)}${generatedBlock}${editorHtml.slice(declarationEnd + 1)}`;
|
|
} else {
|
|
const declarationStart = editorHtml.indexOf('const PRESETS =');
|
|
if (declarationStart < 0) throw new Error('Could not find PRESETS declaration in theme editor');
|
|
const objectStart = editorHtml.indexOf('{', declarationStart);
|
|
const objectEnd = findMatchingBrace(editorHtml, objectStart);
|
|
const declarationEnd = editorHtml.indexOf(';', objectEnd);
|
|
editorHtml = `${editorHtml.slice(0, declarationStart)}${generatedBlock}${editorHtml.slice(declarationEnd + 1)}`;
|
|
}
|
|
|
|
fs.writeFileSync(editorPath, editorHtml, 'utf8');
|
|
console.log('Theme editor presets synchronized from frontend/src/theme.ts');
|
|
|
|
function readObjectLiteral(source, marker) {
|
|
const markerIndex = source.indexOf(marker);
|
|
if (markerIndex < 0) throw new Error(`Could not find ${marker}`);
|
|
const objectStart = source.indexOf('{', markerIndex + marker.length);
|
|
const objectEnd = findMatchingBrace(source, objectStart);
|
|
const literal = source.slice(objectStart, objectEnd + 1);
|
|
// 仓库源码属于受信任输入;这里只执行抽取出的纯数据对象,供静态 Pages 使用。
|
|
// pi-lens-ignore: no-global-eval-js
|
|
return Function(`"use strict"; return (${literal});`)();
|
|
}
|
|
|
|
function findMatchingBrace(source, startIndex) {
|
|
let depth = 0;
|
|
let quote = null;
|
|
let escaped = false;
|
|
|
|
for (let index = startIndex; index < source.length; index++) {
|
|
const character = source[index];
|
|
if (quote) {
|
|
if (escaped) {
|
|
escaped = false;
|
|
} else if (character === '\\') {
|
|
escaped = true;
|
|
} else if (character === quote) {
|
|
quote = null;
|
|
}
|
|
continue;
|
|
}
|
|
if (character === '"' || character === "'" || character === '`') {
|
|
quote = character;
|
|
continue;
|
|
}
|
|
if (character === '{') depth++;
|
|
if (character === '}') {
|
|
depth--;
|
|
if (depth === 0) return index;
|
|
}
|
|
}
|
|
throw new Error('Unterminated object literal');
|
|
}
|