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:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"commit-commands@claude-plugins-official": true
|
||||
}
|
||||
}
|
||||
Vendored
+123
-23
File diff suppressed because one or more lines are too long
Vendored
+1221
-4
File diff suppressed because it is too large
Load Diff
Vendored
+1
-2
@@ -156,13 +156,12 @@
|
||||
"state": {
|
||||
"type": "outline",
|
||||
"state": {
|
||||
"file": "博客/Git团队协作指南(精简版).md",
|
||||
"followCursor": true,
|
||||
"showSearch": true,
|
||||
"searchQuery": ""
|
||||
},
|
||||
"icon": "lucide-list",
|
||||
"title": "Git团队协作指南(精简版) 的大纲"
|
||||
"title": "大纲"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Vendored
+18
@@ -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
|
||||
}
|
||||
Vendored
+24
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+58
-1
@@ -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
|
||||
}
|
||||
@@ -196,7 +196,11 @@
|
||||
},
|
||||
"tag_manager_modal": {
|
||||
"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...",
|
||||
"empty": "No tags",
|
||||
"column_name": "Name",
|
||||
@@ -218,7 +222,11 @@
|
||||
},
|
||||
"category_manager_modal": {
|
||||
"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...",
|
||||
"empty": "No categories",
|
||||
"column_name": "Name",
|
||||
|
||||
@@ -196,7 +196,11 @@
|
||||
},
|
||||
"tag_manager_modal": {
|
||||
"title": "标签管理",
|
||||
"button_create": "创建标签",
|
||||
"title_create": "创建标签",
|
||||
"title_edit": "编辑标签",
|
||||
"button_create": "创建",
|
||||
"button_save": "保存",
|
||||
"button_cancel": "取消",
|
||||
"loading": "加载中...",
|
||||
"empty": "暂无标签",
|
||||
"column_name": "名称",
|
||||
@@ -218,7 +222,11 @@
|
||||
},
|
||||
"category_manager_modal": {
|
||||
"title": "分类管理",
|
||||
"button_create": "创建分类",
|
||||
"title_create": "创建分类",
|
||||
"title_edit": "编辑分类",
|
||||
"button_create": "创建",
|
||||
"button_save": "保存",
|
||||
"button_cancel": "取消",
|
||||
"loading": "加载中...",
|
||||
"empty": "暂无分类",
|
||||
"column_name": "名称",
|
||||
|
||||
@@ -196,7 +196,11 @@
|
||||
},
|
||||
"tag_manager_modal": {
|
||||
"title": "標籤管理",
|
||||
"button_create": "創建標籤",
|
||||
"title_create": "創建標籤",
|
||||
"title_edit": "編輯標籤",
|
||||
"button_create": "創建",
|
||||
"button_save": "保存",
|
||||
"button_cancel": "取消",
|
||||
"loading": "加載中...",
|
||||
"empty": "暫無標籤",
|
||||
"column_name": "名稱",
|
||||
@@ -218,7 +222,11 @@
|
||||
},
|
||||
"category_manager_modal": {
|
||||
"title": "分類管理",
|
||||
"button_create": "創建分類",
|
||||
"title_create": "創建分類",
|
||||
"title_edit": "編輯分類",
|
||||
"button_create": "創建",
|
||||
"button_save": "保存",
|
||||
"button_cancel": "取消",
|
||||
"loading": "加載中...",
|
||||
"empty": "暫無分類",
|
||||
"column_name": "名稱",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import i18next from "i18next";
|
||||
import { Modal, Notice, Setting } from "obsidian";
|
||||
import { Modal, Notice } from "obsidian";
|
||||
import type HaloPlugin from "../main";
|
||||
import type { HaloSite } from "../settings";
|
||||
import HaloService from "../service";
|
||||
@@ -9,8 +9,145 @@ export function openCategoryManagerModal(plugin: HaloPlugin, site: HaloSite): vo
|
||||
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 {
|
||||
private categories: any[] = [];
|
||||
private categories: Array<{ metadata: { name: string }; spec: { displayName: string; slug: string; priority: number } }> = [];
|
||||
private loading = true;
|
||||
|
||||
constructor(
|
||||
@@ -26,34 +163,37 @@ class CategoryManagerModal extends Modal {
|
||||
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("category_manager_modal.title"),
|
||||
cls: "modal-title"
|
||||
});
|
||||
|
||||
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)
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("category_manager_modal.button_create"))
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.showCreateCategoryDialog();
|
||||
const createBtn = actionsEl.createEl("button", {
|
||||
text: i18next.t("category_manager_modal.button_create"),
|
||||
cls: "halo-btn halo-btn-primary"
|
||||
});
|
||||
})
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("common.button_close"))
|
||||
.onClick(() => this.close());
|
||||
createBtn.addEventListener("click", () => this.showCreateCategoryForm());
|
||||
|
||||
const closeBtn = actionsEl.createEl("button", {
|
||||
text: i18next.t("common.button_close"),
|
||||
cls: "halo-btn halo-btn-secondary"
|
||||
});
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
|
||||
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 {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
this.categories = await service.getCategories();
|
||||
this.loading = false;
|
||||
this.renderCategoryList();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
this.close();
|
||||
}
|
||||
@@ -68,107 +208,134 @@ class CategoryManagerModal extends Modal {
|
||||
listEl.empty();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const table = listEl.createEl("table", { cls: "category-table" });
|
||||
const header = table.createEl("tr");
|
||||
const table = listEl.createEl("table", { cls: "halo-table" });
|
||||
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_slug") });
|
||||
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) {
|
||||
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: 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");
|
||||
actionsCell.createEl("button", {
|
||||
actionsCell.style.cssText = "text-align: right;";
|
||||
|
||||
const editBtn = actionsCell.createEl("button", {
|
||||
text: i18next.t("category_manager_modal.button_edit"),
|
||||
cls: "category-action-btn",
|
||||
}).addEventListener("click", () => {
|
||||
this.showEditCategoryDialog(category);
|
||||
cls: "halo-btn halo-btn-sm halo-btn-ghost"
|
||||
});
|
||||
editBtn.addEventListener("click", () => this.showEditCategoryForm(category));
|
||||
|
||||
actionsCell.createEl("button", {
|
||||
const deleteBtn = actionsCell.createEl("button", {
|
||||
text: i18next.t("category_manager_modal.button_delete"),
|
||||
cls: "category-action-btn danger",
|
||||
}).addEventListener("click", () => {
|
||||
this.showDeleteCategoryDialog(category);
|
||||
cls: "halo-btn halo-btn-sm halo-btn-ghost"
|
||||
});
|
||||
deleteBtn.style.cssText += "color: var(--halo-danger);";
|
||||
deleteBtn.addEventListener("click", () => this.showDeleteCategoryDialog(category));
|
||||
}
|
||||
}
|
||||
|
||||
private async showCreateCategoryDialog() {
|
||||
const name = prompt(i18next.t("category_manager_modal.prompt_name"));
|
||||
if (!name) return;
|
||||
|
||||
const slug = prompt(i18next.t("category_manager_modal.prompt_slug"), this.generateSlug(name));
|
||||
if (!slug) return;
|
||||
|
||||
const priority = prompt(i18next.t("category_manager_modal.prompt_priority"), "0");
|
||||
if (priority === null) return;
|
||||
|
||||
private showCreateCategoryForm() {
|
||||
new CategoryFormModal(this.plugin, this.site, null, async (data) => {
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.createCategory(name, slug, parseInt(priority) || 0);
|
||||
await service.createCategory(data.name, data.slug, data.priority);
|
||||
new Notice(i18next.t("category_manager_modal.notice_create_success"));
|
||||
|
||||
this.categories = await service.getCategories();
|
||||
this.renderCategoryList();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
new Notice(i18next.t("category_manager_modal.error_create_failed"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async showEditCategoryDialog(category: any) {
|
||||
const name = prompt(i18next.t("category_manager_modal.prompt_name"), category.spec.displayName);
|
||||
if (name === null) return;
|
||||
|
||||
const slug = prompt(i18next.t("category_manager_modal.prompt_slug"), category.spec.slug);
|
||||
if (slug === null) return;
|
||||
|
||||
const priority = prompt(i18next.t("category_manager_modal.prompt_priority"), String(category.spec.priority || 0));
|
||||
if (priority === null) return;
|
||||
|
||||
private showEditCategoryForm(category: typeof this.categories[0]) {
|
||||
new CategoryFormModal(this.plugin, this.site, category, async (data) => {
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.updateCategory(category.metadata.name, name, slug, parseInt(priority) || 0);
|
||||
await service.updateCategory(category.metadata.name, data.name, data.slug, data.priority);
|
||||
new Notice(i18next.t("category_manager_modal.notice_update_success"));
|
||||
|
||||
this.categories = await service.getCategories();
|
||||
this.renderCategoryList();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
new Notice(i18next.t("category_manager_modal.error_update_failed"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async showDeleteCategoryDialog(category: any) {
|
||||
if (!confirm(i18next.t("category_manager_modal.confirm_delete", { name: category.spec.displayName }))) {
|
||||
return;
|
||||
}
|
||||
private showDeleteCategoryDialog(category: typeof this.categories[0]) {
|
||||
const modalEl = document.createElement("div");
|
||||
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: 400px; padding: var(--space-6);";
|
||||
|
||||
const title = content.createEl("h3", {
|
||||
text: i18next.t("category_manager_modal.confirm_delete", { name: category.spec.displayName })
|
||||
});
|
||||
title.style.cssText = "font-size: 16px; font-weight: 600; margin: 0 0 var(--space-4) 0;";
|
||||
|
||||
const actions = content.createDiv("halo-modal-footer");
|
||||
actions.style.cssText = "margin-top: var(--space-4);";
|
||||
|
||||
const cancelBtn = actions.createEl("button", {
|
||||
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 (error) {
|
||||
} catch {
|
||||
new Notice(i18next.t("category_manager_modal.error_delete_failed"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
private generateSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
modalEl.addEventListener("click", (e) => {
|
||||
if (e.target === modalEl) modalEl.remove();
|
||||
});
|
||||
|
||||
document.body.appendChild(modalEl);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
|
||||
@@ -9,8 +9,191 @@ export function openTagManagerModal(plugin: HaloPlugin, site: HaloSite): void {
|
||||
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 {
|
||||
private tags: any[] = [];
|
||||
private tags: Array<{ metadata: { name: string }; spec: { displayName: string; slug: string; color: string } }> = [];
|
||||
private loading = true;
|
||||
|
||||
constructor(
|
||||
@@ -26,34 +209,37 @@ class TagManagerModal extends Modal {
|
||||
|
||||
contentEl.createEl("h2", {
|
||||
text: i18next.t("tag_manager_modal.title"),
|
||||
cls: "modal-title"
|
||||
});
|
||||
|
||||
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)
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("tag_manager_modal.button_create"))
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.showCreateTagDialog();
|
||||
const createBtn = actionsEl.createEl("button", {
|
||||
text: i18next.t("tag_manager_modal.button_create"),
|
||||
cls: "halo-btn halo-btn-primary"
|
||||
});
|
||||
})
|
||||
.addButton((button) => {
|
||||
button
|
||||
.setButtonText(i18next.t("common.button_close"))
|
||||
.onClick(() => this.close());
|
||||
createBtn.addEventListener("click", () => this.showCreateTagForm());
|
||||
|
||||
const closeBtn = actionsEl.createEl("button", {
|
||||
text: i18next.t("common.button_close"),
|
||||
cls: "halo-btn halo-btn-secondary"
|
||||
});
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
|
||||
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 {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
this.tags = await service.getTags();
|
||||
this.loading = false;
|
||||
this.renderTagList();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
new Notice(i18next.t("common.error_connection_failed"));
|
||||
this.close();
|
||||
}
|
||||
@@ -68,113 +254,134 @@ class TagManagerModal extends Modal {
|
||||
listEl.empty();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const table = listEl.createEl("table", { cls: "tag-table" });
|
||||
const header = table.createEl("tr");
|
||||
const table = listEl.createEl("table", { cls: "halo-table" });
|
||||
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_slug") });
|
||||
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) {
|
||||
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 });
|
||||
|
||||
const colorCell = row.createEl("td");
|
||||
colorCell.createEl("span", {
|
||||
text: tag.spec.color || "#ffffff",
|
||||
cls: "tag-color-preview",
|
||||
attr: { style: `background-color: ${tag.spec.color || "#ffffff"}` }
|
||||
});
|
||||
const colorBadge = colorCell.createEl("span", { text: " " });
|
||||
colorBadge.style.cssText = `
|
||||
display: inline-block; width: 16px; height: 16px;
|
||||
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");
|
||||
actionsCell.createEl("button", {
|
||||
actionsCell.style.cssText = "text-align: right;";
|
||||
|
||||
const editBtn = actionsCell.createEl("button", {
|
||||
text: i18next.t("tag_manager_modal.button_edit"),
|
||||
cls: "tag-action-btn",
|
||||
}).addEventListener("click", () => {
|
||||
this.showEditTagDialog(tag);
|
||||
cls: "halo-btn halo-btn-sm halo-btn-ghost"
|
||||
});
|
||||
editBtn.addEventListener("click", () => this.showEditTagForm(tag));
|
||||
|
||||
actionsCell.createEl("button", {
|
||||
const deleteBtn = actionsCell.createEl("button", {
|
||||
text: i18next.t("tag_manager_modal.button_delete"),
|
||||
cls: "tag-action-btn danger",
|
||||
}).addEventListener("click", () => {
|
||||
this.showDeleteTagDialog(tag);
|
||||
cls: "halo-btn halo-btn-sm halo-btn-ghost"
|
||||
});
|
||||
deleteBtn.style.cssText += "color: var(--halo-danger);";
|
||||
deleteBtn.addEventListener("click", () => this.showDeleteTagDialog(tag));
|
||||
}
|
||||
}
|
||||
|
||||
private async showCreateTagDialog() {
|
||||
const name = prompt(i18next.t("tag_manager_modal.prompt_name"));
|
||||
if (!name) return;
|
||||
|
||||
const slug = prompt(i18next.t("tag_manager_modal.prompt_slug"), this.generateSlug(name));
|
||||
if (!slug) return;
|
||||
|
||||
const color = prompt(i18next.t("tag_manager_modal.prompt_color"), "#4A90E2");
|
||||
if (!color) return;
|
||||
|
||||
private showCreateTagForm() {
|
||||
new TagFormModal(this.plugin, this.site, null, async (data) => {
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.createTag(name, slug, color);
|
||||
await service.createTag(data.name, data.slug, data.color);
|
||||
new Notice(i18next.t("tag_manager_modal.notice_create_success"));
|
||||
|
||||
this.tags = await service.getTags();
|
||||
this.renderTagList();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
new Notice(i18next.t("tag_manager_modal.error_create_failed"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async showEditTagDialog(tag: any) {
|
||||
const name = prompt(i18next.t("tag_manager_modal.prompt_name"), tag.spec.displayName);
|
||||
if (name === null) return;
|
||||
|
||||
const slug = prompt(i18next.t("tag_manager_modal.prompt_slug"), tag.spec.slug);
|
||||
if (slug === null) return;
|
||||
|
||||
const color = prompt(i18next.t("tag_manager_modal.prompt_color"), tag.spec.color || "#4A90E2");
|
||||
if (color === null) return;
|
||||
|
||||
private showEditTagForm(tag: typeof this.tags[0]) {
|
||||
new TagFormModal(this.plugin, this.site, tag, async (data) => {
|
||||
try {
|
||||
const service = new HaloService(this.plugin.app, this.plugin.settings, this.site);
|
||||
await service.updateTag(tag.metadata.name, name, slug, color);
|
||||
await service.updateTag(tag.metadata.name, data.name, data.slug, data.color);
|
||||
new Notice(i18next.t("tag_manager_modal.notice_update_success"));
|
||||
|
||||
this.tags = await service.getTags();
|
||||
this.renderTagList();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
new Notice(i18next.t("tag_manager_modal.error_update_failed"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async showDeleteTagDialog(tag: any) {
|
||||
if (!confirm(i18next.t("tag_manager_modal.confirm_delete", { name: tag.spec.displayName }))) {
|
||||
return;
|
||||
}
|
||||
private showDeleteTagDialog(tag: typeof this.tags[0]) {
|
||||
const modalEl = document.createElement("div");
|
||||
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: 400px; padding: var(--space-6);";
|
||||
|
||||
const title = content.createEl("h3", {
|
||||
text: i18next.t("tag_manager_modal.confirm_delete", { name: tag.spec.displayName })
|
||||
});
|
||||
title.style.cssText = "font-size: 16px; font-weight: 600; margin: 0 0 var(--space-4) 0;";
|
||||
|
||||
const actions = content.createDiv("halo-modal-footer");
|
||||
actions.style.cssText = "margin-top: var(--space-4);";
|
||||
|
||||
const cancelBtn = actions.createEl("button", {
|
||||
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 (error) {
|
||||
} catch {
|
||||
new Notice(i18next.t("tag_manager_modal.error_delete_failed"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
private generateSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
modalEl.addEventListener("click", (e) => {
|
||||
if (e.target === modalEl) modalEl.remove();
|
||||
});
|
||||
|
||||
document.body.appendChild(modalEl);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
|
||||
+1076
-118
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user