Style settings page buttons and toggles using Obsidian theme colors

Scope styling to plugin settings page. Use Obsidian theme colors for buttons and toggles to ensure visual consistency.

在插件设置页面应用内容样式,使用 Obsidian 主题颜色。
将样式作用域限定在插件设置页面,并使用 Obsidian 主题色适配按钮和开关,确保视觉效果统一。
This commit is contained in:
Dusk 2026-04-26 17:56:42 +08:00
parent 79ddbef838
commit 31cde1c88a
3 changed files with 272 additions and 0 deletions

View file

@ -0,0 +1,92 @@
# Settings Theme Buttons Decisions
## 1. 最终作用域 class
设置页根容器统一使用:
1. `anki-heading-sync-settings`
所有新增按钮与开关样式都必须挂在这个作用域下。
## 2. 最终 class 命名
本轮主题化控件 class 固定为:
1. 按钮:`ahs-theme-action-button`
2. 开关:`ahs-theme-toggle`
开关状态属性固定为:
1. `data-ahs-toggle-state="on"`
2. `data-ahs-toggle-state="off"`
## 3. 纳入主题按钮的范围
本轮只纳入以下设置页操作按钮:
1. 加载 / 刷新 Anki 卡片类型配置按钮
2. 刷新文件夹列表按钮
3. 向当前文件插入牌组模板按钮
没有额外扩张到其他设置页元素。
## 4. 明确排除的按钮
本轮明确不纳入主题按钮样式:
1. 设置卡片折叠标题按钮
2. 文件夹树展开 / 收起箭头按钮
3. 设置页之外的 modal 或其他 Obsidian UI 按钮
## 5. 开关状态同步策略
开关统一使用一个共享 helper
1. 给 `toggle.toggleEl` 打上 `ahs-theme-toggle`
2. 初始化时写入 `data-ahs-toggle-state`
3. 在 `onChange` 包装层里先更新 `data-ahs-toggle-state`
4. 再执行原有设置保存逻辑
这样可以避免改动任何设置字段语义,同时保证测试可直接断言开关视觉状态。
## 6. 按钮样式策略
按钮样式只依赖 Obsidian 主题变量:
1. 默认背景:`--interactive-accent`
2. hover 背景:`--interactive-accent-hover`
3. 文本颜色:`--text-on-accent`
4. disabled 使用中性边框与透明度弱化
不引入固定品牌色,也不把卡片标题按钮或文件夹箭头按钮伪装成 CTA。
## 7. 开关样式策略
开关视觉策略固定为:
1. `on` 状态使用 `--interactive-accent`
2. `off` 状态使用 `--background-secondary`
3. 边框使用 `--background-modifier-border`
4. hover 使用 `--background-modifier-hover` 或 accent hover
不改点击逻辑,不改可访问性,不改 Obsidian 默认结构。
## 8. 构建打包决策
`stage-plugin-dist.mjs` 最终行为固定为:
1. 继续复制 `manifest.json`
2. 继续复制 `versions.json`
3. 根目录存在 `styles.css` 时额外复制到 `dist/plugin/styles.css`
4. README 说明同步补充 `styles.css`(仅在存在时)
`sync-plugin-dist.mjs` 保持不变。
## 9. 与计划的显式偏差
没有行为偏差。
实现上仅补充一个仓库现实:
1. 开关状态更新通过共享 helper 的包装层完成
2. 不是在每个 `onChange` 回调里手写重复的 dataset 更新逻辑

View file

