feat(workspace): add custom icon selection for workspaces

- Add icon property to WorkspaceData type for custom workspace icons
- Extract workspace modals to separate module for better organization
- Implement icon selector in create/rename workspace dialogs
- Update workspace selector to display custom icons
- Add dedicated styles for workspace icon UI components
- Support icon inheritance when cloning workspaces
- Enhance workspace settings tab with icon display

This allows users to visually distinguish workspaces beyond names,
improving workspace identification and navigation experience.
This commit is contained in:
Quorafind 2025-09-24 18:48:11 +08:00
parent 6c282f976c
commit b709c90f6f
10 changed files with 556 additions and 530 deletions

View file

@ -1,8 +1,13 @@
import { Menu, Modal, Notice, Setting } from "obsidian";
import { Menu, Setting, setIcon } from "obsidian";
import { TaskProgressBarSettingTab } from "@/setting";
import TaskProgressBarPlugin from "@/index";
import { WorkspaceData } from "@/experimental/v2/types/workspace";
import { t } from "@/translations/helper";
import {
CreateWorkspaceModal,
RenameWorkspaceModal,
DeleteWorkspaceModal,
} from "@/experimental/v2/components/modals/WorkspaceModals";
export function renderWorkspaceSettingsTab(settingTab: TaskProgressBarSettingTab, containerEl: HTMLElement) {
const workspacesSection = containerEl.createDiv();
@ -70,9 +75,15 @@ export function renderWorkspaceSettingsTab(settingTab: TaskProgressBarSettingTab
workspaceItemEl.addClass("workspace-item-active");
}
new Setting(workspaceItemEl)
.setName(workspace.name)
.setDesc(
const setting = new Setting(workspaceItemEl);
// Add workspace icon to the name
const nameWithIcon = setting.nameEl.createDiv({cls: "workspace-name-with-icon"});
const iconEl = nameWithIcon.createDiv({cls: "workspace-list-icon"});
setIcon(iconEl, workspace.icon || "layers");
nameWithIcon.createSpan({text: workspace.name});
setting.setDesc(
isDefaultWs
? t("Default workspace")
: t("Last updated: {{date}}", {
@ -174,237 +185,23 @@ function showWorkspaceSelector(settingTab: TaskProgressBarSettingTab, event: Mou
function showCreateWorkspaceDialog(settingTab: TaskProgressBarSettingTab) {
if (!settingTab.plugin.workspaceManager) return;
new CreateWorkspaceModal(settingTab.plugin, () => {
this.display();
settingTab.display();
}).open();
}
function showRenameWorkspaceDialog(settingTab: TaskProgressBarSettingTab, workspace: WorkspaceData) {
if (!settingTab.plugin.workspaceManager) return;
new RenameWorkspaceModal(settingTab.plugin, workspace, () => {
this.display();
settingTab.display();
}).open();
}
function showDeleteWorkspaceDialog(settingTab: TaskProgressBarSettingTab, workspace: WorkspaceData) {
if (!settingTab.plugin.workspaceManager) return;
new DeleteWorkspaceModal(settingTab.plugin, workspace, () => {
this.display();
settingTab.display();
}).open();
}
class CreateWorkspaceModal extends Modal {
private nameInput: HTMLInputElement;
private baseSelect: HTMLSelectElement;
constructor(
private plugin: TaskProgressBarPlugin,
private onCreated: () => void
) {
super(plugin.app);
}
onOpen() {
const {contentEl} = this;
contentEl.createEl("h2", {text: t("Create New Workspace")});
new Setting(contentEl)
.setName(t("Workspace Name"))
.addText((text) => {
text.setPlaceholder(t("Enter workspace name"));
this.nameInput = text.inputEl;
});
new Setting(contentEl)
.setName(t("Copy Settings From"))
.addDropdown((dropdown) => {
dropdown.addOption("", t("Default settings"));
this.plugin.workspaceManager
?.getAllWorkspaces()
.forEach((ws) => {
dropdown.addOption(ws.id, ws.name);
});
this.baseSelect = dropdown.selectEl;
});
const buttonContainer = contentEl.createDiv({
cls: "modal-button-container",
});
const createButton = buttonContainer.createEl("button", {
text: t("Create"),
cls: "mod-cta",
});
const cancelButton = buttonContainer.createEl("button", {
text: t("Cancel"),
});
createButton.addEventListener("click", async () => {
const name = this.nameInput.value.trim();
const baseId = this.baseSelect.value || undefined;
if (name && this.plugin.workspaceManager) {
console.log("[TG-WORKSPACE] settings:create", {
name,
baseId,
});
await this.plugin.workspaceManager.createWorkspace(
name,
baseId
);
new Notice(t("Workspace created"));
this.onCreated();
this.close();
} else {
new Notice(t("Please enter a workspace name"));
}
});
cancelButton.addEventListener("click", () => {
this.close();
});
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}
class RenameWorkspaceModal extends Modal {
private nameInput: HTMLInputElement;
constructor(
private plugin: TaskProgressBarPlugin,
private workspace: WorkspaceData,
private onRenamed: () => void
) {
super(plugin.app);
}
onOpen() {
const {contentEl} = this;
contentEl.createEl("h2", {text: t("Rename Workspace")});
new Setting(contentEl)
.setName(t("New Name"))
.addText((text) => {
text.setValue(this.workspace.name).setPlaceholder(
t("Enter new name")
);
this.nameInput = text.inputEl;
});
const buttonContainer = contentEl.createDiv({
cls: "modal-button-container",
});
const renameButton = buttonContainer.createEl("button", {
text: t("Rename"),
cls: "mod-cta",
});
const cancelButton = buttonContainer.createEl("button", {
text: t("Cancel"),
});
renameButton.addEventListener("click", async () => {
const newName = this.nameInput.value.trim();
if (
newName &&
newName !== this.workspace.name &&
this.plugin.workspaceManager
) {
console.log("[TG-WORKSPACE] settings:rename", {
id: this.workspace.id,
to: newName,
});
await this.plugin.workspaceManager.renameWorkspace(
this.workspace.id,
newName
);
new Notice(t("Workspace renamed"));
this.onRenamed();
this.close();
} else {
new Notice(t("Please enter a different name"));
}
});
cancelButton.addEventListener("click", () => {
this.close();
});
this.nameInput.focus();
this.nameInput.select();
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}
class DeleteWorkspaceModal extends Modal {
constructor(
private plugin: TaskProgressBarPlugin,
private workspace: WorkspaceData,
private onDeleted: () => void
) {
super(plugin.app);
}
onOpen() {
const {contentEl} = this;
contentEl.createEl("h2", {text: t("Delete Workspace")});
contentEl.createEl("p", {
text: t(
'Are you sure you want to delete "{{name}}"? This action cannot be undone.',
{name: this.workspace.name}
),
});
const buttonContainer = contentEl.createDiv({
cls: "modal-button-container",
});
const deleteButton = buttonContainer.createEl("button", {
text: t("Delete"),
cls: "mod-warning",
});
const cancelButton = buttonContainer.createEl("button", {
text: t("Cancel"),
});
deleteButton.addEventListener("click", async () => {
if (this.plugin.workspaceManager) {
console.log("[TG-WORKSPACE] settings:delete", {
id: this.workspace.id,
});
await this.plugin.workspaceManager.deleteWorkspace(
this.workspace.id
);
new Notice(t("Workspace deleted"));
this.onDeleted();
this.close();
}
});
cancelButton.addEventListener("click", () => {
this.close();
});
}
onClose() {
const {contentEl} = this;
contentEl.empty();
}
}

View file

@ -157,16 +157,16 @@ export class TaskViewV2 extends ItemView {
filters: {},
};
private workspaceId: string = "";
private isSavingFilterState: boolean = false;
private workspaceId = "";
private isSavingFilterState = false;
private tasks: Task[] = [];
private filteredTasks: Task[] = [];
private currentViewId: string = "inbox";
private isLoading: boolean = false;
private currentViewId = "inbox";
private isLoading = false;
private loadError: string | null = null;
private isInitializing: boolean = true;
private updateScheduled: boolean = false;
private isInitializing = true;
private updateScheduled = false;
private pendingUpdates: Set<string> = new Set();
// Track the current active component for view mode switching
@ -465,8 +465,10 @@ export class TaskViewV2 extends ItemView {
this.plugin,
(query) => this.handleSearch(query),
(mode) => this.handleViewModeChange(mode),
() => {}, // Filter is now in Obsidian view header
() => {}, // Sort is now in Obsidian view header
() => {
}, // Filter is now in Obsidian view header
() => {
}, // Sort is now in Obsidian view header
() => this.handleSettingsClick(),
availableModes,
() => this.toggleSidebar() // Pass sidebar toggle callback
@ -645,12 +647,12 @@ export class TaskViewV2 extends ItemView {
// Create legacy containers for backward compatibility
this.listContainer = this.contentArea.createDiv({
cls: "task-list-container",
attr: { style: "display: none;" },
attr: {style: "display: none;"},
});
this.treeContainer = this.contentArea.createDiv({
cls: "task-tree-container",
attr: { style: "display: none;" },
attr: {style: "display: none;"},
});
// Hide all components initially (force hide all during initialization)
@ -662,8 +664,8 @@ export class TaskViewV2 extends ItemView {
private createSidebarToggle() {
const headerBtns = !Platform.isPhone
? (this.headerEl?.find(
".view-header-nav-buttons"
) as HTMLElement | null)
".view-header-nav-buttons"
) as HTMLElement | null)
: (this.headerEl?.find(".view-header-left") as HTMLElement);
if (!headerBtns) {
console.warn("[TG-V2] header buttons container not found");
@ -825,7 +827,8 @@ export class TaskViewV2 extends ItemView {
this.sidebar?.setCollapsed(true);
this.rootContainerEl?.addClass("v2-sidebar-collapsed");
}
} catch (_) {}
} catch (_) {
}
}
private hideAllComponents(forceHideAll: boolean = false) {
@ -902,7 +905,7 @@ export class TaskViewV2 extends ItemView {
isDataflowEnabled(this.plugin) &&
this.plugin.dataflowOrchestrator
) {
const { on, Events } = await import("../../dataflow/events/Events");
const {on, Events} = await import("../../dataflow/events/Events");
// Listen for cache ready event
this.registerEvent(
@ -1224,8 +1227,8 @@ export class TaskViewV2 extends ItemView {
// Skip if we're switching to the same view (but never skip during initialization or if no filters/projects)
// Also don't skip if we have no active filters - this ensures proper refresh when clearing filters
const hasActiveFilters = (this.currentFilterState?.filterGroups?.length > 0) ||
this.viewState.selectedProject;
const hasActiveFilters = (this.currentFilterState?.filterGroups?.length ?? 0) > 0 ||
!!this.viewState.selectedProject;
if (
!this.isInitializing &&
this.currentViewId === viewId &&
@ -1948,7 +1951,7 @@ export class TaskViewV2 extends ItemView {
const loadingEl = this.contentArea.createDiv({
cls: "tg-v2-loading",
});
loadingEl.createDiv({ cls: "tg-v2-spinner" });
loadingEl.createDiv({cls: "tg-v2-spinner"});
loadingEl.createDiv({
cls: "tg-v2-loading-text",
text: t("Loading tasks..."),
@ -1967,7 +1970,7 @@ export class TaskViewV2 extends ItemView {
const errorEl = this.contentArea.createDiv({
cls: "tg-v2-error-state",
});
const errorIcon = errorEl.createDiv({ cls: "tg-v2-error-icon" });
const errorIcon = errorEl.createDiv({cls: "tg-v2-error-icon"});
setIcon(errorIcon, "alert-triangle");
errorEl.createDiv({
@ -2003,7 +2006,7 @@ export class TaskViewV2 extends ItemView {
const emptyEl = this.contentArea.createDiv({
cls: "tg-v2-empty-state",
});
const emptyIcon = emptyEl.createDiv({ cls: "tg-v2-empty-icon" });
const emptyIcon = emptyEl.createDiv({cls: "tg-v2-empty-icon"});
setIcon(emptyIcon, "inbox");
emptyEl.createDiv({
@ -2075,7 +2078,7 @@ export class TaskViewV2 extends ItemView {
const total = projectTasks.length;
const percentage = total > 0 ? (completed / total) * 100 : 0;
return { completed, total, percentage };
return {completed, total, percentage};
}
// Workspace management
@ -2228,7 +2231,7 @@ export class TaskViewV2 extends ItemView {
}, 100);
});
popover.showAtPosition({ x: e.clientX, y: e.clientY });
popover.showAtPosition({x: e.clientX, y: e.clientY});
} else {
const modal = new ViewTaskFilterModal(
this.app,
@ -2336,7 +2339,10 @@ export class TaskViewV2 extends ItemView {
this.saveFilterStateToWorkspace();
// Broadcast filter change to ensure UI components update
this.app.workspace.trigger("task-genius:filter-changed", null);
this.app.workspace.trigger("task-genius:filter-changed", {
rootCondition: "all",
filterGroups: [],
} as any);
// Clear any active project selection in sidebar
this.sidebar?.projectList?.setActiveProject(null);
@ -2428,7 +2434,7 @@ export class TaskViewV2 extends ItemView {
if (this.detailsPanelEl) return;
const wrapper = this.contentArea?.parentElement as HTMLElement;
if (!wrapper) return;
this.detailsPanelEl = wrapper.createDiv({ cls: "tg-v2-details-panel" });
this.detailsPanelEl = wrapper.createDiv({cls: "tg-v2-details-panel"});
const header = this.detailsPanelEl.createDiv({
cls: "tg-v2-details-header",
});
@ -2443,7 +2449,7 @@ export class TaskViewV2 extends ItemView {
this.ensureDetailsPanel();
if (this.detailsBodyEl) {
this.detailsBodyEl.empty();
this.detailsBodyEl.createEl("div", { text: message });
this.detailsBodyEl.createEl("div", {text: message});
}
}
@ -2479,7 +2485,7 @@ export class TaskViewV2 extends ItemView {
return;
}
const updatedTask = { ...task, completed: !task.completed };
const updatedTask = {...task, completed: !task.completed};
if (updatedTask.completed) {
updatedTask.metadata.completedDate = Date.now();
@ -2665,12 +2671,13 @@ export class TaskViewV2 extends ItemView {
});
}
}
private async editTask(task: Task) {
const file = this.app.vault.getFileByPath(task.filePath);
if (!(file instanceof TFile)) return;
const leaf = this.app.workspace.getLeaf(false);
await leaf.openFile(file, {
eState: { line: task.line },
eState: {line: task.line},
});
}
@ -2752,8 +2759,8 @@ export class TaskViewV2 extends ItemView {
} else {
new Notice(
t("Failed to delete task") +
": " +
(result.error || "Unknown error")
": " +
(result.error || "Unknown error")
);
}
} catch (error) {

View file

@ -1,7 +1,12 @@
import { Menu, setIcon, Notice, Modal, Setting } from "obsidian";
import { Menu, setIcon } from "obsidian";
import type TaskProgressBarPlugin from "@/index";
import { WorkspaceData } from "@/experimental/v2/types/workspace";
import { t } from "@/translations/helper";
import {
CreateWorkspaceModal,
RenameWorkspaceModal,
DeleteWorkspaceModal,
} from "@/experimental/v2/components/modals/WorkspaceModals";
export class WorkspaceSelector {
private containerEl: HTMLElement;
@ -49,7 +54,7 @@ export class WorkspaceSelector {
// Use a color scheme for workspaces if needed
workspaceIcon.style.backgroundColor =
this.getWorkspaceColor(currentWorkspace);
setIcon(workspaceIcon, "layers");
setIcon(workspaceIcon, currentWorkspace.icon || "layers");
const workspaceDetails = workspaceInfo.createDiv({
cls: "workspace-details",
@ -119,7 +124,7 @@ export class WorkspaceSelector {
const title = isDefault ? `${workspace.name}` : workspace.name;
item.setTitle(title)
.setIcon("layers")
.setIcon(workspace.icon || "layers")
.onClick(async () => {
await this.onWorkspaceChange(workspace.id);
this.currentWorkspaceId = workspace.id;
@ -186,111 +191,6 @@ export class WorkspaceSelector {
}
private showCreateWorkspaceDialog() {
class CreateWorkspaceModal extends Modal {
private nameInput: HTMLInputElement;
private baseWorkspaceSelect: HTMLSelectElement;
private name: string = "";
private selectWorkspaceId: string = "";
constructor(
private plugin: TaskProgressBarPlugin,
private onCreated: (workspace: WorkspaceData) => void
) {
super(plugin.app);
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("h2", { text: t("Create New Workspace") });
// Name input
new Setting(contentEl)
.setName(t("Workspace Name"))
.setDesc(t("A descriptive name for the workspace"))
.addText((text) => {
text.setPlaceholder(t("Workspace Name"))
.setValue("")
.onChange((value) => {
this.name = value;
});
});
const currentWorkspace =
this.plugin.workspaceManager?.getActiveWorkspace();
// Base workspace selector
new Setting(contentEl)
.setName(t("Copy Settings From"))
.setDesc(t("Copy settings from an existing workspace"))
.addDropdown((dropdown) => {
dropdown.addOption("", t("Select a workspace..."));
if (currentWorkspace) {
dropdown.addOption(
currentWorkspace.id,
`Current (${currentWorkspace.name})`
);
}
this.plugin.workspaceManager
?.getAllWorkspaces()
.forEach((ws) => {
dropdown.addOption(ws.id, ws.name);
});
dropdown.onChange((value) => {
this.selectWorkspaceId = value;
});
});
// Buttons
const buttonContainer = contentEl.createDiv({
cls: "modal-button-container",
});
const createButton = buttonContainer.createEl("button", {
text: t("Create"),
cls: "mod-cta",
});
const cancelButton = buttonContainer.createEl("button", {
text: t("Cancel"),
});
createButton.addEventListener("click", async () => {
const name = this.name.trim();
if (!name) {
new Notice(t("Please enter a workspace name"));
return;
}
const baseId = this.selectWorkspaceId;
if (name && this.plugin.workspaceManager) {
const workspace =
await this.plugin.workspaceManager.createWorkspace(
name,
baseId
);
new Notice(
t('Workspace "{{name}}" created', {
interpolation: { name: name },
})
);
this.onCreated(workspace);
this.close();
} else {
new Notice(t("Please enter a workspace name"));
}
});
cancelButton.addEventListener("click", () => {
this.close();
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
new CreateWorkspaceModal(this.plugin, (workspace) => {
this.onWorkspaceChange(workspace.id);
this.currentWorkspaceId = workspace.id;
@ -299,144 +199,12 @@ export class WorkspaceSelector {
}
private showRenameWorkspaceDialog(workspace: WorkspaceData) {
class RenameWorkspaceModal extends Modal {
private nameInput: HTMLInputElement;
constructor(
private plugin: TaskProgressBarPlugin,
private workspace: WorkspaceData,
private onRenamed: () => void
) {
super(plugin.app);
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("h2", { text: t("Rename Workspace") });
const inputContainer = contentEl.createDiv({
cls: "setting-item",
});
inputContainer.createEl("label", { text: t("New Name") + ":" });
this.nameInput = inputContainer.createEl("input", {
type: "text",
value: this.workspace.name,
placeholder: t("Enter workspace name..."),
});
const buttonContainer = contentEl.createDiv({
cls: "modal-button-container",
});
const renameButton = buttonContainer.createEl("button", {
text: t("Rename"),
cls: "mod-cta",
});
const cancelButton = buttonContainer.createEl("button", {
text: t("Cancel"),
});
renameButton.addEventListener("click", async () => {
const newName = this.nameInput.value.trim();
if (
newName &&
newName !== this.workspace.name &&
this.plugin.workspaceManager
) {
await this.plugin.workspaceManager.renameWorkspace(
this.workspace.id,
newName
);
new Notice(
t('Workspace renamed to "{{name}}"', {
interpolation: { name: newName },
})
);
this.onRenamed();
this.close();
} else {
new Notice(t("Please enter a different name"));
}
});
cancelButton.addEventListener("click", () => {
this.close();
});
this.nameInput.focus();
this.nameInput.select();
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
new RenameWorkspaceModal(this.plugin, workspace, () => {
this.render();
}).open();
}
private showDeleteWorkspaceDialog(workspace: WorkspaceData) {
class DeleteWorkspaceModal extends Modal {
constructor(
private plugin: TaskProgressBarPlugin,
private workspace: WorkspaceData,
private onDeleted: () => void
) {
super(plugin.app);
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("h2", { text: t("Delete Workspace") });
contentEl.createEl("p", {
text: t(
'Are you sure you want to delete the workspace "{{name}}"? This action cannot be undone.',
{ interpolation: { name: this.workspace.name } }
),
});
const buttonContainer = contentEl.createDiv({
cls: "modal-button-container",
});
const deleteButton = buttonContainer.createEl("button", {
text: t("Delete"),
cls: "mod-warning",
});
const cancelButton = buttonContainer.createEl("button", {
text: t("Cancel"),
});
deleteButton.addEventListener("click", async () => {
if (this.plugin.workspaceManager) {
await this.plugin.workspaceManager.deleteWorkspace(
this.workspace.id
);
new Notice(
t('Workspace "{{name}}" deleted', {
interpolation: {
name: this.workspace.name,
},
})
);
this.onDeleted();
this.close();
}
});
cancelButton.addEventListener("click", () => {
this.close();
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
new DeleteWorkspaceModal(this.plugin, workspace, () => {
// After deletion, workspace manager will automatically switch to default
this.currentWorkspaceId =

View file

@ -0,0 +1,345 @@
import { Modal, Notice, Setting, ButtonComponent, setIcon } from "obsidian";
import type TaskProgressBarPlugin from "@/index";
import { WorkspaceData } from "@/experimental/v2/types/workspace";
import { t } from "@/translations/helper";
import { attachIconMenu } from "@/components/ui/menus/IconMenu";
/**
* Helper function to create a workspace icon selector
*/
export function createWorkspaceIconSelector(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
initialIcon: string,
onIconSelected: (iconId: string) => void
): ButtonComponent {
const iconButton = new ButtonComponent(containerEl);
iconButton.setIcon(initialIcon);
attachIconMenu(iconButton, {
containerEl,
plugin,
onIconSelected: (iconId: string) => {
iconButton.setIcon(iconId);
onIconSelected(iconId);
},
});
return iconButton;
}
/**
* Modal for creating a new workspace
*/
export class CreateWorkspaceModal extends Modal {
private nameInput: HTMLInputElement;
private baseSelect: HTMLSelectElement;
private selectedIcon: string = "layers";
private name: string = "";
private selectWorkspaceId: string = "";
constructor(
private plugin: TaskProgressBarPlugin,
private onCreated: (workspace: WorkspaceData) => void
) {
super(plugin.app);
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("h2", { text: t("Create New Workspace") });
// Name input
new Setting(contentEl)
.setName(t("Workspace Name"))
.setDesc(t("A descriptive name for the workspace"))
.addText((text) => {
text
.setPlaceholder(t("Enter workspace name"))
.setValue("")
.onChange((value) => {
this.name = value;
});
this.nameInput = text.inputEl;
});
// Icon selection
const iconSetting = new Setting(contentEl)
.setName(t("Workspace Icon"))
.setDesc(t("Choose an icon for this workspace"));
const iconContainer = iconSetting.controlEl.createDiv({
cls: "workspace-icon-selector",
});
createWorkspaceIconSelector(
iconContainer,
this.plugin,
this.selectedIcon,
(iconId) => {
this.selectedIcon = iconId;
}
);
// Base workspace selector
new Setting(contentEl)
.setName(t("Copy Settings From"))
.setDesc(t("Copy settings from an existing workspace"))
.addDropdown((dropdown) => {
dropdown.addOption("", t("Default settings"));
const currentWorkspace = this.plugin.workspaceManager?.getActiveWorkspace();
if (currentWorkspace) {
dropdown.addOption(
currentWorkspace.id,
`Current (${currentWorkspace.name})`
);
}
this.plugin.workspaceManager
?.getAllWorkspaces()
.forEach((ws) => {
if (ws.id !== currentWorkspace?.id) {
dropdown.addOption(ws.id, ws.name);
}
});
dropdown.onChange((value) => {
this.selectWorkspaceId = value;
});
this.baseSelect = dropdown.selectEl;
});
// Buttons
const buttonContainer = contentEl.createDiv({
cls: "modal-button-container",
});
const createButton = buttonContainer.createEl("button", {
text: t("Create"),
cls: "mod-cta",
});
const cancelButton = buttonContainer.createEl("button", {
text: t("Cancel"),
});
createButton.addEventListener("click", async () => {
const name = this.name.trim();
const baseId = this.selectWorkspaceId || undefined;
if (!name) {
new Notice(t("Please enter a workspace name"));
return;
}
if (this.plugin.workspaceManager) {
console.log("[TG-WORKSPACE] modal:create", {
name,
baseId,
icon: this.selectedIcon,
});
const workspace = await this.plugin.workspaceManager.createWorkspace(
name,
baseId,
this.selectedIcon
);
new Notice(
t('Workspace "{{name}}" created', {
interpolation: { name: name },
})
);
this.onCreated(workspace);
this.close();
}
});
cancelButton.addEventListener("click", () => {
this.close();
});
// Focus the name input
this.nameInput.focus();
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
/**
* Modal for renaming a workspace
*/
export class RenameWorkspaceModal extends Modal {
private nameInput: HTMLInputElement;
private selectedIcon: string;
constructor(
private plugin: TaskProgressBarPlugin,
private workspace: WorkspaceData,
private onRenamed: () => void
) {
super(plugin.app);
this.selectedIcon = workspace.icon || "layers";
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("h2", { text: t("Rename Workspace") });
// Name input
new Setting(contentEl)
.setName(t("New Name"))
.addText((text) => {
text
.setValue(this.workspace.name)
.setPlaceholder(t("Enter new name"));
this.nameInput = text.inputEl;
});
// Icon selection
const iconSetting = new Setting(contentEl)
.setName(t("Workspace Icon"))
.setDesc(t("Choose an icon for this workspace"));
const iconContainer = iconSetting.controlEl.createDiv({
cls: "workspace-icon-selector",
});
createWorkspaceIconSelector(
iconContainer,
this.plugin,
this.selectedIcon,
(iconId) => {
this.selectedIcon = iconId;
}
);
// Buttons
const buttonContainer = contentEl.createDiv({
cls: "modal-button-container",
});
const saveButton = buttonContainer.createEl("button", {
text: t("Save"),
cls: "mod-cta",
});
const cancelButton = buttonContainer.createEl("button", {
text: t("Cancel"),
});
saveButton.addEventListener("click", async () => {
const newName = this.nameInput.value.trim();
if (!newName) {
new Notice(t("Please enter a name"));
return;
}
if (this.plugin.workspaceManager) {
console.log("[TG-WORKSPACE] modal:rename", {
id: this.workspace.id,
name: newName,
icon: this.selectedIcon,
});
await this.plugin.workspaceManager.renameWorkspace(
this.workspace.id,
newName,
this.selectedIcon
);
new Notice(t("Workspace updated"));
this.onRenamed();
this.close();
}
});
cancelButton.addEventListener("click", () => {
this.close();
});
// Focus and select the name input
this.nameInput.focus();
this.nameInput.select();
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
/**
* Modal for deleting a workspace
*/
export class DeleteWorkspaceModal extends Modal {
constructor(
private plugin: TaskProgressBarPlugin,
private workspace: WorkspaceData,
private onDeleted: () => void
) {
super(plugin.app);
}
onOpen() {
const { contentEl } = this;
contentEl.createEl("h2", { text: t("Delete Workspace") });
contentEl.createEl("p", {
text: t(
'Are you sure you want to delete "{{name}}"? This action cannot be undone.',
{ interpolation: { name: this.workspace.name } }
),
});
const buttonContainer = contentEl.createDiv({
cls: "modal-button-container",
});
const deleteButton = buttonContainer.createEl("button", {
text: t("Delete"),
cls: "mod-warning",
});
const cancelButton = buttonContainer.createEl("button", {
text: t("Cancel"),
});
deleteButton.addEventListener("click", async () => {
if (this.plugin.workspaceManager) {
console.log("[TG-WORKSPACE] modal:delete", {
id: this.workspace.id,
});
await this.plugin.workspaceManager.deleteWorkspace(
this.workspace.id
);
new Notice(
t('Workspace "{{name}}" deleted', {
interpolation: { name: this.workspace.name },
})
);
this.onDeleted();
this.close();
}
});
cancelButton.addEventListener("click", () => {
this.close();
});
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}

View file

@ -288,7 +288,8 @@ export class WorkspaceManager {
// Create new workspace (cloned from current or default)
public async createWorkspace(
name: string,
baseWorkspaceId?: string
baseWorkspaceId?: string,
icon?: string
): Promise<WorkspaceData> {
const config = this.getWorkspacesConfig();
const id = this.generateId();
@ -316,6 +317,13 @@ export class WorkspaceManager {
settings,
};
// Add icon if provided, otherwise inherit from base workspace if cloning
if (icon) {
newWorkspace.icon = icon;
} else if (baseWorkspace?.icon && baseId !== config.defaultWorkspaceId) {
newWorkspace.icon = baseWorkspace.icon;
}
config.byId[id] = newWorkspace;
config.order.push(id);
@ -360,7 +368,8 @@ export class WorkspaceManager {
// Rename workspace
public async renameWorkspace(
workspaceId: string,
newName: string
newName: string,
icon?: string
): Promise<void> {
const config = this.getWorkspacesConfig();
const workspace = config.byId[workspaceId];
@ -370,6 +379,9 @@ export class WorkspaceManager {
}
workspace.name = newName;
if (icon !== undefined) {
workspace.icon = icon;
}
workspace.updatedAt = Date.now();
await this.plugin.saveSettings();

View file

@ -5,12 +5,9 @@
/* Shadows */
--tg-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tg-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--tg-shadow-md:
0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tg-shadow-lg:
0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tg-shadow-xl:
0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--tg-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tg-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--tg-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
/* Animations */
--tg-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
@ -564,9 +561,8 @@
border-radius: var(--tg-radius-full);
background: rgba(255, 255, 255, 0.2);
transform: translate(-50%, -50%);
transition:
width var(--tg-transition-slow),
height var(--tg-transition-slow);
transition: width var(--tg-transition-slow),
height var(--tg-transition-slow);
}
.tg-v2-button:active::before {
@ -687,20 +683,16 @@
.theme-dark {
--tg-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
--tg-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.4), 0 1px 2px -1px rgb(0 0 0 / 0.3);
--tg-shadow-md:
0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
--tg-shadow-lg:
0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
--tg-shadow-xl:
0 20px 25px -5px rgb(0 0 0 / 0.4), 0 8px 10px -6px rgb(0 0 0 / 0.3);
--tg-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4), 0 2px 4px -2px rgb(0 0 0 / 0.3);
--tg-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.3);
--tg-shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.4), 0 8px 10px -6px rgb(0 0 0 / 0.3);
}
.theme-dark .tg-v2-card:hover,
.theme-dark .task-list-item:hover,
.theme-dark .task-tree-project:hover {
box-shadow:
0 0 0 1px var(--interactive-accent),
var(--tg-shadow-md);
box-shadow: 0 0 0 1px var(--interactive-accent),
var(--tg-shadow-md);
}
/* ========== Progress Bars ========== */
@ -790,3 +782,10 @@
color: white;
border-color: var(--text-error);
}
.workspace-name-with-icon {
display: flex;
align-items: center;
justify-content: flex-start;
gap: var(--size-2-2);
}

View file

@ -2,6 +2,7 @@ export interface WorkspaceData {
id: string;
name: string;
color?: string;
icon?: string; // Optional custom icon for the workspace
updatedAt: number;
order?: number;
settings: WorkspaceOverrides; // Empty for Default workspace

View file

@ -70,8 +70,6 @@ class PathTrie {
.split("/")
.filter((part) => part.length > 0);
console.log(parts, path);
// Try to match the rule starting at any segment in the input path
for (let start = 0; start < parts.length; start++) {
let current = this.root;
@ -219,6 +217,7 @@ export class FileFilterManager {
clearCache(): void {
this.cache.clear();
}
/**
* Build a cache key that is scoped by kind and scope to avoid cross-scope pollution
*/
@ -289,20 +288,20 @@ export class FileFilterManager {
scope === "file"
? this.fileSetFile
: scope === "inline"
? this.fileSetInline
: this.fileSet;
? this.fileSetInline
: this.fileSet;
const folderTrie =
scope === "file"
? this.folderTrieFile
: scope === "inline"
? this.folderTrieInline
: this.folderTrie;
? this.folderTrieInline
: this.folderTrie;
const patternRegexes =
scope === "file"
? this.patternRegexesFile
: scope === "inline"
? this.patternRegexesInline
: this.patternRegexes;
? this.patternRegexesInline
: this.patternRegexes;
// Detailed match breakdown
const fileHit = fileSet.has(normalizedPath);
@ -341,28 +340,28 @@ export class FileFilterManager {
switch (rule.type) {
case "file":
(bucket === "file"
? this.fileSetFile
: bucket === "inline"
? this.fileSetInline
: this.fileSet
? this.fileSetFile
: bucket === "inline"
? this.fileSetInline
: this.fileSet
).add(this.normalizePath(rule.path));
break;
case "folder":
(bucket === "file"
? this.folderTrieFile
: bucket === "inline"
? this.folderTrieInline
: this.folderTrie
? this.folderTrieFile
: bucket === "inline"
? this.folderTrieInline
: this.folderTrie
).insert(rule.path, true);
break;
case "pattern":
try {
const regexPattern = this.globToRegex(rule.path);
(bucket === "file"
? this.patternRegexesFile
: bucket === "inline"
? this.patternRegexesInline
: this.patternRegexes
? this.patternRegexesFile
: bucket === "inline"
? this.patternRegexesInline
: this.patternRegexes
).push(new RegExp(regexPattern, "i"));
} catch (error) {
console.warn(

View file

@ -0,0 +1,92 @@
/* Workspace Icon Selection Styles */
/* Icon selector container in modals */
.workspace-icon-selector {
display: flex;
align-items: center;
gap: 12px;
}
.workspace-icon-preview {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
background-color: var(--background-modifier-hover);
border: 1px solid var(--background-modifier-border);
}
.workspace-icon-preview svg {
width: 20px;
height: 20px;
color: var(--text-normal);
}
/* Workspace list in settings */
.workspace-name-with-icon {
display: flex;
align-items: center;
gap: 8px;
}
.workspace-list-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.workspace-list-icon svg {
width: 16px;
height: 16px;
color: var(--text-muted);
}
.workspace-item-active .workspace-list-icon svg {
color: var(--text-accent);
}
/* Workspace selector button */
.workspace-selector .workspace-icon {
display: flex;
align-items: center;
justify-content: center;
}
.workspace-selector .workspace-icon svg {
width: 16px;
height: 16px;
color: white;
}
/* Modal button container alignment */
.modal-button-container {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--background-modifier-border);
}
/* Icon menu overrides for workspace selection */
.tg-icon-menu {
z-index: var(--layer-modal);
}
/* Hover effects */
.workspace-icon-preview:hover {
background-color: var(--background-modifier-hover-more);
}
/* Active workspace visual emphasis */
.workspace-item-active {
background-color: var(--background-secondary);
border-left: 3px solid var(--text-accent);
margin-left: -12px;
padding-left: 9px;
}

File diff suppressed because one or more lines are too long