fix(settings): use native CTA buttons

中文: 将设置页操作按钮改为 Obsidian 原生 ButtonComponent CTA,移除自定义按钮样式覆盖,并更新测试与构建产物。

English: Use native Obsidian ButtonComponent CTA buttons for settings actions, remove custom button style overrides, and update tests and build output.
This commit is contained in:
Dusk 2026-04-26 20:48:13 +08:00
parent c9e80d4034
commit dfccdcbfc5
4 changed files with 60 additions and 80 deletions

32
main.js

File diff suppressed because one or more lines are too long

View file

@ -6,6 +6,7 @@ import { DEFAULT_SETTINGS, normalizePluginSettings } from "@/application/config/
const QA_GROUP_USER_MODEL_NAME = "问答题(多级列表)";
const {
FakeButtonComponent,
FakeElement,
FakePluginSettingTab,
FakeSetting,
@ -175,9 +176,13 @@ const {
class HoistedFakeButtonComponent {
public text = "";
public readonly buttonEl = new HoistedFakeElement(new HoistedFakeContainerEl(), "button", null);
public readonly buttonEl: HoistedFakeElement;
private onClickHandler?: () => void | Promise<void>;
constructor(containerEl?: HoistedFakeElement) {
this.buttonEl = containerEl?.createEl("button") ?? new HoistedFakeElement(new HoistedFakeContainerEl(), "button", null);
}
setButtonText(text: string): this {
this.text = text;
this.buttonEl.text = text;
@ -185,8 +190,19 @@ const {
return this;
}
setCta(): this {
this.buttonEl.classList.add("mod-cta");
return this;
}
setDisabled(disabled: boolean): this {
this.buttonEl.disabled = disabled;
return this;
}
onClick(callback: () => void | Promise<void>): this {
this.onClickHandler = callback;
this.buttonEl.addEventListener("click", callback);
return this;
}
@ -373,6 +389,7 @@ const {
});
vi.mock("obsidian", () => ({
ButtonComponent: FakeButtonComponent,
getLanguage: getLanguageMock,
PluginSettingTab: FakePluginSettingTab,
Setting: FakeSetting,
@ -837,7 +854,7 @@ describe("PluginSettingTab", () => {
expect(plugin.updateCalls).toContainEqual({ convertHighlightsToCloze: false });
});
it("applies the themed class only to settings action buttons", async () => {
it("uses native Obsidian CTA buttons only for settings actions", async () => {
const plugin = new FakePlugin();
plugin.settings = normalizePluginSettings({
...plugin.settings,
@ -850,14 +867,14 @@ describe("PluginSettingTab", () => {
await expandCard(tab, "scope");
await flushPromises();
expect(queryByDataset(tab.containerEl, "cardTypesRefresh", "true").classList.contains("ahs-theme-action-button")).toBe(true);
expect(queryByDataset(tab.containerEl, "scopeRefreshFolders", "true").classList.contains("ahs-theme-action-button")).toBe(true);
expect(queryByDataset(tab.containerEl, "cardTypesRefresh", "true").classList.contains("mod-cta")).toBe(true);
expect(queryByDataset(tab.containerEl, "scopeRefreshFolders", "true").classList.contains("mod-cta")).toBe(true);
const cardHeaderButton = queryByDataset(tab.containerEl, "settingsCardToggle", "card-types");
expect(cardHeaderButton.classList.contains("ahs-theme-action-button")).toBe(false);
expect(cardHeaderButton.classList.contains("mod-cta")).toBe(false);
const folderToggleButton = queryByDataset(tab.containerEl, "folderToggle", "notes");
expect(folderToggleButton.classList.contains("ahs-theme-action-button")).toBe(false);
expect(folderToggleButton.classList.contains("mod-cta")).toBe(false);
await expandCard(tab, "deck");
const fileDeckSetting = findSetting(tab.containerEl, "开启文件级自定义牌组");
@ -865,7 +882,7 @@ describe("PluginSettingTab", () => {
const insertTemplateSetting = findSetting(tab.containerEl, "向当前文件插入牌组模板");
const insertTemplateButton = getButton(insertTemplateSetting);
expect(insertTemplateButton.buttonEl.classList.contains("ahs-theme-action-button")).toBe(true);
expect(insertTemplateButton.buttonEl.classList.contains("mod-cta")).toBe(true);
await insertTemplateButton.click();
expect(plugin.insertDeckTemplateCalls).toBe(1);

View file

@ -1,5 +1,4 @@
import { PluginSettingTab, Setting } from "obsidian";
import type { ButtonComponent } from "obsidian";
import { ButtonComponent, PluginSettingTab, Setting } from "obsidian";
import {
DEFAULT_OBSIDIAN_BACKLINK_LABEL,
@ -51,7 +50,6 @@ const SETTINGS_PAGE_HEADER_MASK_TOP = "-128px";
const SETTINGS_PAGE_HEADER_MASK_SIDE = "-24px";
const DECK_HELPER_TEXT_INDENT = "32px";
const SETTINGS_ROOT_CLASS = "anki-heading-sync-settings";
const THEME_ACTION_BUTTON_CLASS = "ahs-theme-action-button";
type SettingsCardId = (typeof SETTINGS_CARD_ORDER)[number];
type NoteTypeCacheCheckStatus = "idle" | "checking" | "same" | "changed" | "failed";
@ -167,12 +165,8 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
containerEl.scrollTop = previousScrollTop;
}
private markThemeButton(buttonEl: HTMLButtonElement): void {
buttonEl.classList.add(THEME_ACTION_BUTTON_CLASS);
}
private markThemeButtonComponent(button: ButtonComponent): void {
this.markThemeButton(button.buttonEl);
private markActionButton(button: ButtonComponent): void {
button.setCta();
}
private initializeCards(containerEl: HTMLElement): void {
@ -305,16 +299,14 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
actionRow.style.flexDirection = "column";
actionRow.style.alignItems = "flex-start";
actionRow.style.gap = "8px";
const loadButton = actionRow.createEl("button", {
text: this.ankiConfigLoading
const loadButton = new ButtonComponent(actionRow)
.setButtonText(this.ankiConfigLoading
? t("settings.cards.cardTypes.loadAnki.loading")
: t("settings.cards.cardTypes.loadAnki.button"),
}) as HTMLButtonElement;
this.markThemeButton(loadButton);
loadButton.type = "button";
loadButton.dataset.cardTypesRefresh = "true";
loadButton.disabled = this.ankiConfigLoading;
loadButton.addEventListener("click", () => this.loadAnkiCardTypeConfig());
: t("settings.cards.cardTypes.loadAnki.button"))
.setCta()
.setDisabled(this.ankiConfigLoading)
.onClick(() => this.loadAnkiCardTypeConfig());
loadButton.buttonEl.dataset.cardTypesRefresh = "true";
actionRow.createEl("span", {
text: `${t("settings.cards.cardTypes.statusLabel")}${renderUserFacingMessage(this.getCardTypeStatusMessage())}`,
});
@ -691,14 +683,14 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
}
const refreshRow = containerEl.createDiv();
const refreshButton = refreshRow.createEl("button", { text: t("settings.cards.scope.refreshFolders") }) as HTMLButtonElement;
this.markThemeButton(refreshButton);
refreshButton.type = "button";
refreshButton.dataset.scopeRefreshFolders = "true";
refreshButton.disabled = Boolean(this.folderTreeLoadPromise);
refreshButton.addEventListener("click", () => {
void this.refreshFolderTree();
});
const refreshButton = new ButtonComponent(refreshRow)
.setButtonText(t("settings.cards.scope.refreshFolders"))
.setCta()
.setDisabled(Boolean(this.folderTreeLoadPromise))
.onClick(() => {
void this.refreshFolderTree();
});
refreshButton.buttonEl.dataset.scopeRefreshFolders = "true";
this.ensureFolderTreeLoaded();
containerEl.createEl("p", { text: getScopeModeTreeDescription(this.plugin.settings.scopeMode) });
@ -809,7 +801,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
.setName(t("settings.deck.insertTemplate.name"))
.setDesc(t("settings.deck.insertTemplate.desc"))
.addButton((button) => {
this.markThemeButtonComponent(button);
this.markActionButton(button);
button.setButtonText(t("settings.deck.insertTemplate.button")).onClick(() => {
void this.plugin.insertDeckTemplateToCurrentFile();
});

View file

@ -1,30 +1 @@
.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;
}
/* Intentionally empty: settings controls use native Obsidian component styles. */