feat: Enhance Obsidian Halo Plugin with comprehensive design system and component styles

- Updated styles.css to implement a three-layer design token architecture: Primitive, Semantic, and Component.
- Introduced new CSS variables for colors, spacing, typography, and shadows.
- Developed styles for buttons, cards, inputs, textareas, selects, badges, tables, modals, forms, color pickers, empty states, and loading spinners.
- Improved sync status view with responsive design and enhanced UI elements.
- Added skeleton loading animations for better user experience.

chore: Add configuration files for development environment

- Created .claude/settings.json to enable commit commands plugin.
- Added .vscode/c_cpp_properties.json for C/C++ IntelliSense configuration.
- Introduced .vscode/launch.json for debugging C/C++ applications with GDB.
This commit is contained in:
2026-04-28 20:20:56 +08:00
parent 86514309cf
commit 3fd29c824a
13 changed files with 3131 additions and 355 deletions
+5
View File
@@ -0,0 +1,5 @@
{
"enabledPlugins": {
"commit-commands@claude-plugins-official": true
}
}
+123 -23
View File
File diff suppressed because one or more lines are too long
+1221 -4
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -156,13 +156,12 @@
"state": { "state": {
"type": "outline", "type": "outline",
"state": { "state": {
"file": "博客/Git团队协作指南(精简版).md",
"followCursor": true, "followCursor": true,
"showSearch": true, "showSearch": true,
"searchQuery": "" "searchQuery": ""
}, },
"icon": "lucide-list", "icon": "lucide-list",
"title": "Git团队协作指南(精简版) 的大纲" "title": "大纲"
} }
}, },
{ {
+18
View File
@@ -0,0 +1,18 @@
{
"configurations": [
{
"name": "windows-gcc-x64",
"includePath": [
"${workspaceFolder}/**"
],
"compilerPath": "D:/settings/Language/C/mingw64/bin/gcc.exe",
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "windows-gcc-x64",
"compilerArgs": [
""
]
}
],
"version": 4
}
+24
View File
@@ -0,0 +1,24 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "C/C++ Runner: Debug Session",
"type": "cppdbg",
"request": "launch",
"args": [],
"stopAtEntry": false,
"externalConsole": true,
"cwd": "d:/Code/Obsidian/.obsidian/plugins/halo",
"program": "d:/Code/Obsidian/.obsidian/plugins/halo/build/Debug/outDebug",
"MIMode": "gdb",
"miDebuggerPath": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
+58 -1
View File
@@ -1,3 +1,60 @@
{ {
"git.ignoreLimitWarning": true "git.ignoreLimitWarning": true,
"C_Cpp_Runner.cCompilerPath": "gcc",
"C_Cpp_Runner.cppCompilerPath": "g++",
"C_Cpp_Runner.debuggerPath": "gdb",
"C_Cpp_Runner.cStandard": "c17",
"C_Cpp_Runner.cppStandard": "c++17",
"C_Cpp_Runner.msvcBatchPath": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\VC\\Auxiliary\\Build\\vcvarsall.bat",
"C_Cpp_Runner.useMsvc": false,
"C_Cpp_Runner.warnings": [
"-Wall",
"-Wextra",
"-Wpedantic",
"-Wshadow",
"-Wformat=2",
"-Wcast-align",
"-Wconversion",
"-Wsign-conversion",
"-Wnull-dereference"
],
"C_Cpp_Runner.msvcWarnings": [
"/W4",
"/permissive-",
"/w14242",
"/w14287",
"/w14296",
"/w14311",
"/w14826",
"/w44062",
"/w44242",
"/w14905",
"/w14906",
"/w14263",
"/w44265",
"/w14928"
],
"C_Cpp_Runner.enableWarnings": true,
"C_Cpp_Runner.warningsAsError": false,
"C_Cpp_Runner.compilerArgs": [],
"C_Cpp_Runner.linkerArgs": [],
"C_Cpp_Runner.includePaths": [],
"C_Cpp_Runner.includeSearch": [
"*",
"**/*"
],
"C_Cpp_Runner.excludeSearch": [
"**/build",
"**/build/**",
"**/.*",
"**/.*/**",
"**/.vscode",
"**/.vscode/**"
],
"C_Cpp_Runner.useAddressSanitizer": false,
"C_Cpp_Runner.useUndefinedSanitizer": false,
"C_Cpp_Runner.useLeakSanitizer": false,
"C_Cpp_Runner.showCompilationTime": false,
"C_Cpp_Runner.useLinkTimeOptimization": false,
"C_Cpp_Runner.msvcSecureNoWarnings": false
} }
+10 -2
View File
@@ -196,7 +196,11 @@
}, },
"tag_manager_modal": { "tag_manager_modal": {
"title": "Tag Manager", "title": "Tag Manager",
"button_create": "Create Tag", "title_create": "Create Tag",
"title_edit": "Edit Tag",
"button_create": "Create",
"button_save": "Save",
"button_cancel": "Cancel",
"loading": "Loading...", "loading": "Loading...",
"empty": "No tags", "empty": "No tags",
"column_name": "Name", "column_name": "Name",
@@ -218,7 +222,11 @@
}, },
"category_manager_modal": { "category_manager_modal": {
"title": "Category Manager", "title": "Category Manager",
"button_create": "Create Category", "title_create": "Create Category",
"title_edit": "Edit Category",
"button_create": "Create",
"button_save": "Save",
"button_cancel": "Cancel",
"loading": "Loading...", "loading": "Loading...",
"empty": "No categories", "empty": "No categories",
"column_name": "Name", "column_name": "Name",
+10 -2
View File
@@ -196,7 +196,11 @@
}, },
"tag_manager_modal": { "tag_manager_modal": {
"title": "标签管理", "title": "标签管理",
"button_create": "创建标签", "title_create": "创建标签",
"title_edit": "编辑标签",
"button_create": "创建",
"button_save": "保存",
"button_cancel": "取消",
"loading": "加载中...", "loading": "加载中...",
"empty": "暂无标签", "empty": "暂无标签",
"column_name": "名称", "column_name": "名称",
@@ -218,7 +222,11 @@
}, },
"category_manager_modal": { "category_manager_modal": {
"title": "分类管理", "title": "分类管理",
"button_create": "创建分类", "title_create": "创建分类",
"title_edit": "编辑分类",
"button_create": "创建",
"button_save": "保存",
"button_cancel": "取消",
"loading": "加载中...", "loading": "加载中...",
"empty": "暂无分类", "empty": "暂无分类",
"column_name": "名称", "column_name": "名称",
+10 -2
View File
@@ -196,7 +196,11 @@
}, },
"tag_manager_modal": { "tag_manager_modal": {
"title": "標籤管理", "title": "標籤管理",
"button_create": "創建標籤", "title_create": "創建標籤",
"title_edit": "編輯標籤",
"button_create": "創建",
"button_save": "保存",
"button_cancel": "取消",
"loading": "加載中...", "loading": "加載中...",
"empty": "暫無標籤", "empty": "暫無標籤",
"column_name": "名稱", "column_name": "名稱",
@@ -218,7 +222,11 @@
}, },
"category_manager_modal": { "category_manager_modal": {
"title": "分類管理", "title": "分類管理",
"button_create": "創建分類", "title_create": "創建分類",
"title_edit": "編輯分類",
"button_create": "創建",
"button_save": "保存",
"button_cancel": "取消",
"loading": "加載中...", "loading": "加載中...",
"empty": "暫無分類", "empty": "暫無分類",
"column_name": "名稱", "column_name": "名稱",
@@ -1,5 +1,5 @@
import i18next from "i18next"; import i18next from "i18next";
import { Modal, Notice, Setting } from "obsidian"; import { Modal, Notice } from "obsidian";
import type HaloPlugin from "../main"; import type HaloPlugin from "../main";
import type { HaloSite } from "../settings"; import type { HaloSite } from "../settings";
import HaloService from "../service"; import HaloService from "../service";
@@ -9,8 +9,145 @@ export function openCategoryManagerModal(plugin: HaloPlugin, site: HaloSite): vo
modal.open(); modal.open();
} }
interface CategoryFormData {
name: string;
slug: string;
priority: number;
}
class CategoryFormModal extends Modal {
private category: { metadata: { name: string }; spec: { displayName: string; slug: string; priority: number } } | null = null;
private isEdit = false;
private onSubmit: (data: CategoryFormData) => void;
constructor(
private plugin: HaloPlugin,
private site: HaloSite,
category: CategoryFormModal["category"],
private onSubmitFn: (data: CategoryFormData) => void
) {
super(app);
this.category = category;
this.isEdit = !!category;
this.onSubmit = onSubmitFn;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
const modalEl = contentEl.createDiv("halo-modal");
modalEl.style.cssText = `
position: fixed; inset: 0; z-index: 1000;
display: flex; align-items: center; justify-content: center;
background: rgba(0,0,0,0.5); -webkit-backdrop-filter: blur(4px);
`;
const content = modalEl.createDiv("halo-card");
content.style.cssText = `
width: 100%; max-width: 440px; max-height: 90vh; overflow-y: auto;
padding: var(--space-6);
`;
const header = content.createDiv("halo-modal-header");
header.style.cssText = "margin-bottom: var(--space-4);";
const title = header.createEl("h3");
title.textContent = this.isEdit
? i18next.t("category_manager_modal.title_edit")
: i18next.t("category_manager_modal.title_create");
title.style.cssText = "font-size: 18px; font-weight: 600; margin: 0;";
const closeBtn = header.createEl("button", { text: "✕" });
closeBtn.className = "halo-modal-close";
closeBtn.style.cssText = `
display: flex; align-items: center; justify-content: center;
width: 32px; height: 32px; background: transparent; border: none;
border-radius: 8px; color: var(--text-muted); cursor: pointer; font-size: 16px;
`;
closeBtn.addEventListener("click", () => this.close());
const form = content.createDiv("halo-modal-body");
// Name field
const nameGroup = form.createDiv("halo-form-group");
const nameLabel = nameGroup.createEl("label", { text: i18next.t("category_manager_modal.column_name") });
nameLabel.className = "halo-form-label";
const nameInput = nameGroup.createEl("input", { type: "text" }) as HTMLInputElement;
nameInput.className = "halo-input";
nameInput.style.cssText = "width: 100%;";
nameInput.value = this.category?.spec.displayName || "";
nameInput.placeholder = i18next.t("category_manager_modal.prompt_name");
// Slug field
const slugGroup = form.createDiv("halo-form-group");
const slugLabel = slugGroup.createEl("label", { text: i18next.t("category_manager_modal.column_slug") });
slugLabel.className = "halo-form-label";
const slugInput = slugGroup.createEl("input", { type: "text" }) as HTMLInputElement;
slugInput.className = "halo-input";
slugInput.style.cssText = "width: 100%;";
slugInput.value = this.category?.spec.slug || "";
slugInput.placeholder = i18next.t("category_manager_modal.prompt_slug");
// Priority field
const priorityGroup = form.createDiv("halo-form-group");
const priorityLabel = priorityGroup.createEl("label", { text: i18next.t("category_manager_modal.column_priority") });
priorityLabel.className = "halo-form-label";
const priorityInput = priorityGroup.createEl("input", { type: "number" }) as HTMLInputElement;
priorityInput.className = "halo-input";
priorityInput.style.cssText = "width: 100%;";
priorityInput.value = String(this.category?.spec.priority ?? 0);
priorityInput.placeholder = i18next.t("category_manager_modal.prompt_priority");
// Footer buttons
const footer = content.createDiv("halo-modal-footer");
footer.style.cssText = "margin-top: var(--space-4);";
const cancelBtn = footer.createEl("button", { text: i18next.t("common.button_cancel") });
cancelBtn.className = "halo-btn halo-btn-secondary";
cancelBtn.addEventListener("click", () => this.close());
const submitBtn = footer.createEl("button", { text: this.isEdit ? i18next.t("common.button_save") : i18next.t("category_manager_modal.button_create") });
submitBtn.className = "halo-btn halo-btn-primary";
submitBtn.addEventListener("click", () => {
const name = nameInput.value.trim();
const slug = slugInput.value.trim() || this.generateSlug(name);
const priority = parseInt(priorityInput.value) || 0;
if (!name) {
nameInput.classList.add("error");
return;
}
this.onSubmit({ name, slug, priority });
this.close();
});
// Auto-generate slug on name change
nameInput.addEventListener("input", () => {
if (!this.isEdit || !this.category) {
slugInput.value = this.generateSlug(nameInput.value);
}
});
modalEl.addEventListener("click", (e) => {
if (e.target === modalEl) this.close();
});
nameInput.focus();
}
private generateSlug(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9一-龥]+/g, "-")
.replace(/^-|-$/g, "");
}
}
class CategoryManagerModal extends Modal { class CategoryManagerModal extends Modal {
private categories: any[] = []; private categories: Array<{ metadata: { name: string }; spec: { displayName: string; slug: string; priority: number } }> = [];
private loading = true; private loading = true;
constructor( constructor(
@@ -26,34 +163,37 @@ class CategoryManagerModal extends Modal {
contentEl.createEl("h2", { contentEl.createEl("h2", {
text: i18next.t("category_manager_modal.title"), text: i18next.t("category_manager_modal.title"),
cls: "modal-title"
}); });
const actionsEl = contentEl.createDiv("category-manager-actions"); const actionsEl = contentEl.createDiv("category-manager-actions");
actionsEl.style.cssText = "display: flex; justify-content: space-between; align-items: center; padding: var(--space-4);";
new Setting(actionsEl) const createBtn = actionsEl.createEl("button", {
.addButton((button) => { text: i18next.t("category_manager_modal.button_create"),
button cls: "halo-btn halo-btn-primary"
.setButtonText(i18next.t("category_manager_modal.button_create")) });
.setCta() createBtn.addEventListener("click", () => this.showCreateCategoryForm());
.onClick(() => {
this.showCreateCategoryDialog(); const closeBtn = actionsEl.createEl("button", {
}); text: i18next.t("common.button_close"),
}) cls: "halo-btn halo-btn-secondary"
.addButton((button) => { });
button closeBtn.addEventListener("click", () => this.close());
.setButtonText(i18next.t("common.button_close"))
.onClick(() => this.close());
});
const listEl = contentEl.createDiv("category-list"); const listEl = contentEl.createDiv("category-list");
listEl.createEl("p", { text: i18next.t("category_manager_modal.loading") }); listEl.style.cssText = "padding: 0 var(--space-4) var(--space-4);";
const loadingEl = listEl.createDiv("halo-empty");
loadingEl.style.cssText = "padding: var(--space-6);";
loadingEl.innerHTML = `<div class="halo-spinner"></div><p style="margin-top: var(--space-3); color: var(--text-muted);">${i18next.t("category_manager_modal.loading")}</p>`;
try { try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site); const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
this.categories = await service.getCategories(); this.categories = await service.getCategories();
this.loading = false; this.loading = false;
this.renderCategoryList(); this.renderCategoryList();
} catch (error) { } catch {
new Notice(i18next.t("common.error_connection_failed")); new Notice(i18next.t("common.error_connection_failed"));
this.close(); this.close();
} }
@@ -68,107 +208,134 @@ class CategoryManagerModal extends Modal {
listEl.empty(); listEl.empty();
if (this.categories.length === 0) { if (this.categories.length === 0) {
listEl.createEl("p", { text: i18next.t("category_manager_modal.empty") }); const emptyEl = listEl.createDiv("halo-empty");
emptyEl.innerHTML = `
<div style="font-size: 48px; opacity: 0.5;">📁</div>
<p style="margin-top: var(--space-3); font-weight: 500;">${i18next.t("category_manager_modal.empty")}</p>
`;
return; return;
} }
const table = listEl.createEl("table", { cls: "category-table" }); const table = listEl.createEl("table", { cls: "halo-table" });
const header = table.createEl("tr"); const header = table.createEl("thead").createEl("tr");
header.createEl("th", { text: i18next.t("category_manager_modal.column_name") }); header.createEl("th", { text: i18next.t("category_manager_modal.column_name") });
header.createEl("th", { text: i18next.t("category_manager_modal.column_slug") }); header.createEl("th", { text: i18next.t("category_manager_modal.column_slug") });
header.createEl("th", { text: i18next.t("category_manager_modal.column_priority") }); header.createEl("th", { text: i18next.t("category_manager_modal.column_priority") });
header.createEl("th", { text: i18next.t("category_manager_modal.column_actions") }); header.createEl("th", { text: i18next.t("category_manager_modal.column_actions"), cls: "text-right" });
const tbody = table.createEl("tbody");
for (const category of this.categories) { for (const category of this.categories) {
const row = table.createEl("tr"); const row = tbody.createEl("tr");
const nameCell = row.createEl("td");
nameCell.style.cssText = "font-weight: 500;";
nameCell.textContent = category.spec.displayName;
row.createEl("td", { text: category.spec.displayName });
row.createEl("td", { text: category.spec.slug }); row.createEl("td", { text: category.spec.slug });
row.createEl("td", { text: String(category.spec.priority || 0) });
const priorityCell = row.createEl("td");
const priorityBadge = priorityCell.createEl("span", { text: String(category.spec.priority || 0) });
priorityBadge.style.cssText = `
display: inline-flex; align-items: center; justify-content: center;
min-width: 28px; height: 22px; padding: 0 8px;
background: var(--background-secondary); border-radius: var(--radius-full);
font-size: var(--font-size-xs); font-weight: 500;
`;
const actionsCell = row.createEl("td"); const actionsCell = row.createEl("td");
actionsCell.createEl("button", { actionsCell.style.cssText = "text-align: right;";
const editBtn = actionsCell.createEl("button", {
text: i18next.t("category_manager_modal.button_edit"), text: i18next.t("category_manager_modal.button_edit"),
cls: "category-action-btn", cls: "halo-btn halo-btn-sm halo-btn-ghost"
}).addEventListener("click", () => {
this.showEditCategoryDialog(category);
}); });
editBtn.addEventListener("click", () => this.showEditCategoryForm(category));
actionsCell.createEl("button", { const deleteBtn = actionsCell.createEl("button", {
text: i18next.t("category_manager_modal.button_delete"), text: i18next.t("category_manager_modal.button_delete"),
cls: "category-action-btn danger", cls: "halo-btn halo-btn-sm halo-btn-ghost"
}).addEventListener("click", () => {
this.showDeleteCategoryDialog(category);
}); });
deleteBtn.style.cssText += "color: var(--halo-danger);";
deleteBtn.addEventListener("click", () => this.showDeleteCategoryDialog(category));
} }
} }
private async showCreateCategoryDialog() { private showCreateCategoryForm() {
const name = prompt(i18next.t("category_manager_modal.prompt_name")); new CategoryFormModal(this.plugin, this.site, null, async (data) => {
if (!name) return; try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
const slug = prompt(i18next.t("category_manager_modal.prompt_slug"), this.generateSlug(name)); await service.createCategory(data.name, data.slug, data.priority);
if (!slug) return; new Notice(i18next.t("category_manager_modal.notice_create_success"));
this.categories = await service.getCategories();
const priority = prompt(i18next.t("category_manager_modal.prompt_priority"), "0"); this.renderCategoryList();
if (priority === null) return; } catch {
new Notice(i18next.t("category_manager_modal.error_create_failed"));
try { }
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site); });
await service.createCategory(name, slug, parseInt(priority) || 0);
new Notice(i18next.t("category_manager_modal.notice_create_success"));
this.categories = await service.getCategories();
this.renderCategoryList();
} catch (error) {
new Notice(i18next.t("category_manager_modal.error_create_failed"));
}
} }
private async showEditCategoryDialog(category: any) { private showEditCategoryForm(category: typeof this.categories[0]) {
const name = prompt(i18next.t("category_manager_modal.prompt_name"), category.spec.displayName); new CategoryFormModal(this.plugin, this.site, category, async (data) => {
if (name === null) return; try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
const slug = prompt(i18next.t("category_manager_modal.prompt_slug"), category.spec.slug); await service.updateCategory(category.metadata.name, data.name, data.slug, data.priority);
if (slug === null) return; new Notice(i18next.t("category_manager_modal.notice_update_success"));
this.categories = await service.getCategories();
const priority = prompt(i18next.t("category_manager_modal.prompt_priority"), String(category.spec.priority || 0)); this.renderCategoryList();
if (priority === null) return; } catch {
new Notice(i18next.t("category_manager_modal.error_update_failed"));
try { }
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site); });
await service.updateCategory(category.metadata.name, name, slug, parseInt(priority) || 0);
new Notice(i18next.t("category_manager_modal.notice_update_success"));
this.categories = await service.getCategories();
this.renderCategoryList();
} catch (error) {
new Notice(i18next.t("category_manager_modal.error_update_failed"));
}
} }
private async showDeleteCategoryDialog(category: any) { private showDeleteCategoryDialog(category: typeof this.categories[0]) {
if (!confirm(i18next.t("category_manager_modal.confirm_delete", { name: category.spec.displayName }))) { const modalEl = document.createElement("div");
return; modalEl.style.cssText = `
} position: fixed; inset: 0; z-index: 1000;
display: flex; align-items: center; justify-content: center;
background: rgba(0,0,0,0.5); -webkit-backdrop-filter: blur(4px);
`;
try { const content = modalEl.createDiv("halo-card");
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site); content.style.cssText = "width: 100%; max-width: 400px; padding: var(--space-6);";
await service.deleteCategory(category.metadata.name);
new Notice(i18next.t("category_manager_modal.notice_delete_success"));
this.categories = await service.getCategories(); const title = content.createEl("h3", {
this.renderCategoryList(); text: i18next.t("category_manager_modal.confirm_delete", { name: category.spec.displayName })
} catch (error) { });
new Notice(i18next.t("category_manager_modal.error_delete_failed")); title.style.cssText = "font-size: 16px; font-weight: 600; margin: 0 0 var(--space-4) 0;";
}
}
private generateSlug(name: string): string { const actions = content.createDiv("halo-modal-footer");
return name actions.style.cssText = "margin-top: var(--space-4);";
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-") const cancelBtn = actions.createEl("button", {
.replace(/^-|-$/g, ""); text: i18next.t("common.button_cancel"),
cls: "halo-btn halo-btn-secondary"
});
cancelBtn.addEventListener("click", () => modalEl.remove());
const deleteBtn = actions.createEl("button", {
text: i18next.t("common.button_delete"),
cls: "halo-btn halo-btn-danger"
});
deleteBtn.addEventListener("click", async () => {
modalEl.remove();
try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
await service.deleteCategory(category.metadata.name);
new Notice(i18next.t("category_manager_modal.notice_delete_success"));
this.categories = await service.getCategories();
this.renderCategoryList();
} catch {
new Notice(i18next.t("category_manager_modal.error_delete_failed"));
}
});
modalEl.addEventListener("click", (e) => {
if (e.target === modalEl) modalEl.remove();
});
document.body.appendChild(modalEl);
} }
onClose() { onClose() {
+302 -95
View File
@@ -9,8 +9,191 @@ export function openTagManagerModal(plugin: HaloPlugin, site: HaloSite): void {
modal.open(); modal.open();
} }
interface TagFormData {
name: string;
slug: string;
color: string;
}
class TagFormModal extends Modal {
private tag: { metadata: { name: string }; spec: { displayName: string; slug: string; color: string } } | null = null;
private isEdit = false;
private onSubmit: (data: TagFormData) => void;
constructor(
private plugin: HaloPlugin,
private site: HaloSite,
tag: TagFormModal["tag"],
private onSubmitFn: (data: TagFormData) => void
) {
super(app);
this.tag = tag;
this.isEdit = !!tag;
this.onSubmit = onSubmitFn;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
const modalEl = contentEl.createDiv("halo-modal");
modalEl.style.cssText = `
position: fixed; inset: 0; z-index: 1000;
display: flex; align-items: center; justify-content: center;
background: rgba(0,0,0,0.5); -webkit-backdrop-filter: blur(4px);
`;
const content = modalEl.createDiv("halo-card");
content.style.cssText = `
width: 100%; max-width: 440px; max-height: 90vh; overflow-y: auto;
padding: var(--space-6);
`;
const header = content.createDiv("halo-modal-header");
header.style.cssText = "margin-bottom: var(--space-4);";
const title = header.createEl("h3");
title.textContent = this.isEdit
? i18next.t("tag_manager_modal.title_edit")
: i18next.t("tag_manager_modal.title_create");
title.style.cssText = "font-size: 18px; font-weight: 600; margin: 0;";
const closeBtn = header.createEl("button", { text: "✕" });
closeBtn.className = "halo-modal-close";
closeBtn.style.cssText = `
display: flex; align-items: center; justify-content: center;
width: 32px; height: 32px; background: transparent; border: none;
border-radius: 8px; color: var(--text-muted); cursor: pointer; font-size: 16px;
`;
closeBtn.addEventListener("click", () => this.close());
const form = content.createDiv("halo-modal-body");
// Name field
const nameGroup = form.createDiv("halo-form-group");
const nameLabel = nameGroup.createEl("label", { text: i18next.t("tag_manager_modal.column_name") });
nameLabel.className = "halo-form-label";
const nameInput = nameGroup.createEl("input", { type: "text" }) as HTMLInputElement;
nameInput.className = "halo-input";
nameInput.style.cssText = "width: 100%;";
nameInput.value = this.tag?.spec.displayName || "";
nameInput.placeholder = i18next.t("tag_manager_modal.prompt_name");
// Slug field
const slugGroup = form.createDiv("halo-form-group");
const slugLabel = slugGroup.createEl("label", { text: i18next.t("tag_manager_modal.column_slug") });
slugLabel.className = "halo-form-label";
const slugInput = slugGroup.createEl("input", { type: "text" }) as HTMLInputElement;
slugInput.className = "halo-input";
slugInput.style.cssText = "width: 100%;";
slugInput.value = this.tag?.spec.slug || "";
slugInput.placeholder = i18next.t("tag_manager_modal.prompt_slug");
// Color field
const colorGroup = form.createDiv("halo-form-group");
const colorLabel = colorGroup.createEl("label", { text: i18next.t("tag_manager_modal.column_color") });
colorLabel.className = "halo-form-label";
const colorPicker = colorGroup.createDiv("halo-color-picker");
colorPicker.style.cssText = "display: flex; align-items: center; gap: var(--space-3);";
const colorPreview = colorPicker.createEl("input", { type: "color" }) as HTMLInputElement;
colorPreview.style.cssText = `
width: 40px; height: 36px; padding: 2px; border: 1px solid var(--border-color);
border-radius: var(--radius-md); cursor: pointer; background: transparent;
`;
colorPreview.value = this.tag?.spec.color || "#4A90E2";
const colorInput = colorPicker.createEl("input", { type: "text" }) as HTMLInputElement;
colorInput.className = "halo-input halo-color-input";
colorInput.style.cssText = "flex: 1; font-family: monospace;";
colorInput.value = this.tag?.spec.color || "#4A90E2";
// Color preview update
colorPreview.addEventListener("input", () => {
colorInput.value = colorPreview.value;
});
colorInput.addEventListener("input", () => {
if (/^#[0-9A-Fa-f]{6}$/.test(colorInput.value)) {
colorPreview.value = colorInput.value;
}
});
// Preset colors
const presets = colorGroup.createDiv("halo-color-presets");
presets.style.cssText = "display: flex; gap: var(--space-2); margin-top: var(--space-2); flex-wrap: wrap;";
const presetColors = ["#4A90E2", "#67C23A", "#E6A23C", "#F56C6C", "#909399", "#8E44AD", "#3498DB", "#1ABC9C"];
for (const color of presetColors) {
const preset = presets.createEl("button");
preset.style.cssText = `
width: 24px; height: 24px; border-radius: var(--radius-sm);
background: ${color}; border: 2px solid transparent;
cursor: pointer; transition: all var(--transition-fast);
`;
preset.addEventListener("click", () => {
colorPreview.value = color;
colorInput.value = color;
});
preset.addEventListener("mouseenter", () => {
preset.style.transform = "scale(1.15)";
preset.style.borderColor = "var(--interactive-accent)";
});
preset.addEventListener("mouseleave", () => {
preset.style.transform = "scale(1)";
preset.style.borderColor = "transparent";
});
}
// Footer buttons
const footer = content.createDiv("halo-modal-footer");
footer.style.cssText = "margin-top: var(--space-4);";
const cancelBtn = footer.createEl("button", { text: i18next.t("common.button_cancel") });
cancelBtn.className = "halo-btn halo-btn-secondary";
cancelBtn.addEventListener("click", () => this.close());
const submitBtn = footer.createEl("button", { text: this.isEdit ? i18next.t("common.button_save") : i18next.t("tag_manager_modal.button_create") });
submitBtn.className = "halo-btn halo-btn-primary";
submitBtn.addEventListener("click", () => {
const name = nameInput.value.trim();
const slug = slugInput.value.trim() || this.generateSlug(name);
const color = colorInput.value.trim() || "#4A90E2";
if (!name) {
nameInput.classList.add("error");
return;
}
this.onSubmit({ name, slug, color });
this.close();
});
// Auto-generate slug on name change
nameInput.addEventListener("input", () => {
if (!this.isEdit || !this.tag) {
slugInput.value = this.generateSlug(nameInput.value);
}
});
modalEl.addEventListener("click", (e) => {
if (e.target === modalEl) this.close();
});
nameInput.focus();
}
private generateSlug(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9一-龥]+/g, "-")
.replace(/^-|-$/g, "");
}
}
class TagManagerModal extends Modal { class TagManagerModal extends Modal {
private tags: any[] = []; private tags: Array<{ metadata: { name: string }; spec: { displayName: string; slug: string; color: string } }> = [];
private loading = true; private loading = true;
constructor( constructor(
@@ -26,34 +209,37 @@ class TagManagerModal extends Modal {
contentEl.createEl("h2", { contentEl.createEl("h2", {
text: i18next.t("tag_manager_modal.title"), text: i18next.t("tag_manager_modal.title"),
cls: "modal-title"
}); });
const actionsEl = contentEl.createDiv("tag-manager-actions"); const actionsEl = contentEl.createDiv("tag-manager-actions");
actionsEl.style.cssText = "display: flex; justify-content: space-between; align-items: center; padding: var(--space-4);";
new Setting(actionsEl) const createBtn = actionsEl.createEl("button", {
.addButton((button) => { text: i18next.t("tag_manager_modal.button_create"),
button cls: "halo-btn halo-btn-primary"
.setButtonText(i18next.t("tag_manager_modal.button_create")) });
.setCta() createBtn.addEventListener("click", () => this.showCreateTagForm());
.onClick(() => {
this.showCreateTagDialog(); const closeBtn = actionsEl.createEl("button", {
}); text: i18next.t("common.button_close"),
}) cls: "halo-btn halo-btn-secondary"
.addButton((button) => { });
button closeBtn.addEventListener("click", () => this.close());
.setButtonText(i18next.t("common.button_close"))
.onClick(() => this.close());
});
const listEl = contentEl.createDiv("tag-list"); const listEl = contentEl.createDiv("tag-list");
listEl.createEl("p", { text: i18next.t("tag_manager_modal.loading") }); listEl.style.cssText = "padding: 0 var(--space-4) var(--space-4);";
const loadingEl = listEl.createDiv("halo-empty");
loadingEl.style.cssText = "padding: var(--space-6);";
loadingEl.innerHTML = `<div class="halo-spinner"></div><p style="margin-top: var(--space-3); color: var(--text-muted);">${i18next.t("tag_manager_modal.loading")}</p>`;
try { try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site); const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
this.tags = await service.getTags(); this.tags = await service.getTags();
this.loading = false; this.loading = false;
this.renderTagList(); this.renderTagList();
} catch (error) { } catch {
new Notice(i18next.t("common.error_connection_failed")); new Notice(i18next.t("common.error_connection_failed"));
this.close(); this.close();
} }
@@ -68,113 +254,134 @@ class TagManagerModal extends Modal {
listEl.empty(); listEl.empty();
if (this.tags.length === 0) { if (this.tags.length === 0) {
listEl.createEl("p", { text: i18next.t("tag_manager_modal.empty") }); const emptyEl = listEl.createDiv("halo-empty");
emptyEl.innerHTML = `
<div style="font-size: 48px; opacity: 0.5;">🏷</div>
<p style="margin-top: var(--space-3); font-weight: 500;">${i18next.t("tag_manager_modal.empty")}</p>
`;
return; return;
} }
const table = listEl.createEl("table", { cls: "tag-table" }); const table = listEl.createEl("table", { cls: "halo-table" });
const header = table.createEl("tr"); const header = table.createEl("thead").createEl("tr");
header.createEl("th", { text: i18next.t("tag_manager_modal.column_name") }); header.createEl("th", { text: i18next.t("tag_manager_modal.column_name") });
header.createEl("th", { text: i18next.t("tag_manager_modal.column_slug") }); header.createEl("th", { text: i18next.t("tag_manager_modal.column_slug") });
header.createEl("th", { text: i18next.t("tag_manager_modal.column_color") }); header.createEl("th", { text: i18next.t("tag_manager_modal.column_color") });
header.createEl("th", { text: i18next.t("tag_manager_modal.column_actions") }); header.createEl("th", { text: i18next.t("tag_manager_modal.column_actions"), cls: "text-right" });
const tbody = table.createEl("tbody");
for (const tag of this.tags) { for (const tag of this.tags) {
const row = table.createEl("tr"); const row = tbody.createEl("tr");
const nameCell = row.createEl("td");
nameCell.style.cssText = "font-weight: 500;";
nameCell.textContent = tag.spec.displayName;
row.createEl("td", { text: tag.spec.displayName });
row.createEl("td", { text: tag.spec.slug }); row.createEl("td", { text: tag.spec.slug });
const colorCell = row.createEl("td"); const colorCell = row.createEl("td");
colorCell.createEl("span", { const colorBadge = colorCell.createEl("span", { text: " " });
text: tag.spec.color || "#ffffff", colorBadge.style.cssText = `
cls: "tag-color-preview", display: inline-block; width: 16px; height: 16px;
attr: { style: `background-color: ${tag.spec.color || "#ffffff"}` } background: ${tag.spec.color || "#ffffff"}; border-radius: var(--radius-sm);
}); border: 1px solid var(--border-color); vertical-align: middle;
`;
colorCell.createSpan({ text: ` ${tag.spec.color || "#ffffff"}` });
const actionsCell = row.createEl("td"); const actionsCell = row.createEl("td");
actionsCell.createEl("button", { actionsCell.style.cssText = "text-align: right;";
const editBtn = actionsCell.createEl("button", {
text: i18next.t("tag_manager_modal.button_edit"), text: i18next.t("tag_manager_modal.button_edit"),
cls: "tag-action-btn", cls: "halo-btn halo-btn-sm halo-btn-ghost"
}).addEventListener("click", () => {
this.showEditTagDialog(tag);
}); });
editBtn.addEventListener("click", () => this.showEditTagForm(tag));
actionsCell.createEl("button", { const deleteBtn = actionsCell.createEl("button", {
text: i18next.t("tag_manager_modal.button_delete"), text: i18next.t("tag_manager_modal.button_delete"),
cls: "tag-action-btn danger", cls: "halo-btn halo-btn-sm halo-btn-ghost"
}).addEventListener("click", () => {
this.showDeleteTagDialog(tag);
}); });
deleteBtn.style.cssText += "color: var(--halo-danger);";
deleteBtn.addEventListener("click", () => this.showDeleteTagDialog(tag));
} }
} }
private async showCreateTagDialog() { private showCreateTagForm() {
const name = prompt(i18next.t("tag_manager_modal.prompt_name")); new TagFormModal(this.plugin, this.site, null, async (data) => {
if (!name) return; try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
const slug = prompt(i18next.t("tag_manager_modal.prompt_slug"), this.generateSlug(name)); await service.createTag(data.name, data.slug, data.color);
if (!slug) return; new Notice(i18next.t("tag_manager_modal.notice_create_success"));
this.tags = await service.getTags();
const color = prompt(i18next.t("tag_manager_modal.prompt_color"), "#4A90E2"); this.renderTagList();
if (!color) return; } catch {
new Notice(i18next.t("tag_manager_modal.error_create_failed"));
try { }
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site); });
await service.createTag(name, slug, color);
new Notice(i18next.t("tag_manager_modal.notice_create_success"));
this.tags = await service.getTags();
this.renderTagList();
} catch (error) {
new Notice(i18next.t("tag_manager_modal.error_create_failed"));
}
} }
private async showEditTagDialog(tag: any) { private showEditTagForm(tag: typeof this.tags[0]) {
const name = prompt(i18next.t("tag_manager_modal.prompt_name"), tag.spec.displayName); new TagFormModal(this.plugin, this.site, tag, async (data) => {
if (name === null) return; try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
const slug = prompt(i18next.t("tag_manager_modal.prompt_slug"), tag.spec.slug); await service.updateTag(tag.metadata.name, data.name, data.slug, data.color);
if (slug === null) return; new Notice(i18next.t("tag_manager_modal.notice_update_success"));
this.tags = await service.getTags();
const color = prompt(i18next.t("tag_manager_modal.prompt_color"), tag.spec.color || "#4A90E2"); this.renderTagList();
if (color === null) return; } catch {
new Notice(i18next.t("tag_manager_modal.error_update_failed"));
try { }
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site); });
await service.updateTag(tag.metadata.name, name, slug, color);
new Notice(i18next.t("tag_manager_modal.notice_update_success"));
this.tags = await service.getTags();
this.renderTagList();
} catch (error) {
new Notice(i18next.t("tag_manager_modal.error_update_failed"));
}
} }
private async showDeleteTagDialog(tag: any) { private showDeleteTagDialog(tag: typeof this.tags[0]) {
if (!confirm(i18next.t("tag_manager_modal.confirm_delete", { name: tag.spec.displayName }))) { const modalEl = document.createElement("div");
return; modalEl.style.cssText = `
} position: fixed; inset: 0; z-index: 1000;
display: flex; align-items: center; justify-content: center;
background: rgba(0,0,0,0.5); -webkit-backdrop-filter: blur(4px);
`;
try { const content = modalEl.createDiv("halo-card");
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site); content.style.cssText = "width: 100%; max-width: 400px; padding: var(--space-6);";
await service.deleteTag(tag.metadata.name);
new Notice(i18next.t("tag_manager_modal.notice_delete_success"));
this.tags = await service.getTags(); const title = content.createEl("h3", {
this.renderTagList(); text: i18next.t("tag_manager_modal.confirm_delete", { name: tag.spec.displayName })
} catch (error) { });
new Notice(i18next.t("tag_manager_modal.error_delete_failed")); title.style.cssText = "font-size: 16px; font-weight: 600; margin: 0 0 var(--space-4) 0;";
}
}
private generateSlug(name: string): string { const actions = content.createDiv("halo-modal-footer");
return name actions.style.cssText = "margin-top: var(--space-4);";
.toLowerCase()
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-") const cancelBtn = actions.createEl("button", {
.replace(/^-|-$/g, ""); text: i18next.t("common.button_cancel"),
cls: "halo-btn halo-btn-secondary"
});
cancelBtn.addEventListener("click", () => modalEl.remove());
const deleteBtn = actions.createEl("button", {
text: i18next.t("common.button_delete"),
cls: "halo-btn halo-btn-danger"
});
deleteBtn.addEventListener("click", async () => {
modalEl.remove();
try {
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
await service.deleteTag(tag.metadata.name);
new Notice(i18next.t("tag_manager_modal.notice_delete_success"));
this.tags = await service.getTags();
this.renderTagList();
} catch {
new Notice(i18next.t("tag_manager_modal.error_delete_failed"));
}
});
modalEl.addEventListener("click", (e) => {
if (e.target === modalEl) modalEl.remove();
});
document.body.appendChild(modalEl);
} }
onClose() { onClose() {
+1076 -118
View File
File diff suppressed because it is too large Load Diff