feat(fluent): add responsive sidebar auto-collapse

- Auto-collapse sidebar when view width ≤ 600px
- Auto-expand sidebar when view width > 600px
- Add leaf-width-updated event for width tracking
- Implement debounced onResize handler in FluentTaskView
- Add responsive tab text hiding for narrow views
- Remove unnecessary console warnings in ContentComponent
- Fix CHANGELOG typo (Shinny → Features)
This commit is contained in:
Quorafind 2025-10-22 12:38:03 +08:00
parent f694663991
commit d5f27b7205
9 changed files with 152 additions and 72 deletions

View file

@ -16,7 +16,7 @@ All notable changes to this project will be documented in this file.
## [9.9.0](https://github.com/Quorafind/Obsidian-Task-Genius/compare/9.8.14...9.9.0) (2025-10-21)
### Shinny new things
### Features
- **Fluent Interface**: Brand new modern interface mode
@ -607,4 +607,4 @@ All notable changes to this project will be documented in this file.
### Tests
* improve test reliability and fix flaky date tests ([d66a13a](https://github.com/Quorafind/Obsidian-Task-Progress-Bar/commit/d66a13a5f41a5ea74d22c7b9215087aef80b5b07))
* improve test reliability and fix flaky date tests ([d66a13a](https://github.com/Quorafind/Obsidian-Task-Progress-Bar/commit/d66a13a5f41a5ea74d22c7b9215087aef80b5b07))

View file

@ -24,6 +24,7 @@ import { t } from "@/translations/helper";
import { ViewConfigModal } from "@/components/features/task/view/modals/ViewConfigModal";
import { TASK_SPECIFIC_VIEW_TYPE } from "@/pages/TaskSpecificView";
import { ViewConfig, ViewFilterRule } from "@/common/setting-definition";
import { Events, on } from "@/dataflow/events/Events";
export class FluentSidebar extends Component {
private containerEl: HTMLElement;
@ -74,8 +75,7 @@ export class FluentSidebar extends Component {
const manager = this.plugin.workspaceManager;
if (!manager) return true;
const workspaceId =
this.currentWorkspaceId ||
manager.getActiveWorkspace()?.id;
this.currentWorkspaceId || manager.getActiveWorkspace()?.id;
return !manager.isViewHidden(viewId, workspaceId);
}
@ -168,10 +168,7 @@ export class FluentSidebar extends Component {
this.registerDomEvent(treeToggleBtn, "click", () => {
this.isTreeView = !this.isTreeView;
setIcon(
treeToggleBtn,
this.isTreeView ? "git-branch" : "list",
);
setIcon(treeToggleBtn, this.isTreeView ? "git-branch" : "list");
// Save preference
this.plugin.app.saveLocalStorage(
"task-genius-project-view-mode",
@ -246,20 +243,20 @@ export class FluentSidebar extends Component {
this.primaryItems
.filter((item) => this.isViewVisible(item.id))
.forEach((item) => {
const btn = this.railEl!.createDiv({
cls: "fluent-rail-btn",
attr: { "aria-label": item.label, "data-view-id": item.id },
const btn = this.railEl!.createDiv({
cls: "fluent-rail-btn",
attr: { "aria-label": item.label, "data-view-id": item.id },
});
setIcon(btn, item.icon);
this.registerDomEvent(btn, "click", () => {
this.setActiveItem(item.id);
this.onNavigate(item.id);
});
// Add context menu handler for rail button
this.registerDomEvent(btn, "contextmenu", (e) => {
this.showViewContextMenu(e as MouseEvent, item.id);
});
});
setIcon(btn, item.icon);
this.registerDomEvent(btn, "click", () => {
this.setActiveItem(item.id);
this.onNavigate(item.id);
});
// Add context menu handler for rail button
this.registerDomEvent(btn, "contextmenu", (e) => {
this.showViewContextMenu(e as MouseEvent, item.id);
});
});
// Other view icons with overflow menu when > 5
if (

View file

@ -230,6 +230,27 @@ export class FluentLayoutManager extends Component {
.setTooltip(t("Toggle Sidebar"))
.setClass("clickable-icon")
.onClick(() => this.toggleSidebar());
this.registerEvent(
this.plugin.app.workspace.on(
"task-genius:leaf-width-updated",
(width: number) => {
if (
width <= 600 &&
width !== 0 &&
!this.isSidebarCollapsed
) {
this.toggleSidebar();
} else if (
width > 600 &&
width !== 0 &&
this.isSidebarCollapsed
) {
this.toggleSidebar();
}
},
),
);
}
/**

View file

@ -376,9 +376,6 @@ export class ContentComponent extends Component {
private refreshTaskList() {
// Defer rendering if container is not visible yet (e.g., view hidden during init)
if (!this.isContainerVisible()) {
console.warn(
"ContentComponent: Cannot render: Container not visible. Queuing refresh...",
);
this.pendingForceRefresh = true;
if (!this.pendingVisibilityRetry) {
this.pendingVisibilityRetry = true;
@ -398,9 +395,6 @@ export class ContentComponent extends Component {
} else {
this.pendingVisibilityRetry = false;
this.visibilityRetryCount = 0;
console.warn(
"ContentComponent: Container still not visible after retries; will wait for next trigger.",
);
}
};
tryAgain();
@ -585,7 +579,7 @@ export class ContentComponent extends Component {
this.currentViewId, // Pass currentViewId
this.app,
this.plugin,
this.params.selectionManager // Pass selection manager
this.params.selectionManager, // Pass selection manager
);
// Attach event handlers
@ -646,7 +640,7 @@ export class ContentComponent extends Component {
childTasks,
taskMap,
this.plugin,
this.params.selectionManager // Pass selection manager
this.params.selectionManager, // Pass selection manager
);
// Attach event handlers
@ -827,7 +821,7 @@ export class ContentComponent extends Component {
this.currentViewId,
this.app,
this.plugin,
this.params.selectionManager // Pass selection manager
this.params.selectionManager, // Pass selection manager
);
// Attach events
taskComponent.onTaskSelected = this.selectTask.bind(this);
@ -941,7 +935,7 @@ export class ContentComponent extends Component {
childTasks,
taskMap,
this.plugin,
this.params.selectionManager // Pass selection manager
this.params.selectionManager, // Pass selection manager
);
newRoot.onTaskSelected = this.selectTask.bind(this);
newRoot.onTaskCompleted = (t) => {

View file

@ -2,22 +2,22 @@ import type { App, EventRef } from "obsidian";
// Keep names simple and consistent
export const Events = {
CACHE_READY: "task-genius:cache-ready",
TASK_CACHE_UPDATED: "task-genius:task-cache-updated",
FILE_UPDATED: "task-genius:file-updated",
PROJECT_DATA_UPDATED: "task-genius:project-data-updated",
SETTINGS_CHANGED: "task-genius:settings-changed",
TASK_COMPLETED: "task-genius:task-completed",
TASK_ADDED: "task-genius:task-added",
TASK_UPDATED: "task-genius:task-updated",
TASK_DELETED: "task-genius:task-deleted",
WRITE_OPERATION_START: "task-genius:write-operation-start",
WRITE_OPERATION_COMPLETE: "task-genius:write-operation-complete",
ICS_EVENTS_UPDATED: "task-genius:ics-events-updated",
FILE_TASK_UPDATED: "task-genius:file-task-updated",
FILE_TASK_REMOVED: "task-genius:file-task-removed",
BATCH_OPERATION_START: "task-genius:batch-operation-start",
BATCH_OPERATION_COMPLETE: "task-genius:batch-operation-complete",
CACHE_READY: "task-genius:cache-ready",
TASK_CACHE_UPDATED: "task-genius:task-cache-updated",
FILE_UPDATED: "task-genius:file-updated",
PROJECT_DATA_UPDATED: "task-genius:project-data-updated",
SETTINGS_CHANGED: "task-genius:settings-changed",
TASK_COMPLETED: "task-genius:task-completed",
TASK_ADDED: "task-genius:task-added",
TASK_UPDATED: "task-genius:task-updated",
TASK_DELETED: "task-genius:task-deleted",
WRITE_OPERATION_START: "task-genius:write-operation-start",
WRITE_OPERATION_COMPLETE: "task-genius:write-operation-complete",
ICS_EVENTS_UPDATED: "task-genius:ics-events-updated",
FILE_TASK_UPDATED: "task-genius:file-task-updated",
FILE_TASK_REMOVED: "task-genius:file-task-removed",
BATCH_OPERATION_START: "task-genius:batch-operation-start",
BATCH_OPERATION_COMPLETE: "task-genius:batch-operation-complete",
} as const;
// Batch operation payload types
@ -34,31 +34,38 @@ export type SeqClock = { next(): number };
let _seq = 0;
export const Seq: SeqClock = {
next() {
_seq = (_seq + 1) >>> 0;
return _seq;
},
next() {
_seq = (_seq + 1) >>> 0;
return _seq;
},
};
// Emit helpers (payload kept as any to maintain compatibility with existing trigger signatures)
export function emit(app: App, name: string, payload?: any): void {
// @ts-expect-error keep compatibility with existing Obsidian typing overloads
app.workspace.trigger(name, payload);
// @ts-expect-error keep compatibility with existing Obsidian typing overloads
app.workspace.trigger(name, payload);
}
export function on(app: App, name: string, handler: (...args: any[]) => void): EventRef {
// Consumers should use component.registerEvent(on(...)) to auto-unsub
return app.workspace.on(name as any, handler) as EventRef;
export function on(
app: App,
name: string,
handler: (...args: any[]) => void,
): EventRef {
// Consumers should use component.registerEvent(on(...)) to auto-unsub
return app.workspace.on(name as any, handler) as EventRef;
}
// Convenience
export const onTaskCacheUpdated = (app: App, handler: (payload: any) => void) =>
on(app, Events.TASK_CACHE_UPDATED, handler);
on(app, Events.TASK_CACHE_UPDATED, handler);
export const emitTaskCacheUpdated = (app: App, payload: any) =>
emit(app, Events.TASK_CACHE_UPDATED, payload);
export const emitBatchOperationStart = (app: App, payload: BatchOperationStartPayload) =>
emit(app, Events.BATCH_OPERATION_START, payload);
export const emitBatchOperationComplete = (app: App, payload: BatchOperationCompletePayload) =>
emit(app, Events.BATCH_OPERATION_COMPLETE, payload);
emit(app, Events.TASK_CACHE_UPDATED, payload);
export const emitBatchOperationStart = (
app: App,
payload: BatchOperationStartPayload,
) => emit(app, Events.BATCH_OPERATION_START, payload);
export const emitBatchOperationComplete = (
app: App,
payload: BatchOperationCompletePayload,
) => emit(app, Events.BATCH_OPERATION_COMPLETE, payload);

View file

@ -1,4 +1,4 @@
import { ItemView, Scope, WorkspaceLeaf } from "obsidian";
import { debounce, ItemView, Scope, WorkspaceLeaf } from "obsidian";
import TaskProgressBarPlugin from "@/index";
import { Task } from "@/types/task";
import "@/styles/fluent/fluent-main.css";
@ -137,6 +137,17 @@ export class FluentTaskView extends ItemView {
return !!this.plugin.settings.fluentView?.useWorkspaceSideLeaves;
}
updateWorkspaceLeafWidth = debounce(() => {
this.app.workspace.trigger(
"task-genius:leaf-width-updated",
this.leaf.width,
);
}, 200);
onResize(): void {
this.updateWorkspaceLeafWidth();
}
private ensureViewModeForView(viewId: string): ViewMode {
const availableModes =
this.componentManager?.getAvailableModesForView(viewId) ?? [];
@ -480,21 +491,28 @@ export class FluentTaskView extends ItemView {
// When navigating to projects view directly, clear project selection
// This enables the full project overview mode
if (viewId === "projects") {
console.log("[TG] Navigating to projects overview - clearing project selection");
console.log(
"[TG] Navigating to projects overview - clearing project selection",
);
this.viewState.selectedProject = undefined;
// Clear project filter from filter state
try {
if (this.liveFilterState) {
const nextState = { ...this.liveFilterState };
nextState.filterGroups = (nextState.filterGroups || [])
nextState.filterGroups = (
nextState.filterGroups || []
)
.map((g: any) => ({
...g,
filters: (g.filters || []).filter(
(f: any) => f.property !== "project",
),
}))
.filter((g: any) => g.filters && g.filters.length > 0);
.filter(
(g: any) =>
g.filters && g.filters.length > 0,
);
this.liveFilterState = nextState as any;
this.currentFilterState = nextState as any;
@ -657,6 +675,9 @@ export class FluentTaskView extends ItemView {
this.selectionManager.clearSelection();
});
// Trigger initial workspace leaf width update
this.updateWorkspaceLeafWidth();
console.log("[TG] Managers initialized");
}
@ -856,7 +877,7 @@ export class FluentTaskView extends ItemView {
this.workspaceStateManager.captureFilterStateSnapshot();
if (snapshot) {
await this.workspaceStateManager.saveFilterStateImmediately(
snapshot
snapshot,
);
}

View file

@ -89,7 +89,9 @@
width: var(--size-4-9);
flex-shrink: 0;
transition: width 0.3s ease-in-out, flex-shrink 0.3s ease-in-out;
transition:
width 0.3s ease-in-out,
flex-shrink 0.3s ease-in-out;
}
.task-sidebar.collapsed .nav-item-icon {
@ -409,12 +411,12 @@
}
.mod-left-split
.workspace-tab-header-status-container:has(.task-genius-action-btn) {
.workspace-tab-header-status-container:has(.task-genius-action-btn) {
display: none;
}
.mod-right-split
.workspace-tab-header-status-container:has(.task-genius-action-btn) {
.workspace-tab-header-status-container:has(.task-genius-action-btn) {
display: none;
}
@ -428,6 +430,18 @@
justify-content: center;
}
.tg-fluent-error-state {
display: flex;
align-items: center;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
--icon-size: var(--size-4-12);
font-size: var(--font-ui-large);
gap: var(--size-2-2);
}
.mod-root .task-genius-tab-header {
container-type: inline-size !important;
}

View file

@ -378,6 +378,11 @@ declare module "obsidian" {
callback: (taskId: string) => void,
): EventRef;
on(
event: "task-genius:leaf-width-updated",
callback: (width: number) => void,
): EventRef;
on(
event: "task-genius:task-cache-updated",
callback: (cache: TaskCache) => void,
@ -426,6 +431,8 @@ declare module "obsidian" {
trigger(event: "task-genius:task-deleted", taskId: string): void;
trigger(event: "task-genius:leaf-width-updated", width: number): void;
trigger(
event: "task-genius:task-cache-updated",
cache: TaskCache,

File diff suppressed because one or more lines are too long