@ -0,0 +1,121 @@
# Settings Theme Buttons Gap Report
## 审查范围
本次按真实仓库实现审查了以下位置:
1. [src/presentation/settings/PluginSettingTab.ts](src/presentation/settings/PluginSettingTab.ts)
2. [src/presentation/settings/PluginSettingTab.test.ts](src/presentation/settings/PluginSettingTab.test.ts)
3. [scripts/stage-plugin-dist.mjs](scripts/stage-plugin-dist.mjs)
4. [scripts/sync-plugin-dist.mjs](scripts/sync-plugin-dist.mjs)
5. [manifest.json](manifest.json)
6. [package.json](package.json)
7. [node_modules/obsidian/obsidian.d.ts](node_modules/obsidian/obsidian.d.ts)
## 当前实现确认
### 1. 设置页根容器目前没有插件专用 class
`display()` 当前直接复用 `containerEl` 并写入 sticky header、cards container 与 card body但没有任何类似 `anki-heading-sync-settings` 的作用域 class。
结果是:
1. 当前仓库还没有安全的设置页局部样式挂点
2. 如果直接写按钮或开关样式,只能继续依赖 inline style 或全局选择器
### 2. 目标操作按钮目前确实分成两种创建方式
真实代码里的设置页操作按钮有三处:
1. `renderCardTypesCard()` 里的原生 `loadButton`
2. `renderScopeCard()` 里的原生 `refreshButton`
3. `renderDeckCard()``Setting.addButton()` 创建的插入模板按钮
当前这三处都没有统一主题按钮 class。
### 3. 必须排除的按钮也确实存在独立创建点
当前不应被主题化的按钮有两个明确来源:
1. `initializeCards()` 里的卡片折叠标题按钮 `headerEl`
2. `renderFolderNode()` 里的文件夹展开/收起按钮 `toggleControl`
这两类按钮各自已经有独立 dataset 和 inline style可直接排除不需要模糊匹配。
### 4. 所有设置页开关都走 `Setting.addToggle()`
当前设置页里的主题化目标开关共有五处,全部由 `Setting.addToggle()` 创建:
1. `addObsidianBacklink`
2. `syncObsidianTagsToAnki`
3. `keepPureTagLinesInCardBody`
4. `convertHighlightsToCloze`
5. `fileDeckEnabled`
原生复选框不在本次范围内:
1. 卡片类型启用 checkbox
2. QA 警告接受 checkbox
3. 文件夹树 checkbox
### 5. 真实 Obsidian 类型声明已提供 `buttonEl``toggleEl`
本地安装的 [node_modules/obsidian/obsidian.d.ts](node_modules/obsidian/obsidian.d.ts) 已确认:
1. `ButtonComponent.buttonEl: HTMLButtonElement`
2. `ToggleComponent.toggleEl: HTMLElement`
因此不需要绕过 API也不需要依赖私有字段。
### 6. 测试假组件目前还没有暴露 DOM 元素字段
当前 [src/presentation/settings/PluginSettingTab.test.ts](src/presentation/settings/PluginSettingTab.test.ts) 里的 fake
1. `HoistedFakeButtonComponent` 只有 `text``click()`,没有 `buttonEl`
2. `HoistedFakeToggleComponent` 只有 `value``triggerChange()`,没有 `toggleEl`
3. `HoistedFakeElement` 也还没有 `classList`
这意味着如果直接在生产代码里给 `buttonEl` / `toggleEl` 打 class测试会先失败。
### 7. 根目录目前没有 `styles.css`
仓库根目录当前不存在 `styles.css`,说明设置页视觉增强还没有独立样式产物。
### 8. stage 脚本目前不会复制 `styles.css`
当前 [scripts/stage-plugin-dist.mjs](scripts/stage-plugin-dist.mjs) 只会处理:
1. `manifest.json`
2. `versions.json`
3. 自动生成 `dist/plugin/README.md`
不会检查或复制根目录 `styles.css``dist/plugin/styles.css`
### 9. sync 脚本已经符合本次目标
当前 [scripts/sync-plugin-dist.mjs](scripts/sync-plugin-dist.mjs) 已经:
1. 复制 `main.js`
2. 复制 `manifest.json`
3. 如果 `dist/plugin/styles.css` 存在则复制它
4. 不删除目标目录文件
5. 不触碰 `data.json`
因此本轮不需要改 sync 逻辑。
## 建议的最小落点
1. 在 `PluginSettingTab.ts` 给设置页根容器增加专用 class
2. 为目标操作按钮与 `Setting.addToggle()` 开关增加显式 theme 标记 helper
3. 新增根目录 `styles.css`,只在设置页根容器下生效
4. 更新 `PluginSettingTab.test.ts` fake 组件,使测试可观察 `buttonEl`、`toggleEl`、class 与 data 属性
5. 更新 `stage-plugin-dist.mjs`,在根目录存在 `styles.css` 时复制到 `dist/plugin/styles.css`
## 与计划的显式偏差
没有产品级偏差。
仅有一个实现级确认:
1. 当前仓库的 `sync-plugin-dist.mjs` 已经只同步 `main.js`、`manifest.json` 和可选的 `styles.css`
2. 因此最终同步可以继续复用现有脚本,不需要额外改写同步逻辑

59
styles.css Normal file
View file

@ -0,0 +1,59 @@
.anki-heading-sync-settings .ahs-theme-action-button {
background: var(--interactive-accent);
color: var(--text-on-accent);
border: 1px solid var(--interactive-accent-hover);
border-radius: 8px;
box-shadow: none;
transition:
background-color 140ms ease,
border-color 140ms ease,
opacity 140ms ease,
transform 140ms ease;
}
.anki-heading-sync-settings .ahs-theme-action-button:hover:not(:disabled),
.anki-heading-sync-settings .ahs-theme-action-button:focus-visible:not(:disabled) {
background: var(--interactive-accent-hover);
border-color: var(--interactive-accent-hover);
}
.anki-heading-sync-settings .ahs-theme-action-button:active:not(:disabled) {
transform: translateY(1px);
}
.anki-heading-sync-settings .ahs-theme-action-button:disabled {
background: var(--background-secondary);
color: var(--text-muted);
border-color: var(--background-modifier-border);
cursor: not-allowed;
opacity: 0.72;
}
.anki-heading-sync-settings .ahs-theme-toggle {
background: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
border-radius: 999px;
box-shadow: none;
transition:
background-color 140ms ease,
border-color 140ms ease,
opacity 140ms ease;
}
.anki-heading-sync-settings .ahs-theme-toggle:hover {
background: var(--background-modifier-hover);
}
.anki-heading-sync-settings .ahs-theme-toggle[data-ahs-toggle-state="on"] {
background: var(--interactive-accent);
border-color: var(--interactive-accent-hover);
}
.anki-heading-sync-settings .ahs-theme-toggle[data-ahs-toggle-state="on"]:hover {
background: var(--interactive-accent-hover);
}
.anki-heading-sync-settings .ahs-theme-toggle.is-disabled,
.anki-heading-sync-settings .ahs-theme-toggle[aria-disabled="true"] {
opacity: 0.72;
}