Themes & UI 主题与界面
SXSEditor's UI is driven by a token-based theme system. Every color, spacing, radius, font size, motion duration, and shadow is a CSS custom property (--token) injected on :root. Themes are plain JSON objects that override these tokens. The system lives in src/themes/ and runs in the renderer process.
SXSEditor 的界面由基于 token 的主题系统驱动。每种颜色、间距、圆角、字号、动效时长与阴影都是一个注入到 :root 上的 CSS 自定义属性(--token)。主题是覆盖这些 token 的纯 JSON 对象。该系统位于 src/themes/,运行在渲染进程中。
Three source files form the core: tokenCatalog.js (token metadata + defaults), themeManager.js (registry, activation, history), and themeValidator.js (validation rules). canvasTheme.js bridges tokens to Canvas 2D rendering.
三个源文件构成核心:tokenCatalog.js(token 元数据 + 默认值)、themeManager.js(注册表、激活、历史)、themeValidator.js(校验规则)。canvasTheme.js 将 token 桥接到 Canvas 2D 渲染。
Theme System Overview 主题系统总览
┌─ tokenCatalog.js ─────────────────────────────────────────┐
│ TOKEN_CATALOG: { '--token-name': { layer, group, type, │
│ default, label } } │
│ buildDefaultTokens(): flat { name: value } from catalog │
│ Layers: 'global' | 'alias' | 'component' │
│ Types: 'color' | 'size' | 'motion' | 'shadow' | 'string' │
└────────────────────────────────────────────────────────────┘
│ defaults
▼
┌─ themeManager.js (renderer) ───────────────────────────────┐
│ registry: Map(id -> { theme, source }) │
│ activate(id): flattenTheme(extends chain) → injectTokens │
│ mergeOverrides(patch): transient patch (not persisted) │
│ pushHistory / undo / redo: 20-step undo stack │
│ export(id) → JSON string; import(json) → theme object │
│ Events: 'theme-changed', 'theme-list-changed', │
│ 'theme-overwritten', 'theme-imported' │
└────────────────────────────────────────────────────────────┘
│ setProperty('--token', value) on documentElement
▼
┌─ DOM :root ────────────────────────────────────────────────┐
│ All CSS in src/renderer.css consumes var(--token) │
│ Canvas 2D reads via canvasTheme.getCanvasColors() │
└────────────────────────────────────────────────────────────┘
Token Catalog Token 目录
TOKEN_CATALOG in src/themes/tokenCatalog.js is the single source of truth for every token. Each entry has a layer, group, type, default value, and a Chinese label. Tokens are organized into three layers:
src/themes/tokenCatalog.js 中的 TOKEN_CATALOG 是每个 token 的唯一来源。每个条目包含 layer、group、type、default 值与中文 label。token 分为三层:
| Layer层级 | Purpose用途 | Example Groups示例分组 |
|---|---|---|
global |
Raw design tokens (scales, primitives)原始设计 token(刻度、原语) | color-blue, color-gray, color-ink, color-red, color-green, color-amber, color-purple, color-base, space, radius, font, motion, shadowcolor-blue、color-gray、color-ink、color-red、color-green、color-amber、color-purple、color-base、space、radius、font、motion、shadow |
alias |
Semantic tokens referencing globals引用 global 的语义 token | bg, fg, accent, border, status (success/warning/danger/info)bg、fg、accent、border、status(success/warning/danger/info) |
component |
Component-scoped tokens (button, input, panel, tooltip, scrollbar, selection)组件级 token(button、input、panel、tooltip、scrollbar、selection) | button-primary-bg, input-border, panel-bg, tooltip-bgbutton-primary-bg、input-border、panel-bg、tooltip-bg |
Token types drive validation: color (hex/rgb/hsl), size (number + unit), motion (duration), shadow (free-form CSS), string (any non-empty). buildDefaultTokens() produces a flat { name: value } map from the catalog defaults, used as the base when no theme is loaded.
Token 类型驱动校验:color(hex/rgb/hsl)、size(数值 + 单位)、motion(时长)、shadow(自由 CSS)、string(任意非空)。buildDefaultTokens() 从目录默认值生成扁平的 { name: value } 映射,作为未加载主题时的基础。
Built-in Themes 内置主题
Four themes ship with SXSEditor, defined as JSON files in src/themes/builtins/ and aggregated by builtins/index.js into the BUILTIN_THEMES array:
SXSEditor 内置 4 个主题,以 JSON 文件形式定义在 src/themes/builtins/,由 builtins/index.js 聚合为 BUILTIN_THEMES 数组:
| IDID | Name名称 | Dark?暗色? | Description描述 |
|---|---|---|---|
dark-aurora |
Aurora DarkAurora Dark | Yes (default)是(默认) | Classic blue-purple dark style经典蓝紫暗色风格 |
light-paper |
Paper LightPaper Light | No否 | Bright white background, eye-friendly明亮白底,护眼清爽 |
midnight-amber |
Midnight AmberMidnight Amber | Yes是 | Dark with warm amber accent深色琥珀强调,温暖复古 |
acg |
ACG LightACG Light | No否 | Parchment-style light, teal accent, ACG aesthetic羊皮纸风格亮色,青绿强调,ACG 美学 |
Built-in themes are imported as ES module exports so webpack bundles them. BUILTIN_THEME_IDS is the array of their ids. They are registered on startup via themeManager.registerBuiltins(BUILTIN_THEMES).
内置主题以 ES 模块导出导入,以便 webpack 打包。BUILTIN_THEME_IDS 是其 id 数组。启动时通过 themeManager.registerBuiltins(BUILTIN_THEMES) 注册。
.theme.json Format .theme.json 格式
A theme file is a JSON object with this shape (example abbreviated from dark-aurora.theme.json):
主题文件是如下结构的 JSON 对象(示例节选自 dark-aurora.theme.json):
{
"id": "dark-aurora",
"name": "Aurora Dark",
"version": "1.0.0",
"author": "SXSEditor",
"isDark": true,
"description": "默认暗色主题,复刻 SXSEditor 经典蓝紫风格",
"tags": ["builtin", "dark", "default"],
"extends": null,
"tokens": {
"--color-blue-500": "#4a7de0",
"--bg-app": "#14141f",
"--bg-panel": "#1a1a2a",
"--fg-primary": "#e0e0f0",
"--accent": "#5b8def",
"--button-primary-bg": "var(--bg-button-primary)",
...
}
}
Fields:
字段说明:
id— kebab-case identifier (validated byID_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/). Must be unique in the registry.id— kebab-case 标识符(由ID_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/校验)。在注册表中必须唯一。name— human-readable display name.name— 人类可读的显示名。version— semver string (defaults to1.0.0).version— semver 字符串(默认1.0.0)。isDark— boolean. If omitted,computeIsDarkderives it from--bg-appperceived luminance.isDark— 布尔值。若省略,computeIsDark由--bg-app的感知亮度推导。author,description,tags— optional metadata.author、description、tags— 可选元数据。extends— id of a parent theme to inherit from (depth ≤ 3, no cycles). Parent tokens are applied first, then this theme'stokensoverride them.extends— 要继承的父主题 id(深度 ≤ 3,无环)。父 token 先应用,再被本主题的tokens覆盖。tokens— map of--token-name→ value. Token names must matchTOKEN_NAME_RE = /^--[a-z0-9][a-z0-9-]*$/.tokens—--token-name→ 值 的映射。token 名必须匹配TOKEN_NAME_RE = /^--[a-z0-9][a-z0-9-]*$/。
Creating a Custom Theme 创建自定义主题
- Create the JSON file — place it at
src/themes/builtins/my-theme.theme.json(to bundle it) or load it at runtime viathemeManager.import(jsonString).创建 JSON 文件— 放在src/themes/builtins/my-theme.theme.json(打包)或运行时通过themeManager.import(jsonString)加载。 - Pick a parent — set
extendsto"dark-aurora"or"light-paper"so you only override the tokens you care about.选择父主题— 将extends设为"dark-aurora"或"light-paper",只覆盖关心的 token。 - Override tokens — only list the
--tokenkeys you want to change. Unknown tokens are accepted as free CSS values but won't appear in the editor UI.覆盖 token— 只列出要改的--token键。未知 token 会作为自由 CSS 值接受,但不会出现在编辑器 UI 中。 - Register it — for bundled themes, add an import to
builtins/index.jsand append toBUILTIN_THEMES. For runtime themes, callthemeManager.register(themeObj).注册— 打包主题:在builtins/index.js加导入并追加到BUILTIN_THEMES。运行时主题:调用themeManager.register(themeObj)。 - Validate — run
validate(theme, { getThemeById })fromthemeValidator.js. It returns{ ok, errors, warnings }and does not throw.校验— 调用themeValidator.js的validate(theme, { getThemeById })。返回{ ok, errors, warnings },不会抛异常。 - Activate — call
themeManager.activate('my-theme'). Tokens are injected on:rootand the'theme-changed'event fires.激活— 调用themeManager.activate('my-theme')。token 注入到:root并触发'theme-changed'事件。
For live preview in the theme editor, use mergeOverrides(patch) to apply a transient token patch without persisting. pushHistory() snapshots the current overrides into a 20-step undo stack (HISTORY_LIMIT=20); undo() / redo() traverse it. Call clearOverrides() to discard the patch and return to the base theme.
在主题编辑器中实时预览,用 mergeOverrides(patch) 应用瞬态 token 补丁而不持久化。pushHistory() 将当前覆盖快照到 20 步撤销栈(HISTORY_LIMIT=20);undo() / redo() 遍历该栈。调用 clearOverrides() 丢弃补丁并回到基础主题。
Theme Manager API 主题管理器 API
The themeManager singleton (renderer process) exposes:
themeManager 单例(渲染进程)暴露:
| Method方法 | Description说明 |
|---|---|
register(themeObj) |
Add a theme to the registry将主题加入注册表 |
registerBuiltins(arr) |
Register an array of built-in themes注册内置主题数组 |
unregister(id) |
Remove a theme (built-ins cannot be removed)移除主题(内置不可移除) |
list() |
Return [{id,name,isDark,author,version,source}]返回 [{id,name,isDark,author,version,source}] |
activate(id, { scope }) |
Resolve extends chain, inject tokens, fire event解析 extends 链,注入 token,触发事件 |
current() / currentTokens() |
Get active id / resolved token map获取活动 id / 已解析 token 映射 |
export(id) / import(json) |
Serialize to JSON string / parse and register序列化为 JSON 字符串 / 解析并注册 |
mergeOverrides(patch) / clearOverrides() |
Transient patch (not persisted to localStorage)瞬态补丁(不持久化到 localStorage) |
pushHistory() / undo() / redo() |
20-step undo stack for editor history20 步撤销栈,用于编辑器历史 |
on(eventName, handler) |
Subscribe; returns unsubscribe function订阅;返回取消订阅函数 |
Events: 'theme-changed', 'theme-list-changed', 'theme-overwritten', 'theme-imported'. Persisted overrides use the localStorage key sxseditor-theme-overrides.
事件:'theme-changed'、'theme-list-changed'、'theme-overwritten'、'theme-imported'。持久化的覆盖使用 localStorage 键 sxseditor-theme-overrides。
Extends Resolution extends 解析
flattenTheme(theme) walks the extends chain (parent first, child overrides last) and merges all tokens maps into one flat object. The chain depth is capped at MAX_EXTENDS_DEPTH=3; cycles are rejected. If a parent id is not in the registry, an error is thrown.
flattenTheme(theme) 沿 extends 链(父在前、子覆盖在后)将所有 tokens 映射合并为一个扁平对象。链深度上限 MAX_EXTENDS_DEPTH=3;环会被拒绝。若父 id 不在注册表中,则抛错。
// Example: a custom theme extending dark-aurora
{
"id": "my-theme",
"name": "My Theme",
"extends": "dark-aurora",
"tokens": {
"--accent": "#a855f7", // override accent to purple
"--bg-app": "#1a0a2a" // override app background
// all other tokens inherited from dark-aurora
}
}
Canvas Theming Canvas 主题化
Canvas 2D rendering cannot read CSS var() directly. src/themes/canvasTheme.js bridges this: getCanvasColors() reads the relevant custom properties from :root (inline style first, then getComputedStyle) and returns a plain object like { bgApp, bgPanel, fgPrimary, accent, success, danger, ... }. Results are cached until invalidateCanvasThemeCache() is called.
Canvas 2D 渲染无法直接读取 CSS var()。src/themes/canvasTheme.js 桥接此问题:getCanvasColors() 从 :root 读取相关自定义属性(先内联样式,再 getComputedStyle),返回 { bgApp, bgPanel, fgPrimary, accent, success, danger, ... } 等普通对象。结果缓存直到调用 invalidateCanvasThemeCache()。
import { getCanvasColors, invalidateCanvasThemeCache } from '../themes/canvasTheme.js';
import themeManager from '../themes/themeManager.js';
themeManager.on('theme-changed', () => invalidateCanvasThemeCache());
function drawPianoRoll(ctx) {
const c = getCanvasColors();
ctx.fillStyle = c.bgApp;
ctx.fillRect(0, 0, width, height);
ctx.strokeStyle = c.accent;
// ...
}
Theme Validation 主题校验
themeValidator.js validates theme objects without throwing — it returns { ok, errors, warnings }. Pass getThemeById to enable extends validation (parent existence, depth ≤ 3, no cycles). Validation rules:
themeValidator.js 校验主题对象但不抛异常——返回 { ok, errors, warnings }。传入 getThemeById 可启用 extends 校验(父存在、深度 ≤ 3、无环)。校验规则:
| Field字段 | Rule规则 |
|---|---|
id |
Non-empty, kebab-case (ID_RE), no leading/trailing dash非空、kebab-case(ID_RE)、首尾无连字符 |
name / version / author / description |
Optional; if present must be a string (version defaults to 1.0.0)可选;若存在必须为字符串(version 默认 1.0.0) |
isDark |
Optional boolean; else derived from --bg-app可选布尔;否则由 --bg-app 推导 |
tokens keys |
Must match TOKEN_NAME_RE = /^--[a-z0-9][a-z0-9-]*$/必须匹配 TOKEN_NAME_RE = /^--[a-z0-9][a-z0-9-]*$/ |
tokens color values |
COLOR_VALUE_RE: #hex, rgb()/rgba(), hsl()/hsla(), transparent, currentColor, inheritCOLOR_VALUE_RE:#hex、rgb()/rgba()、hsl()/hsla()、transparent、currentColor、inherit |
tokens size/motion values |
SIZE_VALUE_RE: number + px|rem|em|%|vh|vw|s|ms, or 0SIZE_VALUE_RE:数值 + px|rem|em|%|vh|vw|s|ms,或 0 |
extends |
Parent must exist, depth ≤ MAX_EXTENDS_DEPTH=3, no cycles父必须存在、深度 ≤ MAX_EXTENDS_DEPTH=3、无环 |
If validation fails, ThemeValidationError can be constructed from the errors array (it carries error.errors) — but the validator itself never throws, so callers decide how to surface problems.
若校验失败,可由 errors 数组构造 ThemeValidationError(它带有 error.errors)——但校验器本身从不抛异常,由调用方决定如何呈现问题。
When a theme is activated, injectTokens sets every token via root.style.setProperty(k, v). Switching themes does not clear previously-injected properties that are absent from the new theme — call clearInjectedTokens(tokenNames) for the previous theme's tokens first, or design themes to override the same superset of tokens. Built-in themes all define the full token set, so switching among them is safe.
激活主题时,injectTokens 通过 root.style.setProperty(k, v) 设置每个 token。切换主题不会清除新主题中缺失的先前注入属性——需先对前一主题的 token 调用 clearInjectedTokens(tokenNames),或将主题设计为覆盖相同的 token 超集。内置主题都定义了完整 token 集,因此它们之间切换是安全的。