docs: plan blue content components

This commit is contained in:
Peter Li 2026-07-17 07:16:01 +08:00
parent 82497d7f27
commit 751d088e7c
2 changed files with 954 additions and 11 deletions

View file

@ -0,0 +1,942 @@
# Aera 蓝色基调与正文组件实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development推荐或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 将 Aera 的默认绿色基调迁移到以 `#1677FF` 为核心的冷中性蓝色体系,并实现语义 Callout、折角引用、Monokai 多行代码块和更清晰的行内代码。
**架构:** 继续使用模块化 SCSS 和单一根 `theme.css`。官方变量承担颜色与排版;只有 Callout 图标定位、折角和块级 Monokai 无法由变量表达时才使用经主题政策登记的低特异性选择器。所有行为先写失败测试,再做最小实现,并在专用 Vault 中用 Obsidian CLI 验证真实 DOM。
**技术栈:** Dart Sass、PostCSS、Node.js 24 内置测试、Obsidian 1.12.7 CLI、ImageMagick。
---
## 执行纪律
- 在现有 `feature/aera-v1` 分支执行,不创建 tag不 push不发布。
- 每个任务只允许一个实现代理;实现后依次做规格审查和质量审查。
- 发现问题时由原实现代理按 TDD 修复,再由原审查代理复审。
- 不设置 `--font-text-size`、`--file-line-width` 或 `--font-interface-theme`
- 不使用 `!important`、`:has()`、远程 URL、运行时 `@import`、JavaScript 或图片装饰。
- 每次 `npm run build` 后必须确认 `theme.css` 是唯一生成产物。
## 文件结构
| 文件 | 职责 |
| --- | --- |
| `src/foundations/_colors.scss` | 冷中性浅深色灰阶、默认蓝色 accent、组件颜色令牌 |
| `src/editor/_code.scss` | 行内代码变量和块级 Monokai 作用域 |
| `src/components/_callouts.scss` | Callout 变量、语义表面、原生图标重排 |
| `src/editor/_quotes.scss` | Reading、Live Preview、Source Mode 引用结构 |
| `tests/foundations.test.mjs` | 颜色令牌、用户设置边界和对比度断言 |
| `scripts/contrast.mjs` | 默认颜色、行内代码和 Monokai 关键文本对比度检查 |
| `tests/editor-blocks.test.mjs` | 代码块与引用变量、选择器和声明断言 |
| `tests/components.test.mjs` | Callout 和全局允许选择器集合 |
| `tests/helpers/css.mjs` | 主题选择器与 Callout 变量政策 |
| `fixtures/Theme Playground.md` | 真实 Obsidian 视觉 QA 内容 |
| `tests/link-vault.test.mjs` | fixture 语义覆盖与安全复制回归 |
| `theme.css` | Sass 编译产物 |
| `screenshot.png` | 512×288 社区主题预览 |
## 已确认的 Obsidian 1.12.7 DOM
- Live Preview 容器:`.markdown-source-view.mod-cm6.is-live-preview`。
- Source Mode 容器:`.markdown-source-view.mod-cm6:not(.is-live-preview)`。
- 一级引用行:`.HyperMD-quote.HyperMD-quote-1.cm-line`。
- fenced code 行:`.HyperMD-codeblock`,首尾分别带 `.HyperMD-codeblock-begin``.HyperMD-codeblock-end`
- Reading code`.markdown-rendered pre:not(.frontmatter)`;必须排除隐藏的 YAML frontmatter `pre`
- Callout 结构:`.callout > .callout-title > .callout-icon`,内容为 `.callout-content`,折叠箭头为 `.callout-fold`
### 任务 1蓝色与冷中性色基础
**文件:**
- 修改:`tests/foundations.test.mjs`
- 修改:`scripts/contrast.mjs`
- 修改:`src/foundations/_colors.scss`
- 生成:`theme.css`
- [ ] **步骤 1把颜色基础测试改为新令牌**
`tests/foundations.test.mjs` 中将 `expectedColors` 替换为以下值;保留已有的用户字号、字体和行宽边界测试:
```js
const expectedColors = {
".theme-light": {
"--color-base-00": "#f8fafc",
"--color-base-05": "#f5f7fa",
"--color-base-10": "#f1f4f8",
"--color-base-20": "#e9eef5",
"--color-base-25": "#dde3eb",
"--color-base-30": "#cdd5df",
"--color-base-35": "#bec8d4",
"--color-base-40": "#a9b4c2",
"--color-base-50": "#8793a3",
"--color-base-60": "#647184",
"--color-base-70": "#475467",
"--color-base-100": "#202936",
"--background-primary": "var(--color-base-00)",
"--background-secondary": "var(--color-base-10)",
"--text-normal": "var(--color-base-100)",
"--text-muted": "var(--color-base-60)",
"--text-faint": "var(--color-base-50)",
"--text-highlight-bg": "rgba(245, 198, 50, 0.22)",
"--accent-h": "215",
"--accent-s": "100%",
"--accent-l": "54.3%",
"--interactive-accent": "hsl(var(--accent-h), var(--accent-s), var(--accent-l))",
"--text-accent": "hsl(var(--accent-h), var(--accent-s), calc(var(--accent-l) - 10%))",
"--text-accent-hover": "hsl(var(--accent-h), var(--accent-s), calc(var(--accent-l) - 14%))",
"--aera-inline-code-background": "#d9dee5",
"--aera-inline-code-color": "#566273",
"--aera-callout-background-opacity": "0.08",
"--aera-quote-background": "#f1f3f6",
"--aera-quote-fold": "#d9dee5",
},
".theme-dark": {
"--color-base-00": "#17191c",
"--color-base-05": "#1b1e22",
"--color-base-10": "#20242a",
"--color-base-20": "#282d34",
"--color-base-25": "#30363e",
"--color-base-30": "#39414b",
"--color-base-35": "#46505c",
"--color-base-40": "#596575",
"--color-base-50": "#748092",
"--color-base-60": "#9aa5b4",
"--color-base-70": "#c1c7d0",
"--color-base-100": "#e7ebf0",
"--background-primary": "var(--color-base-00)",
"--background-secondary": "var(--color-base-10)",
"--text-normal": "var(--color-base-100)",
"--text-muted": "var(--color-base-60)",
"--text-faint": "var(--color-base-50)",
"--text-highlight-bg": "rgba(245, 198, 50, 0.26)",
"--accent-h": "213",
"--accent-s": "100%",
"--accent-l": "62.5%",
"--interactive-accent": "hsl(var(--accent-h), var(--accent-s), var(--accent-l))",
"--text-accent": "hsl(var(--accent-h), var(--accent-s), var(--accent-l))",
"--text-accent-hover": "hsl(var(--accent-h), var(--accent-s), calc(var(--accent-l) + 5%))",
"--aera-inline-code-background": "#2a2f36",
"--aera-inline-code-color": "#b8c0cc",
"--aera-callout-background-opacity": "0.12",
"--aera-quote-background": "#22262c",
"--aera-quote-fold": "#3a414a",
},
};
```
- [ ] **步骤 2扩展对比度测试**
`tests/foundations.test.mjs` 中把核心对比度断言改为 11 组:
```js
assert.deepEqual(CORE_CONTRAST_PAIRS, [
["light text", "#202936", "#f8fafc"],
["light muted", "#647184", "#f8fafc"],
["light text accent", "#005ee2", "#f8fafc"],
["dark text", "#e7ebf0", "#17191c"],
["dark muted", "#9aa5b4", "#17191c"],
["dark text accent", "#4096ff", "#17191c"],
["light inline code", "#566273", "#d9dee5"],
["dark inline code", "#b8c0cc", "#2a2f36"],
["monokai normal", "#f8f8f2", "#272822"],
["monokai comment", "#929388", "#272822"],
["monokai keyword", "#ff4b8b", "#272822"],
]);
const expectedRatios = [
"14.02", "4.74", "5.43", "14.71", "7.06", "5.89",
"4.58", "7.35", "13.94", "4.78", "4.69",
];
```
将 CLI 测试的名称和行数断言同步为 11 组。
- [ ] **步骤 3运行测试确认红灯**
运行:
```bash
node --test tests/foundations.test.mjs
```
预期FAIL。失败必须来自旧绿色令牌和旧 6 组对比度数组,而不是语法错误。
- [ ] **步骤 4实现新的颜色基础**
`scripts/contrast.mjs` 中用步骤 2 的 11 组值替换 `CORE_CONTRAST_PAIRS`
`src/foundations/_colors.scss` 中按步骤 1 的完整值替换 `.theme-light``.theme-dark`。颜色声明必须继续保留在两个主题作用域内,不移动到 `body`
- [ ] **步骤 5构建并验证绿灯**
```bash
npm run build
node --test tests/foundations.test.mjs
node scripts/contrast.mjs
npm run check
git diff --check
```
预期:所有测试通过;对比度 CLI 输出 11 行 `PASS``theme.css` 只包含颜色更新。
- [ ] **步骤 6提交颜色基础**
```bash
git add src/foundations/_colors.scss scripts/contrast.mjs tests/foundations.test.mjs theme.css
git commit -m "feat: shift Aera to a blue color system"
```
### 任务 2行内代码与 Monokai 多行代码块
**文件:**
- 修改:`tests/helpers/css.mjs`
- 修改:`tests/components.test.mjs`
- 修改:`tests/editor-blocks.test.mjs`
- 修改:`src/editor/_code.scss`
- 生成:`theme.css`
- [ ] **步骤 1登记代码块选择器**
`tests/helpers/css.mjs``allowedThemeSelectors``tests/components.test.mjs``allowedSelectors` 中移除旧 `.markdown-rendered pre`,并添加:
```js
":where(.markdown-rendered pre:not(.frontmatter))",
":where(.markdown-source-view.mod-cm6 .HyperMD-codeblock)",
":where(.markdown-source-view.mod-cm6 .HyperMD-codeblock-begin)",
":where(.markdown-source-view.mod-cm6 .HyperMD-codeblock-end)",
```
两个集合必须完全相同,不允许通配 `.markdown-source-view`
- [ ] **步骤 2编写失败的代码样式测试**
`tests/editor-blocks.test.mjs` 中将 body 代码变量断言改为行内代码,并为 Reading/Editor 两个块作用域添加相同的 Monokai 断言:
```js
test("keeps inline code in the reading color system", () => {
assertDeclarations({
"--code-background": "var(--aera-inline-code-background)",
"--code-normal": "var(--aera-inline-code-color)",
"--code-white-space": "pre",
"--code-size": "0.88em",
});
});
const monokai = {
"--code-background": "#272822",
"--code-normal": "#f8f8f2",
"--code-comment": "#929388",
"--code-function": "#a6e22e",
"--code-important": "#fd971f",
"--code-keyword": "#ff4b8b",
"--code-operator": "#f8f8f2",
"--code-property": "#66d9ef",
"--code-punctuation": "#f8f8f2",
"--code-string": "#e6db74",
"--code-tag": "#ff4b8b",
"--code-value": "#ae81ff",
};
for (const selector of [
":where(.markdown-rendered pre:not(.frontmatter))",
":where(.markdown-source-view.mod-cm6 .HyperMD-codeblock)",
]) {
test(`${selector} defines scoped Monokai colors`, () => {
const declarations = declarationsFor(css, selector);
for (const [property, value] of Object.entries(monokai)) {
assert.equal(declarations.get(property), value, `${selector} ${property}`);
}
assert.equal(declarations.get("background-color"), "#272822");
assert.equal(declarations.get("color"), "#f8f8f2");
});
}
```
再断言 begin/end 的圆角:首行 `6px 6px 0 0`,末行 `0 0 6px 6px`
- [ ] **步骤 3运行测试确认红灯**
```bash
node --test tests/editor-blocks.test.mjs tests/components.test.mjs
```
预期FAIL。旧 CSS 缺少新选择器body 仍使用正文背景Monokai 声明不存在。
- [ ] **步骤 4实现块级 Monokai**
`src/editor/_code.scss` 改为:
```scss
@mixin monokai-block {
--code-background: #272822;
--code-normal: #f8f8f2;
--code-comment: #929388;
--code-function: #a6e22e;
--code-important: #fd971f;
--code-keyword: #ff4b8b;
--code-operator: #f8f8f2;
--code-property: #66d9ef;
--code-punctuation: #f8f8f2;
--code-string: #e6db74;
--code-tag: #ff4b8b;
--code-value: #ae81ff;
background-color: #272822;
color: #f8f8f2;
border: 1px solid #3e3d32;
}
body {
--code-background: var(--aera-inline-code-background);
--code-white-space: pre;
--code-size: 0.88em;
--code-normal: var(--aera-inline-code-color);
}
:where(.markdown-rendered pre:not(.frontmatter)) {
@include monokai-block;
border-radius: 6px;
}
:where(.markdown-source-view.mod-cm6 .HyperMD-codeblock) {
@include monokai-block;
border-block-width: 0;
border-radius: 0;
}
:where(.markdown-source-view.mod-cm6 .HyperMD-codeblock-begin) {
border-block-start-width: 1px;
border-radius: 6px 6px 0 0;
}
:where(.markdown-source-view.mod-cm6 .HyperMD-codeblock-end) {
border-block-end-width: 1px;
border-radius: 0 0 6px 6px;
}
```
如果 Sass 输出重复边框声明,只允许在绿灯后的重构步骤压缩;不能先改变行为。
- [ ] **步骤 5构建并验证绿灯**
```bash
npm run build
node --test tests/editor-blocks.test.mjs tests/components.test.mjs
npm run check
git diff --check
```
预期:全部通过;`theme.css` 不再包含旧 `.markdown-rendered pre` 规则。
- [ ] **步骤 6提交代码样式**
```bash
git add src/editor/_code.scss tests/editor-blocks.test.mjs tests/components.test.mjs tests/helpers/css.mjs theme.css
git commit -m "feat: add scoped Monokai code blocks"
```
### 任务 3无边框语义 Callout 与右侧图标
**文件:**
- 修改:`tests/helpers/css.mjs`
- 修改:`tests/components.test.mjs`
- 修改:`src/components/_callouts.scss`
- 生成:`theme.css`
- [ ] **步骤 1登记精确 Callout 选择器**
在两个允许选择器集合中添加以下完整字符串:
```js
":where(.callout)",
":where(.callout-title)",
":where(.callout-content)",
":where(.callout-icon)",
":where(.callout-icon svg)",
":where(.callout-fold)",
":where(.callout.is-collapsed .callout-icon)",
```
- [ ] **步骤 2编写失败的 Callout 测试**
将 body Callout 变量期望改为:
```js
{
"--callout-padding": "var(--size-4-3) var(--size-4-4)",
"--callout-title-padding": "0",
"--callout-content-padding": "var(--size-4-2) 0 0",
"--callout-border-width": "0px",
"--callout-border-opacity": "0",
"--callout-radius": "7px",
"--callout-blend-mode": "normal",
"--callout-title-size": "var(--font-small)",
"--callout-title-weight": "600",
"--callout-content-background": "transparent",
}
```
添加直接声明断言:
```js
test("positions the native callout icon as a quiet watermark", () => {
const callout = declarationsFor(css, ":where(.callout)");
assert.equal(callout.get("position"), "relative");
assert.equal(callout.get("overflow"), "hidden");
assert.equal(
callout.get("background-color"),
"rgba(var(--callout-color), var(--aera-callout-background-opacity))",
);
assert.equal(callout.get("border"), "0");
const icon = declarationsFor(css, ":where(.callout-icon)");
assert.equal(icon.get("position"), "absolute");
assert.equal(icon.get("pointer-events"), "none");
assert.equal(icon.get("opacity"), "0.09");
const svg = declarationsFor(css, ":where(.callout-icon svg)");
assert.equal(svg.get("width"), "48px");
assert.equal(svg.get("height"), "48px");
assert.equal(
declarationsFor(css, ":where(.callout.is-collapsed .callout-icon)").get(
"display",
),
"none",
);
});
```
继续断言所有官方 `--callout-*` 类型变量都不存在。
- [ ] **步骤 3运行测试确认红灯**
```bash
node --test tests/components.test.mjs
```
预期FAIL。旧边框仍为 `1px`,新结构选择器不存在。
- [ ] **步骤 4实现 Callout 结构**
`src/components/_callouts.scss` 改为:
```scss
body {
--callout-border-width: 0px;
--callout-border-opacity: 0;
--callout-radius: 7px;
--callout-blend-mode: normal;
--callout-title-size: var(--font-small);
--callout-title-weight: 600;
--callout-content-background: transparent;
}
:where(.callout) {
position: relative;
overflow: hidden;
background-color: rgba(
var(--callout-color),
var(--aera-callout-background-opacity)
);
color: color-mix(
in srgb,
rgb(var(--callout-color)) 72%,
var(--text-normal)
);
border: 0;
}
:where(.callout-title) {
position: relative;
z-index: 1;
padding-inline-end: var(--size-4-12);
color: color-mix(
in srgb,
rgb(var(--callout-color)) 84%,
var(--text-normal)
);
}
:where(.callout-content) {
position: relative;
z-index: 1;
padding-inline-end: var(--size-4-12);
}
:where(.callout-icon) {
position: absolute;
inset-inline-end: var(--size-4-2);
inset-block-end: calc(var(--size-4-2) * -1);
opacity: 0.09;
pointer-events: none;
}
:where(.callout-icon svg) {
width: 48px;
height: 48px;
}
:where(.callout-fold) {
position: relative;
z-index: 2;
}
:where(.callout.is-collapsed .callout-icon) {
display: none;
}
```
不要定义任何 Callout 类型颜色。若 `color-mix()` 的序列化空格与 PostCSS 实际输出不同,只调整测试的精确字符串,不改变比例。
- [ ] **步骤 5构建并验证绿灯**
```bash
npm run build
node --test tests/components.test.mjs
npm run check
git diff --check
```
预期:全部通过;主题政策继续拒绝作用域外 `--callout-color`
- [ ] **步骤 6提交 Callout**
```bash
git add src/components/_callouts.scss tests/components.test.mjs tests/helpers/css.mjs theme.css
git commit -m "feat: restyle semantic callouts"
```
### 任务 4三视图折角引用
**文件:**
- 修改:`tests/helpers/css.mjs`
- 修改:`tests/components.test.mjs`
- 修改:`tests/editor-blocks.test.mjs`
- 修改:`src/editor/_quotes.scss`
- 生成:`theme.css`
- [ ] **步骤 1登记引用选择器**
在两个允许选择器集合中添加:
```js
":where(.markdown-rendered blockquote)",
":where(.markdown-rendered blockquote)::before",
":where(.markdown-rendered blockquote)::after",
":where(.markdown-source-view.mod-cm6.is-live-preview .HyperMD-quote)",
":where(.markdown-source-view.mod-cm6.is-live-preview .HyperMD-quote-1:not(.HyperMD-quote + .HyperMD-quote))",
":where(.markdown-source-view.mod-cm6.is-live-preview .HyperMD-quote-1:not(.HyperMD-quote + .HyperMD-quote))::before",
":where(.markdown-source-view.mod-cm6.is-live-preview .HyperMD-quote-1:not(.HyperMD-quote + .HyperMD-quote))::after",
":where(.markdown-source-view.mod-cm6:not(.is-live-preview) .HyperMD-quote)",
```
- [ ] **步骤 2编写失败的引用测试**
把 body 引用变量改为:
```js
{
"--blockquote-background-color": "var(--aera-quote-background)",
"--blockquote-border-thickness": "0px",
"--blockquote-border-color": "transparent",
"--blockquote-font-style": "normal",
"--blockquote-color": "var(--text-muted)",
}
```
添加以下行为断言:
```js
test("builds the paper fold only in reading and live preview", () => {
const reading = declarationsFor(css, ":where(.markdown-rendered blockquote)");
assert.equal(reading.get("position"), "relative");
assert.equal(reading.get("background"), "var(--aera-quote-background)");
assert.equal(reading.get("border-radius"), "6px");
for (const suffix of ["::before", "::after"]) {
const fold = declarationsFor(
css,
`:where(.markdown-rendered blockquote)${suffix}`,
);
assert.equal(fold.get("content"), '""');
assert.equal(fold.get("position"), "absolute");
}
const source = declarationsFor(
css,
":where(.markdown-source-view.mod-cm6:not(.is-live-preview) .HyperMD-quote)",
);
assert.equal(source.get("background"), "var(--aera-quote-background)");
assert.equal(source.has("content"), false);
});
```
再断言 Live Preview 的第一行选择器及两个伪元素存在,且全 CSS 不含 `:has(`
- [ ] **步骤 3运行测试确认红灯**
```bash
node --test tests/editor-blocks.test.mjs tests/components.test.mjs
```
预期FAIL。旧引用仍为透明背景和绿色左边框新视图规则不存在。
- [ ] **步骤 4实现 Reading、Live Preview 和 Source Mode 规则**
`src/editor/_quotes.scss` 改为以下结构。mixin 只消除重复,不会单独输出 CSS每个伪元素选择器必须独立生成
```scss
@mixin quote-fold-base {
content: "";
position: absolute;
inset-block-start: -1px;
inset-inline-start: -1px;
width: 16px;
height: 16px;
}
@mixin quote-fold-cutout {
@include quote-fold-base;
background: var(--background-primary);
clip-path: polygon(0 0, 100% 0, 0 100%);
}
@mixin quote-fold-paper {
@include quote-fold-base;
background: var(--aera-quote-fold);
clip-path: polygon(100% 0, 100% 100%, 0 100%);
}
body {
--blockquote-background-color: var(--aera-quote-background);
--blockquote-border-thickness: 0px;
--blockquote-border-color: transparent;
--blockquote-font-style: normal;
--blockquote-color: var(--text-muted);
--hr-color: var(--background-modifier-border);
--hr-thickness: 1px;
}
:where(.markdown-rendered blockquote) {
position: relative;
margin-inline: 0;
padding: var(--size-4-4) var(--size-4-5);
background: var(--aera-quote-background);
border: 1px solid var(--background-modifier-border);
border-radius: 6px;
color: var(--blockquote-color);
}
:where(.markdown-rendered blockquote)::before {
@include quote-fold-cutout;
}
:where(.markdown-rendered blockquote)::after {
@include quote-fold-paper;
}
:where(.markdown-source-view.mod-cm6.is-live-preview .HyperMD-quote-1:not(.HyperMD-quote + .HyperMD-quote))::before {
@include quote-fold-cutout;
}
:where(.markdown-source-view.mod-cm6.is-live-preview .HyperMD-quote-1:not(.HyperMD-quote + .HyperMD-quote))::after {
@include quote-fold-paper;
}
:where(.markdown-source-view.mod-cm6.is-live-preview .HyperMD-quote) {
background: var(--aera-quote-background);
color: var(--blockquote-color);
}
:where(.markdown-source-view.mod-cm6.is-live-preview .HyperMD-quote-1:not(.HyperMD-quote + .HyperMD-quote)) {
position: relative;
padding-block-start: var(--size-4-3);
border-radius: 6px 6px 0 0;
}
:where(.markdown-source-view.mod-cm6:not(.is-live-preview) .HyperMD-quote) {
background: var(--aera-quote-background);
color: var(--blockquote-color);
}
```
编译结果必须保持每个登记项与规则一一对应,不允许合并成未登记的逗号选择器。
- [ ] **步骤 5构建并验证绿灯**
```bash
npm run build
node --test tests/editor-blocks.test.mjs tests/components.test.mjs
node scripts/check-theme.mjs
npm run check
git diff --check
```
预期全部通过CSS 不包含 `:has()`Source Mode 规则没有伪元素。
- [ ] **步骤 6提交引用样式**
```bash
git add src/editor/_quotes.scss tests/editor-blocks.test.mjs tests/components.test.mjs tests/helpers/css.mjs theme.css
git commit -m "feat: add folded paper blockquotes"
```
### 任务 5扩展视觉 QA fixture
**文件:**
- 修改:`tests/link-vault.test.mjs`
- 修改:`fixtures/Theme Playground.md`
- [ ] **步骤 1编写失败的 fixture 覆盖测试**
`tests/link-vault.test.mjs` 的 playground 覆盖测试中添加精确断言:
```js
assert.match(playground, /^> 第一行引用,用于检查纸张折角。$/m);
assert.match(playground, /^> 第二行引用,用于检查连续卡片。$/m);
assert.match(playground, /^> > 嵌套引用只保留外层折角。$/m);
for (const type of ["note", "warning", "error", "example", "quote", "tip"]) {
assert.match(playground, new RegExp(`^> \\[!${type}\\]`, "m"));
}
assert.match(playground, /^> \[!tip\]- 收起的提示$/m);
assert.match(playground, /^```javascript$/m);
assert.match(playground, /^```$/m);
assert.ok(playground.trimEnd().endsWith(fixtureOwnershipMarker));
```
测试文件继续复用当前已声明的 `fixtureOwnershipMarker` 常量,不新增第二个 marker 名称。
- [ ] **步骤 2运行测试确认红灯**
```bash
node --test tests/link-vault.test.mjs
```
预期FAIL。旧 fixture 只有单行引用、note/warning 和 CSS 代码块。
- [ ] **步骤 3扩展 fixture**
`fixtures/Theme Playground.md` 中:
1. 用三行普通/嵌套引用替换原单行引用。
2. 保留 note、warning新增 error、example、quote。
3. 新增 `> [!tip]- 收起的提示`,正文一行。
4. 将现有 CSS 代码块替换为包含注释、关键字、字符串、函数、数字和长行的 JavaScript 代码块。
5. 再添加一个无语言 fenced code block。
6. 保持 `<!-- Aera fixture managed by obsidian-aera-theme. -->` 为最后一行。
JavaScript 示例使用以下内容:
```javascript
// Monokai syntax coverage
const accent = "#1677FF";
function renderTheme(name, retries = 3) {
return `${name}:${retries}`;
}
renderTheme("Aera");
```
- [ ] **步骤 4验证 fixture 与安全链接**
```bash
node --test tests/link-vault.test.mjs
AERA_TEST_VAULT=/Users/peter/Code/me/obsidian-plugins/obsidian-aera-theme-test-vault npm run link:vault
cmp fixtures/Theme\ Playground.md /Users/peter/Code/me/obsidian-plugins/obsidian-aera-theme-test-vault/Theme\ Playground.md
npm run check
```
预期全部通过linker 只更新带 ownership marker 的专用 fixture。
- [ ] **步骤 5提交 fixture**
```bash
git add fixtures/Theme\ Playground.md tests/link-vault.test.mjs
git commit -m "test: expand blue component playground"
```
### 任务 6真实 Obsidian 视觉 QA 与预览图
**文件:**
- 修改:`screenshot.png`
- 仅在缺陷回归要求时修改:对应 `src/**/*.scss`、`tests/*.test.mjs`、`theme.css`
- 本地忽略证据:`artifacts/`
- [ ] **步骤 1链接并启用最新主题**
```bash
export AERA_TEST_VAULT=/Users/peter/Code/me/obsidian-plugins/obsidian-aera-theme-test-vault
export AERA_TEST_VAULT_NAME=obsidian-aera-theme-test-vault
npm run build
npm run link:vault
obsidian vault="$AERA_TEST_VAULT_NAME" theme:set name=Aera
obsidian vault="$AERA_TEST_VAULT_NAME" open path="Theme Playground.md"
obsidian vault="$AERA_TEST_VAULT_NAME" dev:errors clear
obsidian vault="$AERA_TEST_VAULT_NAME" dev:console clear
```
预期:所有命令退出 0Aera 启用Playground 打开。
- [ ] **步骤 2验证默认颜色和用户设置优先**
使用 `obsidian eval` 读取 body 的计算变量:
```bash
obsidian vault="$AERA_TEST_VAULT_NAME" eval code="(()=>{const s=getComputedStyle(document.body); const keys=['--accent-h','--accent-s','--accent-l','--interactive-accent','--text-accent','--font-text-size','--file-line-width']; return JSON.stringify(Object.fromEntries(keys.map(k=>[k,s.getPropertyValue(k).trim()])))})()"
```
预期:默认 HSL 对应浅色 `#1677FF` 或深色 `#4096FF`;字号和行宽来自 Obsidian 设置。
仅在专用 Vault 中依次临时设置accent `#7C3AED`、正文 20px、正文 Georgia、等宽字体另一已安装字体。每次用 `getComputedStyle` 检查链接、inline code、代码块和正文确认设置生效。完成后恢复 `accentColor=""`、`baseFontSize=16`、`textFontFamily=""`、等宽字体默认。
- [ ] **步骤 3验证 Callout**
检查 note、warning、error、example、quote 和 tip
```bash
obsidian vault="$AERA_TEST_VAULT_NAME" dev:dom selector='.callout' all
obsidian vault="$AERA_TEST_VAULT_NAME" eval code="JSON.stringify([...document.querySelectorAll('.callout')].map(el=>({type:el.dataset.callout,collapsed:el.classList.contains('is-collapsed'),overflow:el.scrollWidth>el.clientWidth})))"
```
预期:所有类型存在;无边框;正文和标题为语义色;右下原生图标不遮挡;折叠 tip 隐藏大图标;无横向溢出。
- [ ] **步骤 4验证三视图引用和代码**
按顺序验证 Live Preview、Source Mode、Reading View
```bash
mkdir -p artifacts
obsidian vault="$AERA_TEST_VAULT_NAME" dev:screenshot path="$PWD/artifacts/aera-blue-live-preview.png"
obsidian vault="$AERA_TEST_VAULT_NAME" command id=editor:toggle-source
obsidian vault="$AERA_TEST_VAULT_NAME" dev:screenshot path="$PWD/artifacts/aera-blue-source.png"
obsidian vault="$AERA_TEST_VAULT_NAME" command id=editor:toggle-source
obsidian vault="$AERA_TEST_VAULT_NAME" command id=markdown:toggle-preview
obsidian vault="$AERA_TEST_VAULT_NAME" dev:screenshot path="$PWD/artifacts/aera-blue-reading.png"
```
预期:
- Live Preview 的连续引用只有一个左上折角。
- Source Mode 的 `>` 与 fenced markers 可见,引用无折角。
- Reading View 的 blockquote 有折角,代码块排除隐藏 frontmatter。
- 三个视图的多行代码块均为 Monokai行内代码保持中性灰。
- [ ] **步骤 5检查浅色、深色、窄分栏和移动端**
```bash
obsidian vault="$AERA_TEST_VAULT_NAME" command id=markdown:toggle-preview
body_class=$(obsidian vault="$AERA_TEST_VAULT_NAME" dev:dom selector="body" attr=class)
if [[ "$body_class" != *theme-light* ]]; then
obsidian vault="$AERA_TEST_VAULT_NAME" command id=theme:toggle-light-dark
fi
obsidian vault="$AERA_TEST_VAULT_NAME" dev:screenshot path="$PWD/artifacts/aera-blue-light.png"
obsidian vault="$AERA_TEST_VAULT_NAME" command id=theme:toggle-light-dark
obsidian vault="$AERA_TEST_VAULT_NAME" dev:screenshot path="$PWD/artifacts/aera-blue-dark.png"
obsidian vault="$AERA_TEST_VAULT_NAME" dev:mobile on
obsidian vault="$AERA_TEST_VAULT_NAME" dev:screenshot path="$PWD/artifacts/aera-blue-mobile.png"
obsidian vault="$AERA_TEST_VAULT_NAME" dev:mobile off
```
`view_image` 原尺寸检查六张图。正文根节点不得横向溢出;代码长行必须只在代码容器内滚动。
- [ ] **步骤 6对每个视觉缺陷执行 TDD**
如果发现缺陷:
1. 在负责组件的现有测试文件添加一个最小失败断言。
2. 运行目标测试,确认因该缺陷红灯。
3. 只修改负责该组件的 SCSS。
4. `npm run build` 后运行目标测试和 `npm run check`
5. 重复对应截图并用 `view_image` 复核。
禁止直接试 CSS 后补测试。
- [ ] **步骤 7生成社区预览**
从构图最佳的浅色截图生成:
```bash
magick artifacts/aera-blue-light.png \
-crop '1024x576+0+80' +repage \
-resize 512x288 \
-colorspace sRGB -strip \
+set date:create +set date:modify +set date:timestamp \
-define png:exclude-chunk=time,text \
screenshot.png
magick identify -format '%wx%h %z-bit %[colorspace] %b\n' screenshot.png
```
预期:`512x288 8-bit sRGB`。如果窗口尺寸不是 1024×800不照搬 crop先用 `identify` 计算不切断工具栏或标题的 16:9 区域,再生成。
- [ ] **步骤 8读取错误并提交 QA**
```bash
obsidian vault="$AERA_TEST_VAULT_NAME" dev:errors
obsidian vault="$AERA_TEST_VAULT_NAME" dev:console level=error limit=100
npm run check
git diff --check
git add screenshot.png src tests theme.css fixtures
git commit -m "test: verify blue content components in Obsidian"
```
预期Obsidian 无错误;只提交实际变化文件;`artifacts/` 保持忽略。
### 任务 7最终 1.0.0 RC 验证
**文件:**
- 验证:`package.json`、`package-lock.json`、`manifest.json`、`versions.json`
- 验证:`theme.css`、`screenshot.png`、README、LICENSE
- [ ] **步骤 1验证版本未漂移**
```bash
node --input-type=module --eval "
import fs from 'node:fs';
const pkg=JSON.parse(fs.readFileSync('package.json'));
const lock=JSON.parse(fs.readFileSync('package-lock.json'));
const manifest=JSON.parse(fs.readFileSync('manifest.json'));
const versions=JSON.parse(fs.readFileSync('versions.json'));
if (![pkg.version,lock.version,lock.packages[''].version,manifest.version].every(v=>v==='1.0.0')) process.exit(1);
if (versions['1.0.0']!=='1.12.7') process.exit(1);
console.log('Aera 1.0.0 metadata aligned');
"
```
预期:输出 `Aera 1.0.0 metadata aligned`
- [ ] **步骤 2运行完整发布候选门禁**
```bash
npm run check
node scripts/check-theme.mjs --release-tag 1.0.0
git diff --check
git diff --exit-code -- theme.css
magick identify -format '%wx%h\n' screenshot.png
test -s manifest.json
test -s theme.css
test -s README.md
test -s LICENSE
test -s screenshot.png
git status --short --branch
git tag --list
```
预期:自动化测试全部通过;截图为 `512x288`;工作区干净;没有本地 tag。
- [ ] **步骤 3最终独立审查**
独立审查 `82497d7..HEAD`
- 规格覆盖和选择器边界。
- 用户 accent、字体、字号、行宽优先级。
- Callout 语义色与折叠行为。
- 引用三视图差异。
- 行内代码与 Monokai 作用域隔离。
- fixture/linker 安全。
- 截图和版本资产。
Critical 与 Important 必须由原实现代理修复并重新审查;没有未解决问题后才进入分支收尾。
- [ ] **步骤 4保持外部发布审批门**
不要 push、创建 Pull Request、tag、GitHub Release 或提交 Community Themes。使用 `finishing-a-development-branch` 给 Peter 提供本地合并、PR、保持分支或丢弃选项。

View file

@ -55,7 +55,7 @@ Aera 1.0.0 本地发布候选已经完成正文基础、浅深色模式、桌面
| 默认强调色 | `#1677FF` |
| 高亮背景 | 克制的暖黄色透明层 |
浅色默认 accent 的 HSL 必须对应 `#1677FF`。链接、标签、复选框、选择状态与焦点状态继续从 Obsidian 的用户 accent 变量派生,不直接写死蓝色。
浅色默认 accent 的 HSL 必须对应 `#1677FF`,用于控件、焦点和非文本强调。`#1677FF` 在雾白背景上的对比度不足以直接承担小号正文链接,因此文字 accent 从同一组用户 accent 变量动态降低亮度,默认解析为更深的蓝色并达到 WCAG AA。链接、标签、复选框、选择状态与焦点状态仍从用户 accent 变量派生,不锁死用户颜色。
### 深色模式
@ -131,8 +131,8 @@ Aera 1.0.0 本地发布候选已经完成正文基础、浅深色模式、桌面
| --- | --- |
| 背景 | `#272822` |
| 正文 | `#F8F8F2` |
| 注释 | `#75715E` |
| 关键字与标签 | `#F92672` |
| 注释 | `#929388`(由经典 `#75715E` 提亮以达到 AA |
| 关键字与标签 | `#FF4B8B`(由经典 `#F92672` 提亮以达到 AA |
| 字符串 | `#E6DB74` |
| 函数 | `#A6E22E` |
| 属性与类型 | `#66D9EF` |
@ -211,11 +211,12 @@ Aera 1.0.0 本地发布候选已经完成正文基础、浅深色模式、桌面
1. 默认界面在浅色与深色模式下均不再呈现绿色基调。
2. 浅色默认 accent 与 `#1677FF` 一致;深色使用可通过对比度的同系蓝。
3. 用户自定义 accent、字体、字号和行宽继续生效。
4. 所有 Callout 无边框,标题和正文使用语义色,右下角显示低透明度原生图标,折叠状态无冲突。
5. Reading View 与 Live Preview 的引用显示单一左上折角Source Mode 保留 `>` 且无折角。
6. fenced code block 在所有模式下呈现完整 Monokai行内代码保持中性灰风格。
7. Callout、引用、代码块在窄分栏和移动端无溢出、遮挡或重叠。
8. 自动化测试、主题政策、WCAG 检查、真实 Obsidian 错误检查全部通过。
9. `theme.css`、QA 截图和 `screenshot.png` 与最新 SCSS 一致。
10. 工作区提交范围清晰,未执行任何外部发布操作。
3. 默认文字 accent 从同一组 accent 变量派生,在浅色与深色背景上达到 WCAG AA。
4. 用户自定义 accent、字体、字号和行宽继续生效。
5. 所有 Callout 无边框,标题和正文使用语义色,右下角显示低透明度原生图标,折叠状态无冲突。
6. Reading View 与 Live Preview 的引用显示单一左上折角Source Mode 保留 `>` 且无折角。
7. fenced code block 在所有模式下呈现完整 Monokai行内代码保持中性灰风格。
8. Callout、引用、代码块在窄分栏和移动端无溢出、遮挡或重叠。
9. 自动化测试、主题政策、WCAG 检查、真实 Obsidian 错误检查全部通过。
10. `theme.css`、QA 截图和 `screenshot.png` 与最新 SCSS 一致。
11. 工作区提交范围清晰,未执行任何外部发布操作。