mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(fluent): add mobile drawer navigation and responsive design
- Implement mobile drawer with overlay and swipe gestures - Add touch gesture support for drawer open/close - Refactor sidebar rail mode for better collapsed state - Add responsive breakpoints for mobile devices - Optimize search and navigation UI for small screens - Add mobile-specific CSS classes and styling - Improve task details panel layout on mobile
This commit is contained in:
parent
59643c038f
commit
7721ac191c
8 changed files with 1144 additions and 310 deletions
|
|
@ -9,6 +9,7 @@ import {
|
|||
App,
|
||||
Menu,
|
||||
debounce,
|
||||
Platform,
|
||||
} from "obsidian";
|
||||
import { Task } from "@/types/task";
|
||||
import TaskProgressBarPlugin from "@/index";
|
||||
|
|
@ -17,11 +18,21 @@ import "@/styles/task-details.css";
|
|||
import { t } from "@/translations/helper";
|
||||
import { clearAllMarks } from "@/components/ui/renderers/MarkdownRenderer";
|
||||
import { StatusComponent } from "@/components/ui/feedback/StatusIndicator";
|
||||
import { ContextSuggest, ProjectSuggest, TagSuggest } from "@/components/ui/inputs/AutoComplete";
|
||||
import {
|
||||
ContextSuggest,
|
||||
ProjectSuggest,
|
||||
TagSuggest,
|
||||
} from "@/components/ui/inputs/AutoComplete";
|
||||
import { FileTask } from "@/types/file-task";
|
||||
import { getEffectiveProject, isProjectReadonly } from "@/utils/task/task-operations";
|
||||
import {
|
||||
getEffectiveProject,
|
||||
isProjectReadonly,
|
||||
} from "@/utils/task/task-operations";
|
||||
import { OnCompletionConfigurator } from "@/components/features/on-completion/OnCompletionConfigurator";
|
||||
import { timestampToLocalDateString, localDateStringToTimestamp } from "@/utils/date/date-display-helper";
|
||||
import {
|
||||
timestampToLocalDateString,
|
||||
localDateStringToTimestamp,
|
||||
} from "@/utils/date/date-display-helper";
|
||||
|
||||
function getStatus(task: Task, settings: TaskProgressBarSettings) {
|
||||
const status = Object.keys(settings.taskStatuses).find((key) => {
|
||||
|
|
@ -60,16 +71,16 @@ function mapTextStatusToSymbol(status: string): string {
|
|||
if (!status) return " ";
|
||||
if (status.length === 1) return status; // already a symbol mark
|
||||
const map: Record<string, string> = {
|
||||
"completed": "x",
|
||||
"done": "x",
|
||||
"finished": "x",
|
||||
completed: "x",
|
||||
done: "x",
|
||||
finished: "x",
|
||||
"in-progress": "/",
|
||||
"in progress": "/",
|
||||
"doing": "/",
|
||||
"planned": "?",
|
||||
"todo": "?",
|
||||
"cancelled": "-",
|
||||
"canceled": "-",
|
||||
doing: "/",
|
||||
planned: "?",
|
||||
todo: "?",
|
||||
cancelled: "-",
|
||||
canceled: "-",
|
||||
"not-started": " ",
|
||||
"not started": " ",
|
||||
};
|
||||
|
|
@ -155,18 +166,24 @@ export class TaskDetailsComponent extends Component {
|
|||
const headerEl = this.containerEl.createDiv({ cls: "details-header" });
|
||||
headerEl.setText(t("Task Details"));
|
||||
|
||||
headerEl.createEl(
|
||||
"div",
|
||||
{
|
||||
cls: "details-close-btn",
|
||||
},
|
||||
(el) => {
|
||||
new ExtraButtonComponent(el).setIcon("x").onClick(() => {
|
||||
this.toggleDetailsVisibility &&
|
||||
this.toggleDetailsVisibility(false);
|
||||
});
|
||||
}
|
||||
);
|
||||
// Only show close button on mobile or if explicitly requested
|
||||
if (
|
||||
Platform.isPhone ||
|
||||
this.containerEl.parentElement?.hasClass("tg-v2-container")
|
||||
) {
|
||||
headerEl.createEl(
|
||||
"div",
|
||||
{
|
||||
cls: "details-close-btn",
|
||||
},
|
||||
(el) => {
|
||||
new ExtraButtonComponent(el).setIcon("x").onClick(() => {
|
||||
this.toggleDetailsVisibility &&
|
||||
this.toggleDetailsVisibility(false);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Create content container
|
||||
this.contentEl = this.containerEl.createDiv({ cls: "details-content" });
|
||||
|
|
@ -409,10 +426,12 @@ export class TaskDetailsComponent extends Component {
|
|||
console.log("tagsInput", tagsInput, task.metadata.tags);
|
||||
// Remove # prefix from tags when displaying them
|
||||
tagsInput.setValue(
|
||||
task.metadata.tags
|
||||
task.metadata.tags
|
||||
? task.metadata.tags
|
||||
.map(tag => tag.startsWith("#") ? tag.slice(1) : tag)
|
||||
.join(", ")
|
||||
.map((tag) =>
|
||||
tag.startsWith("#") ? tag.slice(1) : tag
|
||||
)
|
||||
.join(", ")
|
||||
: ""
|
||||
);
|
||||
tagsField
|
||||
|
|
@ -462,8 +481,10 @@ export class TaskDetailsComponent extends Component {
|
|||
});
|
||||
if (task.metadata.dueDate) {
|
||||
// Use helper to correctly display UTC noon timestamp as local date
|
||||
dueDateInput.value = timestampToLocalDateString(task.metadata.dueDate);
|
||||
} // Start date
|
||||
dueDateInput.value = timestampToLocalDateString(
|
||||
task.metadata.dueDate
|
||||
);
|
||||
} // Start date
|
||||
const startDateField = this.createFormField(
|
||||
this.editFormEl,
|
||||
t("Start Date")
|
||||
|
|
@ -474,7 +495,9 @@ export class TaskDetailsComponent extends Component {
|
|||
});
|
||||
if (task.metadata.startDate) {
|
||||
// Use helper to correctly display UTC noon timestamp as local date
|
||||
startDateInput.value = timestampToLocalDateString(task.metadata.startDate);
|
||||
startDateInput.value = timestampToLocalDateString(
|
||||
task.metadata.startDate
|
||||
);
|
||||
}
|
||||
|
||||
// Scheduled date
|
||||
|
|
@ -488,7 +511,9 @@ export class TaskDetailsComponent extends Component {
|
|||
});
|
||||
if (task.metadata.scheduledDate) {
|
||||
// Use helper to correctly display UTC noon timestamp as local date
|
||||
scheduledDateInput.value = timestampToLocalDateString(task.metadata.scheduledDate);
|
||||
scheduledDateInput.value = timestampToLocalDateString(
|
||||
task.metadata.scheduledDate
|
||||
);
|
||||
}
|
||||
|
||||
// Cancelled date
|
||||
|
|
@ -502,7 +527,9 @@ export class TaskDetailsComponent extends Component {
|
|||
});
|
||||
if (task.metadata.cancelledDate) {
|
||||
// Use helper to correctly display UTC noon timestamp as local date
|
||||
cancelledDateInput.value = timestampToLocalDateString(task.metadata.cancelledDate);
|
||||
cancelledDateInput.value = timestampToLocalDateString(
|
||||
task.metadata.cancelledDate
|
||||
);
|
||||
}
|
||||
|
||||
// On completion action
|
||||
|
|
@ -538,7 +565,9 @@ export class TaskDetailsComponent extends Component {
|
|||
? tagsValue
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.map((tag) => tag.startsWith("#") ? tag.slice(1) : tag) // Remove # prefix if present
|
||||
.map((tag) =>
|
||||
tag.startsWith("#") ? tag.slice(1) : tag
|
||||
) // Remove # prefix if present
|
||||
.filter((tag) => tag)
|
||||
: [];
|
||||
|
||||
|
|
@ -592,7 +621,8 @@ export class TaskDetailsComponent extends Component {
|
|||
const scheduledDateValue = scheduledDateInput.value;
|
||||
if (scheduledDateValue) {
|
||||
// Use helper to convert local date string to UTC noon timestamp
|
||||
const newScheduledDate = localDateStringToTimestamp(scheduledDateValue);
|
||||
const newScheduledDate =
|
||||
localDateStringToTimestamp(scheduledDateValue);
|
||||
// Only update if the date has changed or is different from the original
|
||||
if (task.metadata.scheduledDate !== newScheduledDate) {
|
||||
metadata.scheduledDate = newScheduledDate;
|
||||
|
|
@ -610,7 +640,8 @@ export class TaskDetailsComponent extends Component {
|
|||
const cancelledDateValue = cancelledDateInput.value;
|
||||
if (cancelledDateValue) {
|
||||
// Use helper to convert local date string to UTC noon timestamp
|
||||
const newCancelledDate = localDateStringToTimestamp(cancelledDateValue);
|
||||
const newCancelledDate =
|
||||
localDateStringToTimestamp(cancelledDateValue);
|
||||
// Only update if the date has changed or is different from the original
|
||||
if (task.metadata.cancelledDate !== newCancelledDate) {
|
||||
metadata.cancelledDate = newCancelledDate;
|
||||
|
|
|
|||
|
|
@ -133,6 +133,15 @@ export class TaskViewV2 extends ItemView {
|
|||
// Sidebar collapse state
|
||||
private isSidebarCollapsed: boolean = false;
|
||||
private sidebarToggleBtn: HTMLElement | null = null;
|
||||
private isMobileDrawerOpen: boolean = false;
|
||||
private drawerOverlay: HTMLElement | null = null;
|
||||
|
||||
// Touch gesture tracking
|
||||
private touchStartX: number = 0;
|
||||
private touchStartY: number = 0;
|
||||
private touchCurrentX: number = 0;
|
||||
private isSwiping: boolean = false;
|
||||
private swipeThreshold: number = 50;
|
||||
|
||||
// V2 Details panel
|
||||
private detailsPanelEl: HTMLElement | null = null;
|
||||
|
|
@ -209,7 +218,7 @@ export class TaskViewV2 extends ItemView {
|
|||
await this.applyWorkspaceSettings();
|
||||
this.restoreFilterStateFromWorkspace();
|
||||
await this.loadTasks();
|
||||
this.scheduleUpdate('workspace-switch');
|
||||
this.scheduleUpdate("workspace-switch");
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
@ -219,7 +228,7 @@ export class TaskViewV2 extends ItemView {
|
|||
if (payload.workspaceId === this.workspaceId) {
|
||||
await this.applyWorkspaceSettings();
|
||||
await this.loadTasks();
|
||||
this.scheduleUpdate('workspace-overrides');
|
||||
this.scheduleUpdate("workspace-overrides");
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
@ -274,6 +283,11 @@ export class TaskViewV2 extends ItemView {
|
|||
cls: "tg-v2-container",
|
||||
});
|
||||
|
||||
// Add mobile class to container for proper styling
|
||||
if (Platform.isPhone) {
|
||||
this.rootContainerEl.addClass("is-mobile");
|
||||
}
|
||||
|
||||
// Create layout structure
|
||||
const layoutContainer = this.rootContainerEl.createDiv({
|
||||
cls: "tg-v2-layout",
|
||||
|
|
@ -284,6 +298,22 @@ export class TaskViewV2 extends ItemView {
|
|||
cls: "tg-v2-sidebar-container",
|
||||
});
|
||||
|
||||
// Add mobile-specific classes and overlay
|
||||
if (Platform.isPhone) {
|
||||
sidebarEl.addClass("is-mobile-drawer");
|
||||
// Create overlay for mobile drawer
|
||||
this.drawerOverlay = layoutContainer.createDiv({
|
||||
cls: "drawer-overlay",
|
||||
});
|
||||
this.drawerOverlay.style.display = "none";
|
||||
this.drawerOverlay.addEventListener("click", () => {
|
||||
this.closeMobileDrawer();
|
||||
});
|
||||
|
||||
// Add swipe gesture support
|
||||
this.initializeMobileSwipeGestures();
|
||||
}
|
||||
|
||||
// Main content area
|
||||
const mainContainer = layoutContainer.createDiv({
|
||||
cls: "tg-v2-main-container",
|
||||
|
|
@ -367,16 +397,24 @@ export class TaskViewV2 extends ItemView {
|
|||
);
|
||||
|
||||
// Single initial render
|
||||
console.log("[TG-V2] Performing initial render with:", this.currentViewId);
|
||||
console.log(
|
||||
"[TG-V2] Performing initial render with:",
|
||||
this.currentViewId
|
||||
);
|
||||
// Keep isInitializing true during first switchView
|
||||
this.switchView(this.currentViewId);
|
||||
|
||||
// Refresh top navigation (badge) after tasks are loaded
|
||||
this.topNavigation?.refresh();
|
||||
|
||||
// Sidebar toggle in header and responsive collapse
|
||||
// Sidebar toggle in header (desktop) and responsive collapse
|
||||
this.createSidebarToggle();
|
||||
this.checkAndCollapseSidebar();
|
||||
// Only check and collapse on desktop
|
||||
if (!Platform.isPhone) {
|
||||
this.checkAndCollapseSidebar();
|
||||
}
|
||||
|
||||
this.createTaskMark();
|
||||
|
||||
// Create action buttons in Obsidian view header
|
||||
console.log("[TG-V2] Creating action buttons");
|
||||
|
|
@ -388,12 +426,29 @@ export class TaskViewV2 extends ItemView {
|
|||
}
|
||||
|
||||
private initializeSidebar(containerEl: HTMLElement) {
|
||||
// On mobile, start with sidebar completely hidden (drawer closed)
|
||||
const initialCollapsedState = Platform.isPhone
|
||||
? true
|
||||
: this.isSidebarCollapsed;
|
||||
|
||||
this.sidebar = new V2Sidebar(
|
||||
containerEl,
|
||||
this.plugin,
|
||||
(viewId) => this.handleNavigate(viewId),
|
||||
(projectId) => this.handleProjectSelect(projectId),
|
||||
this.isSidebarCollapsed
|
||||
(viewId) => {
|
||||
this.handleNavigate(viewId);
|
||||
// Auto-close drawer on mobile after navigation
|
||||
if (Platform.isPhone) {
|
||||
this.closeMobileDrawer();
|
||||
}
|
||||
},
|
||||
(projectId) => {
|
||||
this.handleProjectSelect(projectId);
|
||||
// Auto-close drawer on mobile after selection
|
||||
if (Platform.isPhone) {
|
||||
this.closeMobileDrawer();
|
||||
}
|
||||
},
|
||||
initialCollapsedState
|
||||
);
|
||||
// Add sidebar as a child component for proper lifecycle management
|
||||
this.addChild(this.sidebar);
|
||||
|
|
@ -413,7 +468,8 @@ export class TaskViewV2 extends ItemView {
|
|||
() => {}, // Filter is now in Obsidian view header
|
||||
() => {}, // Sort is now in Obsidian view header
|
||||
() => this.handleSettingsClick(),
|
||||
availableModes
|
||||
availableModes,
|
||||
() => this.toggleSidebar() // Pass sidebar toggle callback
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -604,9 +660,11 @@ export class TaskViewV2 extends ItemView {
|
|||
}
|
||||
|
||||
private createSidebarToggle() {
|
||||
const headerBtns = this.headerEl?.find(
|
||||
".view-header-nav-buttons"
|
||||
) as HTMLElement | null;
|
||||
const headerBtns = !Platform.isPhone
|
||||
? (this.headerEl?.find(
|
||||
".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");
|
||||
return;
|
||||
|
|
@ -618,27 +676,148 @@ export class TaskViewV2 extends ItemView {
|
|||
cls: "panel-toggle-btn",
|
||||
});
|
||||
const btn = new ButtonComponent(this.sidebarToggleBtn);
|
||||
btn.setIcon("panel-left-dashed")
|
||||
btn.setIcon(Platform.isPhone ? "menu" : "panel-left-dashed")
|
||||
.setTooltip(t("Toggle Sidebar"))
|
||||
.setClass("clickable-icon")
|
||||
.onClick(() => this.toggleSidebar());
|
||||
}
|
||||
|
||||
private toggleSidebar() {
|
||||
this.isSidebarCollapsed = !this.isSidebarCollapsed;
|
||||
this.sidebar?.setCollapsed(this.isSidebarCollapsed);
|
||||
this.rootContainerEl?.toggleClass(
|
||||
"v2-sidebar-collapsed",
|
||||
this.isSidebarCollapsed
|
||||
private createTaskMark() {
|
||||
this.titleEl.setText(
|
||||
t("{{num}} Tasks", {
|
||||
interpolation: {
|
||||
num: this.tasks.length,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private toggleSidebar() {
|
||||
if (Platform.isPhone) {
|
||||
// On mobile, toggle the drawer open/closed
|
||||
if (this.isMobileDrawerOpen) {
|
||||
this.closeMobileDrawer();
|
||||
} else {
|
||||
this.openMobileDrawer();
|
||||
}
|
||||
} else {
|
||||
// On desktop, toggle collapse state
|
||||
this.isSidebarCollapsed = !this.isSidebarCollapsed;
|
||||
this.sidebar?.setCollapsed(this.isSidebarCollapsed);
|
||||
this.rootContainerEl?.toggleClass(
|
||||
"v2-sidebar-collapsed",
|
||||
this.isSidebarCollapsed
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private openMobileDrawer() {
|
||||
this.isMobileDrawerOpen = true;
|
||||
this.rootContainerEl?.addClass("drawer-open");
|
||||
if (this.drawerOverlay) {
|
||||
this.drawerOverlay.style.display = "block";
|
||||
}
|
||||
// Show the sidebar
|
||||
this.sidebar?.setCollapsed(false);
|
||||
}
|
||||
|
||||
private closeMobileDrawer() {
|
||||
this.isMobileDrawerOpen = false;
|
||||
this.rootContainerEl?.removeClass("drawer-open");
|
||||
if (this.drawerOverlay) {
|
||||
this.drawerOverlay.style.display = "none";
|
||||
}
|
||||
// Hide the sidebar
|
||||
this.sidebar?.setCollapsed(true);
|
||||
}
|
||||
|
||||
private initializeMobileSwipeGestures() {
|
||||
if (!Platform.isPhone) return;
|
||||
|
||||
// Edge swipe to open drawer
|
||||
this.registerDomEvent(document, "touchstart", (e: TouchEvent) => {
|
||||
if (this.isMobileDrawerOpen) {
|
||||
// Track for swipe-to-close when drawer is open
|
||||
const touch = e.touches[0];
|
||||
this.touchStartX = touch.clientX;
|
||||
this.touchStartY = touch.clientY;
|
||||
this.isSwiping = true;
|
||||
} else {
|
||||
// Check if touch started from left edge
|
||||
const touch = e.touches[0];
|
||||
if (touch.clientX < 20) {
|
||||
// 20px edge detection zone
|
||||
this.touchStartX = touch.clientX;
|
||||
this.touchStartY = touch.clientY;
|
||||
this.isSwiping = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.registerDomEvent(document, "touchmove", (e: TouchEvent) => {
|
||||
if (!this.isSwiping) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
this.touchCurrentX = touch.clientX;
|
||||
const deltaX = this.touchCurrentX - this.touchStartX;
|
||||
const deltaY = Math.abs(touch.clientY - this.touchStartY);
|
||||
|
||||
// Check if horizontal swipe (not vertical scroll)
|
||||
if (deltaY > 50) {
|
||||
this.isSwiping = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isMobileDrawerOpen && deltaX > this.swipeThreshold) {
|
||||
// Swipe right from edge - open drawer
|
||||
this.openMobileDrawer();
|
||||
this.isSwiping = false;
|
||||
} else if (
|
||||
this.isMobileDrawerOpen &&
|
||||
deltaX < -this.swipeThreshold
|
||||
) {
|
||||
// Swipe left when drawer is open - close it
|
||||
const sidebarEl = this.rootContainerEl?.querySelector(
|
||||
".tg-v2-sidebar-container"
|
||||
);
|
||||
if (sidebarEl) {
|
||||
const sidebarRect = sidebarEl.getBoundingClientRect();
|
||||
// Only close if swipe started on the sidebar
|
||||
if (this.touchStartX < sidebarRect.right) {
|
||||
this.closeMobileDrawer();
|
||||
this.isSwiping = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.registerDomEvent(document, "touchend", () => {
|
||||
this.isSwiping = false;
|
||||
this.touchStartX = 0;
|
||||
this.touchCurrentX = 0;
|
||||
});
|
||||
|
||||
this.registerDomEvent(document, "touchcancel", () => {
|
||||
this.isSwiping = false;
|
||||
this.touchStartX = 0;
|
||||
this.touchCurrentX = 0;
|
||||
});
|
||||
}
|
||||
|
||||
onResize(): void {
|
||||
this.checkAndCollapseSidebar();
|
||||
// Only check and collapse on desktop
|
||||
if (!Platform.isPhone) {
|
||||
this.checkAndCollapseSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
private checkAndCollapseSidebar() {
|
||||
// Auto-collapse on narrow panes
|
||||
// Skip auto-collapse on mobile, as we use drawer mode
|
||||
if (Platform.isPhone) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-collapse on narrow panes (desktop only)
|
||||
try {
|
||||
const width = (this.leaf as any)?.width ?? 0;
|
||||
if (width > 0 && width < 768) {
|
||||
|
|
@ -656,18 +835,25 @@ export class TaskViewV2 extends ItemView {
|
|||
|
||||
if (this.isInitializing && !forceHideAll) {
|
||||
// Skip smart hiding during initialization unless forced
|
||||
console.log("[TG-V2] Skipping hideAllComponents during initialization (not initial hide)");
|
||||
console.log(
|
||||
"[TG-V2] Skipping hideAllComponents during initialization (not initial hide)"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Smart hiding - only hide currently visible component (unless initial hide)
|
||||
if (!isInitialHide && this.currentVisibleComponent) {
|
||||
console.log("[TG-V2] Smart hide - only hiding current visible component");
|
||||
console.log(
|
||||
"[TG-V2] Smart hide - only hiding current visible component"
|
||||
);
|
||||
this.currentVisibleComponent.containerEl?.hide();
|
||||
this.currentVisibleComponent = null;
|
||||
} else {
|
||||
// Hide all components (during initial setup or when no current visible tracked)
|
||||
console.log("[TG-V2] Hiding all components", isInitialHide ? "(initial hide)" : "");
|
||||
console.log(
|
||||
"[TG-V2] Hiding all components",
|
||||
isInitialHide ? "(initial hide)" : ""
|
||||
);
|
||||
this.contentComponent?.containerEl.hide();
|
||||
this.forecastComponent?.containerEl.hide();
|
||||
this.tagsComponent?.containerEl.hide();
|
||||
|
|
@ -696,7 +882,7 @@ export class TaskViewV2 extends ItemView {
|
|||
console.log("[TG-V2] debouncedViewUpdate triggered");
|
||||
if (!this.isInitializing) {
|
||||
await this.loadTasks(false); // Don't show loading state for updates
|
||||
this.scheduleUpdate('dataflow');
|
||||
this.scheduleUpdate("dataflow");
|
||||
// Refresh top navigation (badge)
|
||||
this.topNavigation?.refresh();
|
||||
// Also refresh project list when tasks update
|
||||
|
|
@ -707,7 +893,7 @@ export class TaskViewV2 extends ItemView {
|
|||
// Add debounced filter application
|
||||
const debouncedApplyFilter = debounce(() => {
|
||||
if (!this.isInitializing) {
|
||||
this.scheduleUpdate('filter-changed');
|
||||
this.scheduleUpdate("filter-changed");
|
||||
}
|
||||
}, 400);
|
||||
|
||||
|
|
@ -808,7 +994,11 @@ export class TaskViewV2 extends ItemView {
|
|||
try {
|
||||
console.log("[TG-V2] loadTasks started, showLoading:", showLoading);
|
||||
// Only show loading state if requested, not initializing, and we don't have tasks
|
||||
if (showLoading && !this.isInitializing && (!this.tasks || this.tasks.length === 0)) {
|
||||
if (
|
||||
showLoading &&
|
||||
!this.isInitializing &&
|
||||
(!this.tasks || this.tasks.length === 0)
|
||||
) {
|
||||
console.log("[TG-V2] Setting isLoading to true");
|
||||
this.isLoading = true;
|
||||
this.loadError = null;
|
||||
|
|
@ -1033,8 +1223,15 @@ export class TaskViewV2 extends ItemView {
|
|||
);
|
||||
|
||||
// Skip if we're switching to the same view (but never skip during initialization)
|
||||
if (!this.isInitializing && this.currentViewId === viewId && !project && this.filteredTasks.length > 0) {
|
||||
console.log("[TG-V2] Already on this view with data, skipping switchView");
|
||||
if (
|
||||
!this.isInitializing &&
|
||||
this.currentViewId === viewId &&
|
||||
!project &&
|
||||
this.filteredTasks.length > 0
|
||||
) {
|
||||
console.log(
|
||||
"[TG-V2] Already on this view with data, skipping switchView"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1242,7 +1439,10 @@ export class TaskViewV2 extends ItemView {
|
|||
}
|
||||
|
||||
// Apply filters only if needed (not during initialization as they're already applied)
|
||||
if (!this.isInitializing && (this.filteredTasks.length === 0 || project)) {
|
||||
if (
|
||||
!this.isInitializing &&
|
||||
(this.filteredTasks.length === 0 || project)
|
||||
) {
|
||||
console.log("[TG-V2] Applying filters in switchView");
|
||||
this.applyFilters();
|
||||
console.log(
|
||||
|
|
@ -1264,7 +1464,10 @@ export class TaskViewV2 extends ItemView {
|
|||
`[TG-V2] Calling setTasks for ${viewId} with ALL tasks (unfiltered):`,
|
||||
this.tasks.length,
|
||||
"tasks, first few:",
|
||||
this.tasks.slice(0, 3).map(t => ({ id: t.id, project: t.metadata?.project }))
|
||||
this.tasks.slice(0, 3).map((t) => ({
|
||||
id: t.id,
|
||||
project: t.metadata?.project,
|
||||
}))
|
||||
);
|
||||
// ReviewComponent and TagsComponent need all tasks to build their lists
|
||||
// and only accept a single parameter
|
||||
|
|
@ -1654,7 +1857,7 @@ export class TaskViewV2 extends ItemView {
|
|||
private updateView() {
|
||||
// Schedule an update instead of immediately updating
|
||||
if (!this.isInitializing) {
|
||||
this.scheduleUpdate('updateView');
|
||||
this.scheduleUpdate("updateView");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1939,14 +2142,17 @@ export class TaskViewV2 extends ItemView {
|
|||
if (viewId === "new-task") {
|
||||
new QuickCaptureModal(this.app, this.plugin).open();
|
||||
} else {
|
||||
console.log(`[TG-V2] handleNavigate to ${viewId}, current tasks:`, this.tasks.length);
|
||||
console.log(
|
||||
`[TG-V2] handleNavigate to ${viewId}, current tasks:`,
|
||||
this.tasks.length
|
||||
);
|
||||
this.switchView(viewId);
|
||||
}
|
||||
}
|
||||
|
||||
private handleSearch(query: string) {
|
||||
this.viewState.searchQuery = query;
|
||||
this.scheduleUpdate('search');
|
||||
this.scheduleUpdate("search");
|
||||
// Persist and update header UI
|
||||
this.saveFilterStateToWorkspace();
|
||||
this.updateActionButtons();
|
||||
|
|
@ -2078,6 +2284,40 @@ export class TaskViewV2 extends ItemView {
|
|||
visible ? t("Hide Details") : t("Show Details")
|
||||
);
|
||||
}
|
||||
|
||||
// On mobile, add click handler to overlay to close details
|
||||
if (Platform.isPhone && visible) {
|
||||
// Use setTimeout to avoid immediate close on open
|
||||
setTimeout(() => {
|
||||
const overlayClickHandler = (e: MouseEvent) => {
|
||||
// Check if click is on the overlay (pseudo-element area)
|
||||
const detailsEl =
|
||||
this.rootContainerEl?.querySelector(".task-details");
|
||||
if (detailsEl && !detailsEl.contains(e.target as Node)) {
|
||||
this.toggleDetailsVisibility(false);
|
||||
document.removeEventListener(
|
||||
"click",
|
||||
overlayClickHandler
|
||||
);
|
||||
}
|
||||
};
|
||||
// Add the event listener
|
||||
document.addEventListener("click", overlayClickHandler);
|
||||
// Store the handler to remove it later
|
||||
(this as any).mobileDetailsOverlayHandler = overlayClickHandler;
|
||||
}, 100);
|
||||
} else if (
|
||||
Platform.isPhone &&
|
||||
!visible &&
|
||||
(this as any).mobileDetailsOverlayHandler
|
||||
) {
|
||||
// Remove the event listener when closing
|
||||
document.removeEventListener(
|
||||
"click",
|
||||
(this as any).mobileDetailsOverlayHandler
|
||||
);
|
||||
delete (this as any).mobileDetailsOverlayHandler;
|
||||
}
|
||||
}
|
||||
|
||||
private resetCurrentFilter() {
|
||||
|
|
@ -2552,10 +2792,12 @@ export class TaskViewV2 extends ItemView {
|
|||
);
|
||||
|
||||
// Check what kind of updates are pending
|
||||
const needsFullReload = this.pendingUpdates.has('dataflow') ||
|
||||
this.pendingUpdates.has('workspace-switch');
|
||||
const needsFilterRefresh = this.pendingUpdates.has('filter-changed') ||
|
||||
this.pendingUpdates.has('search');
|
||||
const needsFullReload =
|
||||
this.pendingUpdates.has("dataflow") ||
|
||||
this.pendingUpdates.has("workspace-switch");
|
||||
const needsFilterRefresh =
|
||||
this.pendingUpdates.has("filter-changed") ||
|
||||
this.pendingUpdates.has("search");
|
||||
|
||||
this.pendingUpdates.clear();
|
||||
this.updateScheduled = false;
|
||||
|
|
@ -2807,6 +3049,15 @@ export class TaskViewV2 extends ItemView {
|
|||
this.saveWorkspaceLayout();
|
||||
this.saveFilterStateToWorkspace();
|
||||
|
||||
// Clean up mobile event listeners
|
||||
if (Platform.isPhone && (this as any).mobileDetailsOverlayHandler) {
|
||||
document.removeEventListener(
|
||||
"click",
|
||||
(this as any).mobileDetailsOverlayHandler
|
||||
);
|
||||
delete (this as any).mobileDetailsOverlayHandler;
|
||||
}
|
||||
|
||||
// Clean up components
|
||||
if (this.kanbanComponent) {
|
||||
this.kanbanComponent.unload();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
import { Component, Platform, setIcon, Menu, Modal, App } from "obsidian";
|
||||
import TaskProgressBarPlugin from "../../../index";
|
||||
import { Task } from "../../../types/task";
|
||||
import { ProjectPopover, ProjectModal, EditProjectModal } from "./ProjectPopover";
|
||||
import {
|
||||
ProjectPopover,
|
||||
ProjectModal,
|
||||
EditProjectModal,
|
||||
} from "./ProjectPopover";
|
||||
import type { CustomProject } from "../../../common/setting-definition";
|
||||
import { t } from "@/translations/helper";
|
||||
|
||||
|
|
@ -102,7 +106,7 @@ export class ProjectList extends Component {
|
|||
if (projectName) {
|
||||
if (!projectMap.has(projectName)) {
|
||||
// Convert dashes back to spaces for display
|
||||
const displayName = projectName.replace(/-/g, ' ');
|
||||
const displayName = projectName.replace(/-/g, " ");
|
||||
projectMap.set(projectName, {
|
||||
id: projectName,
|
||||
name: projectName,
|
||||
|
|
@ -182,9 +186,15 @@ export class ProjectList extends Component {
|
|||
this.projects.sort((a, b) => {
|
||||
switch (this.currentSort) {
|
||||
case "name-asc":
|
||||
return this.collator.compare(a.displayName || a.name, b.displayName || b.name);
|
||||
return this.collator.compare(
|
||||
a.displayName || a.name,
|
||||
b.displayName || b.name
|
||||
);
|
||||
case "name-desc":
|
||||
return this.collator.compare(b.displayName || b.name, a.displayName || a.name);
|
||||
return this.collator.compare(
|
||||
b.displayName || b.name,
|
||||
a.displayName || a.name
|
||||
);
|
||||
case "tasks-asc":
|
||||
return a.taskCount - b.taskCount;
|
||||
case "tasks-desc":
|
||||
|
|
@ -205,7 +215,7 @@ export class ProjectList extends Component {
|
|||
const separator = this.plugin.settings.projectPathSeparator || "/";
|
||||
|
||||
// Process each project and create intermediate nodes as needed
|
||||
this.projects.forEach(project => {
|
||||
this.projects.forEach((project) => {
|
||||
const segments = this.parseProjectPath(project.name);
|
||||
if (segments.length === 0) return;
|
||||
|
||||
|
|
@ -218,21 +228,27 @@ export class ProjectList extends Component {
|
|||
const isLeaf = i === segments.length - 1;
|
||||
|
||||
// Build the full path up to this segment
|
||||
currentPath = currentPath ? `${currentPath}${separator}${segment}` : segment;
|
||||
currentPath = currentPath
|
||||
? `${currentPath}${separator}${segment}`
|
||||
: segment;
|
||||
|
||||
// Check if node already exists
|
||||
let node = nodeMap.get(currentPath);
|
||||
|
||||
if (!node) {
|
||||
// Create node - use actual project for leaf, virtual for intermediate
|
||||
const nodeProject = isLeaf ? project : {
|
||||
id: currentPath,
|
||||
name: currentPath,
|
||||
displayName: segment,
|
||||
color: this.generateColorForProject(currentPath),
|
||||
taskCount: 0,
|
||||
isVirtual: true
|
||||
};
|
||||
const nodeProject = isLeaf
|
||||
? project
|
||||
: {
|
||||
id: currentPath,
|
||||
name: currentPath,
|
||||
displayName: segment,
|
||||
color: this.generateColorForProject(
|
||||
currentPath
|
||||
),
|
||||
taskCount: 0,
|
||||
isVirtual: true,
|
||||
};
|
||||
|
||||
node = {
|
||||
project: nodeProject,
|
||||
|
|
@ -241,7 +257,7 @@ export class ProjectList extends Component {
|
|||
expanded: this.expandedNodes.has(currentPath),
|
||||
path: segments.slice(0, i + 1),
|
||||
fullPath: currentPath,
|
||||
parent: parentNode
|
||||
parent: parentNode,
|
||||
};
|
||||
|
||||
nodeMap.set(currentPath, node);
|
||||
|
|
@ -280,8 +296,19 @@ export class ProjectList extends Component {
|
|||
// Normalize the path by trimming and removing duplicate separators
|
||||
const normalized = projectName
|
||||
.trim()
|
||||
.replace(new RegExp(`${this.escapeRegExp(separator)}+`, 'g'), separator)
|
||||
.replace(new RegExp(`^${this.escapeRegExp(separator)}|${this.escapeRegExp(separator)}$`, 'g'), '');
|
||||
.replace(
|
||||
new RegExp(`${this.escapeRegExp(separator)}+`, "g"),
|
||||
separator
|
||||
)
|
||||
.replace(
|
||||
new RegExp(
|
||||
`^${this.escapeRegExp(separator)}|${this.escapeRegExp(
|
||||
separator
|
||||
)}$`,
|
||||
"g"
|
||||
),
|
||||
""
|
||||
);
|
||||
|
||||
if (!normalized) {
|
||||
return [];
|
||||
|
|
@ -291,11 +318,11 @@ export class ProjectList extends Component {
|
|||
}
|
||||
|
||||
private escapeRegExp(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
private sortTreeNodes(nodes: ProjectTreeNode[]) {
|
||||
nodes.forEach(node => {
|
||||
nodes.forEach((node) => {
|
||||
if (node.children.length > 0) {
|
||||
this.sortTreeNodes(node.children);
|
||||
}
|
||||
|
|
@ -318,9 +345,13 @@ export class ProjectList extends Component {
|
|||
case "tasks-desc":
|
||||
return b.project.taskCount - a.project.taskCount;
|
||||
case "created-asc":
|
||||
return (a.project.createdAt || 0) - (b.project.createdAt || 0);
|
||||
return (
|
||||
(a.project.createdAt || 0) - (b.project.createdAt || 0)
|
||||
);
|
||||
case "created-desc":
|
||||
return (b.project.createdAt || 0) - (a.project.createdAt || 0);
|
||||
return (
|
||||
(b.project.createdAt || 0) - (a.project.createdAt || 0)
|
||||
);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -328,7 +359,7 @@ export class ProjectList extends Component {
|
|||
}
|
||||
|
||||
private updateParentTaskCounts(nodes: ProjectTreeNode[]) {
|
||||
nodes.forEach(node => {
|
||||
nodes.forEach((node) => {
|
||||
if (node.children.length > 0) {
|
||||
this.updateParentTaskCounts(node.children);
|
||||
// Sum up child task counts
|
||||
|
|
@ -341,7 +372,8 @@ export class ProjectList extends Component {
|
|||
if (node.project.isVirtual) {
|
||||
node.project.taskCount = childTotal;
|
||||
} else {
|
||||
node.project.taskCount = node.project.taskCount + childTotal;
|
||||
node.project.taskCount =
|
||||
node.project.taskCount + childTotal;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -420,10 +452,20 @@ export class ProjectList extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
private renderTreeNodes(container: HTMLElement, nodes: ProjectTreeNode[], level: number) {
|
||||
nodes.forEach(node => {
|
||||
private renderTreeNodes(
|
||||
container: HTMLElement,
|
||||
nodes: ProjectTreeNode[],
|
||||
level: number
|
||||
) {
|
||||
nodes.forEach((node) => {
|
||||
const hasChildren = node.children.length > 0;
|
||||
this.renderProjectItem(container, node.project, level, hasChildren, node);
|
||||
this.renderProjectItem(
|
||||
container,
|
||||
node.project,
|
||||
level,
|
||||
hasChildren,
|
||||
node
|
||||
);
|
||||
|
||||
if (hasChildren && node.expanded) {
|
||||
this.renderTreeNodes(container, node.children, level + 1);
|
||||
|
|
@ -442,7 +484,7 @@ export class ProjectList extends Component {
|
|||
cls: "v2-project-item",
|
||||
attr: {
|
||||
"data-project-id": project.id,
|
||||
"data-level": String(level)
|
||||
"data-level": String(level),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -492,16 +534,24 @@ export class ProjectList extends Component {
|
|||
displayText = project.displayName || project.name;
|
||||
} else {
|
||||
// For real projects, extract the last segment
|
||||
const separator = this.plugin.settings.projectPathSeparator || "/";
|
||||
const separator =
|
||||
this.plugin.settings.projectPathSeparator || "/";
|
||||
const nameToSplit = project.name;
|
||||
const segments = nameToSplit.split(separator);
|
||||
const lastSegment = segments[segments.length - 1] || project.name;
|
||||
const lastSegment =
|
||||
segments[segments.length - 1] || project.name;
|
||||
|
||||
// If project has a custom displayName, try to preserve it
|
||||
// but still show only the relevant part for the tree level
|
||||
if (project.displayName && project.displayName !== project.name) {
|
||||
const displaySegments = project.displayName.split(separator);
|
||||
displayText = displaySegments[displaySegments.length - 1] || lastSegment;
|
||||
if (
|
||||
project.displayName &&
|
||||
project.displayName !== project.name
|
||||
) {
|
||||
const displaySegments =
|
||||
project.displayName.split(separator);
|
||||
displayText =
|
||||
displaySegments[displaySegments.length - 1] ||
|
||||
lastSegment;
|
||||
} else {
|
||||
displayText = lastSegment;
|
||||
}
|
||||
|
|
@ -523,7 +573,7 @@ export class ProjectList extends Component {
|
|||
|
||||
this.registerDomEvent(projectItem, "click", (e: MouseEvent) => {
|
||||
// Don't trigger if clicking on chevron
|
||||
if (!(e.target as HTMLElement).closest('.v2-project-chevron')) {
|
||||
if (!(e.target as HTMLElement).closest(".v2-project-chevron")) {
|
||||
// Virtual nodes select all their children
|
||||
if (project.isVirtual && treeNode) {
|
||||
this.selectVirtualNode(treeNode);
|
||||
|
|
@ -536,10 +586,14 @@ export class ProjectList extends Component {
|
|||
|
||||
// Add context menu handler (only for non-virtual projects)
|
||||
if (!project.isVirtual) {
|
||||
this.registerDomEvent(projectItem, "contextmenu", (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.showProjectContextMenu(e, project);
|
||||
});
|
||||
this.registerDomEvent(
|
||||
projectItem,
|
||||
"contextmenu",
|
||||
(e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
this.showProjectContextMenu(e, project);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -550,7 +604,7 @@ export class ProjectList extends Component {
|
|||
if (!n.project.isVirtual) {
|
||||
projectIds.push(n.project.id);
|
||||
}
|
||||
n.children.forEach(child => collectProjects(child));
|
||||
n.children.forEach((child) => collectProjects(child));
|
||||
};
|
||||
collectProjects(node);
|
||||
|
||||
|
|
@ -593,7 +647,7 @@ export class ProjectList extends Component {
|
|||
this.currentPopover = null;
|
||||
}
|
||||
|
||||
if (Platform.isMobile) {
|
||||
if (Platform.isPhone) {
|
||||
// Mobile: Use Obsidian Modal
|
||||
const modal = new ProjectModal(
|
||||
this.plugin.app,
|
||||
|
|
@ -681,7 +735,8 @@ export class ProjectList extends Component {
|
|||
this.projects.push({
|
||||
id: customProject.id,
|
||||
name: customProject.name,
|
||||
displayName: customProject.displayName || customProject.name,
|
||||
displayName:
|
||||
customProject.displayName || customProject.name,
|
||||
color: customProject.color,
|
||||
taskCount: 0, // Will be updated by task counting
|
||||
createdAt: customProject.createdAt,
|
||||
|
|
@ -691,7 +746,8 @@ export class ProjectList extends Component {
|
|||
// Update existing project with custom color
|
||||
this.projects[existingIndex].id = customProject.id;
|
||||
this.projects[existingIndex].color = customProject.color;
|
||||
this.projects[existingIndex].displayName = customProject.displayName || customProject.name;
|
||||
this.projects[existingIndex].displayName =
|
||||
customProject.displayName || customProject.name;
|
||||
this.projects[existingIndex].createdAt =
|
||||
customProject.createdAt;
|
||||
this.projects[existingIndex].updatedAt =
|
||||
|
|
@ -709,7 +765,11 @@ export class ProjectList extends Component {
|
|||
icon: string;
|
||||
}[] = [
|
||||
{ label: t("Name (A-Z)"), value: "name-asc", icon: "arrow-up-a-z" },
|
||||
{ label: t("Name (Z-A)"), value: "name-desc", icon: "arrow-down-a-z" },
|
||||
{
|
||||
label: t("Name (Z-A)"),
|
||||
value: "name-desc",
|
||||
icon: "arrow-down-a-z",
|
||||
},
|
||||
{
|
||||
label: t("Tasks (Low to High)"),
|
||||
value: "tasks-asc",
|
||||
|
|
@ -763,15 +823,14 @@ export class ProjectList extends Component {
|
|||
const menu = new Menu();
|
||||
|
||||
// Check if this is a custom project
|
||||
const isCustomProject = this.plugin.settings.projectConfig?.customProjects?.some(
|
||||
cp => cp.id === project.id || cp.name === project.name
|
||||
);
|
||||
const isCustomProject =
|
||||
this.plugin.settings.projectConfig?.customProjects?.some(
|
||||
(cp) => cp.id === project.id || cp.name === project.name
|
||||
);
|
||||
|
||||
// Edit Project option
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("Edit Project"))
|
||||
.setIcon("edit");
|
||||
item.setTitle(t("Edit Project")).setIcon("edit");
|
||||
|
||||
if (isCustomProject) {
|
||||
item.onClick(() => {
|
||||
|
|
@ -784,9 +843,7 @@ export class ProjectList extends Component {
|
|||
|
||||
// Delete Project option
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("Delete Project"))
|
||||
.setIcon("trash");
|
||||
item.setTitle(t("Delete Project")).setIcon("trash");
|
||||
|
||||
if (isCustomProject) {
|
||||
item.onClick(() => {
|
||||
|
|
@ -802,9 +859,10 @@ export class ProjectList extends Component {
|
|||
|
||||
private editProject(project: Project) {
|
||||
// Find the custom project data
|
||||
let customProject = this.plugin.settings.projectConfig?.customProjects?.find(
|
||||
cp => cp.id === project.id || cp.name === project.name
|
||||
);
|
||||
let customProject =
|
||||
this.plugin.settings.projectConfig?.customProjects?.find(
|
||||
(cp) => cp.id === project.id || cp.name === project.name
|
||||
);
|
||||
|
||||
if (!customProject) {
|
||||
// Create a new custom project entry if it doesn't exist
|
||||
|
|
@ -814,7 +872,7 @@ export class ProjectList extends Component {
|
|||
displayName: project.displayName || project.name,
|
||||
color: project.color,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -860,14 +918,18 @@ export class ProjectList extends Component {
|
|||
}
|
||||
|
||||
// Find and update the project
|
||||
const index = this.plugin.settings.projectConfig.customProjects.findIndex(
|
||||
cp => cp.id === updatedProject.id
|
||||
);
|
||||
const index =
|
||||
this.plugin.settings.projectConfig.customProjects.findIndex(
|
||||
(cp) => cp.id === updatedProject.id
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
this.plugin.settings.projectConfig.customProjects[index] = updatedProject;
|
||||
this.plugin.settings.projectConfig.customProjects[index] =
|
||||
updatedProject;
|
||||
} else {
|
||||
this.plugin.settings.projectConfig.customProjects.push(updatedProject);
|
||||
this.plugin.settings.projectConfig.customProjects.push(
|
||||
updatedProject
|
||||
);
|
||||
}
|
||||
|
||||
// Save settings
|
||||
|
|
@ -891,23 +953,29 @@ export class ProjectList extends Component {
|
|||
const { contentEl } = this;
|
||||
contentEl.createEl("h2", { text: t("Delete Project") });
|
||||
contentEl.createEl("p", {
|
||||
text: t(`Are you sure you want to delete "${project.displayName || project.name}"?`)
|
||||
text: t(
|
||||
`Are you sure you want to delete "${
|
||||
project.displayName || project.name
|
||||
}"?`
|
||||
),
|
||||
});
|
||||
contentEl.createEl("p", {
|
||||
cls: "mod-warning",
|
||||
text: t("This action cannot be undone.")
|
||||
text: t("This action cannot be undone."),
|
||||
});
|
||||
|
||||
const buttonContainer = contentEl.createDiv({ cls: "modal-button-container" });
|
||||
const buttonContainer = contentEl.createDiv({
|
||||
cls: "modal-button-container",
|
||||
});
|
||||
|
||||
const cancelBtn = buttonContainer.createEl("button", {
|
||||
text: t("Cancel")
|
||||
text: t("Cancel"),
|
||||
});
|
||||
cancelBtn.addEventListener("click", () => this.close());
|
||||
|
||||
const confirmBtn = buttonContainer.createEl("button", {
|
||||
text: t("Delete"),
|
||||
cls: "mod-warning"
|
||||
cls: "mod-warning",
|
||||
});
|
||||
confirmBtn.addEventListener("click", () => {
|
||||
this.onConfirm();
|
||||
|
|
@ -922,12 +990,16 @@ export class ProjectList extends Component {
|
|||
})(this.plugin.app, async () => {
|
||||
// Remove from custom projects
|
||||
if (this.plugin.settings.projectConfig?.customProjects) {
|
||||
const index = this.plugin.settings.projectConfig.customProjects.findIndex(
|
||||
cp => cp.id === project.id || cp.name === project.name
|
||||
);
|
||||
const index =
|
||||
this.plugin.settings.projectConfig.customProjects.findIndex(
|
||||
(cp) => cp.id === project.id || cp.name === project.name
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
this.plugin.settings.projectConfig.customProjects.splice(index, 1);
|
||||
this.plugin.settings.projectConfig.customProjects.splice(
|
||||
index,
|
||||
1
|
||||
);
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
// If this was the active project, clear selection
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, setIcon, Menu, Notice, Modal } from "obsidian";
|
||||
import { Component, setIcon, Menu, Notice, Modal, Platform } from "obsidian";
|
||||
import { WorkspaceSelector } from "./WorkspaceSelector";
|
||||
import { ProjectList } from "@/experimental/v2/components/ProjectList";
|
||||
import { V2NavigationItem } from "@/experimental/v2/types";
|
||||
|
|
@ -26,6 +26,8 @@ export class V2Sidebar extends Component {
|
|||
private collapsed: boolean = false;
|
||||
private currentWorkspaceId: string;
|
||||
private isTreeView: boolean = false;
|
||||
private otherViewsSection: HTMLElement | null = null;
|
||||
private railEl: HTMLElement | null = null;
|
||||
|
||||
private primaryItems: V2NavigationItem[] = [
|
||||
{ id: "inbox", label: t("Inbox"), icon: "inbox", type: "primary" },
|
||||
|
|
@ -81,92 +83,13 @@ export class V2Sidebar extends Component {
|
|||
this.containerEl.addClass("v2-sidebar");
|
||||
this.containerEl.toggleClass("is-collapsed", this.collapsed);
|
||||
|
||||
// Collapsed rail mode: show compact icons for workspace, views, projects, and add
|
||||
if (this.collapsed) {
|
||||
const rail = this.containerEl.createDiv({ cls: "v2-sidebar-rail" });
|
||||
|
||||
// Workspace menu button
|
||||
const wsBtn = rail.createDiv({
|
||||
cls: "v2-rail-btn",
|
||||
attr: { "aria-label": t("Workspace") },
|
||||
// Desktop: show rail mode when collapsed
|
||||
// Mobile: always render full sidebar (CSS handles visibility)
|
||||
if (this.collapsed && !Platform.isPhone) {
|
||||
this.railEl = this.containerEl.createDiv({
|
||||
cls: "v2-sidebar-rail",
|
||||
});
|
||||
setIcon(wsBtn, "layers");
|
||||
wsBtn.addEventListener("click", (e) =>
|
||||
this.showWorkspaceMenuWithManager(e as MouseEvent)
|
||||
);
|
||||
|
||||
// Primary view icons
|
||||
this.primaryItems.forEach((item) => {
|
||||
const btn = rail.createDiv({
|
||||
cls: "v2-rail-btn",
|
||||
attr: { "aria-label": item.label, "data-view-id": item.id },
|
||||
});
|
||||
setIcon(btn, item.icon);
|
||||
btn.addEventListener("click", () => {
|
||||
this.setActiveItem(item.id);
|
||||
this.onNavigate(item.id);
|
||||
});
|
||||
// Add context menu handler for rail button
|
||||
btn.addEventListener("contextmenu", (e) => {
|
||||
this.showViewContextMenu(e as MouseEvent, item.id);
|
||||
});
|
||||
});
|
||||
|
||||
// Other view icons with overflow menu when > 5
|
||||
const allOtherItems = this.computeOtherItems();
|
||||
const visibleCount =
|
||||
this.plugin?.settings?.experimental?.v2Config
|
||||
?.maxOtherViewsBeforeOverflow ?? 5;
|
||||
const displayedOther: V2NavigationItem[] = allOtherItems.slice(
|
||||
0,
|
||||
visibleCount
|
||||
);
|
||||
const remainingOther: V2NavigationItem[] =
|
||||
allOtherItems.slice(visibleCount);
|
||||
displayedOther.forEach((item: V2NavigationItem) => {
|
||||
const btn = rail.createDiv({
|
||||
cls: "v2-rail-btn",
|
||||
attr: { "aria-label": item.label, "data-view-id": item.id },
|
||||
});
|
||||
setIcon(btn, item.icon);
|
||||
btn.addEventListener("click", () => {
|
||||
this.setActiveItem(item.id);
|
||||
this.onNavigate(item.id);
|
||||
});
|
||||
// Add context menu handler for rail button
|
||||
btn.addEventListener("contextmenu", (e) => {
|
||||
this.showViewContextMenu(e as MouseEvent, item.id);
|
||||
});
|
||||
});
|
||||
if (remainingOther.length > 0) {
|
||||
const moreBtn = rail.createDiv({
|
||||
cls: "v2-rail-btn",
|
||||
attr: { "aria-label": t("More views") },
|
||||
});
|
||||
setIcon(moreBtn, "more-horizontal");
|
||||
moreBtn.addEventListener("click", (e) =>
|
||||
this.showOtherViewsMenu(e as MouseEvent, remainingOther)
|
||||
);
|
||||
}
|
||||
|
||||
// Projects menu button
|
||||
const projBtn = rail.createDiv({
|
||||
cls: "v2-rail-btn",
|
||||
attr: { "aria-label": t("Projects") },
|
||||
});
|
||||
setIcon(projBtn, "folder");
|
||||
projBtn.addEventListener("click", (e) =>
|
||||
this.showProjectMenu(e as MouseEvent)
|
||||
);
|
||||
|
||||
// Add (New Task) button
|
||||
const addBtn = rail.createDiv({
|
||||
cls: "v2-rail-btn",
|
||||
attr: { "aria-label": t("New Task") },
|
||||
});
|
||||
setIcon(addBtn, "plus");
|
||||
addBtn.addEventListener("click", () => this.onNavigate("new-task"));
|
||||
|
||||
this.renderRailMode();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -265,19 +188,131 @@ export class V2Sidebar extends Component {
|
|||
this.addChild(this.projectList);
|
||||
|
||||
// Other views section
|
||||
const otherSection = content.createDiv({ cls: "v2-sidebar-section" });
|
||||
const otherHeader = otherSection.createDiv({
|
||||
cls: "v2-section-header",
|
||||
this.otherViewsSection = content.createDiv({
|
||||
cls: "v2-sidebar-section",
|
||||
});
|
||||
this.renderOtherViewsSection();
|
||||
}
|
||||
|
||||
private renderRailMode() {
|
||||
if (!this.railEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing content
|
||||
this.railEl.empty();
|
||||
|
||||
// Workspace menu button
|
||||
const wsBtn = this.railEl.createDiv({
|
||||
cls: "v2-rail-btn",
|
||||
attr: { "aria-label": t("Workspace") },
|
||||
});
|
||||
setIcon(wsBtn, "layers");
|
||||
wsBtn.addEventListener("click", (e) =>
|
||||
this.showWorkspaceMenuWithManager(e as MouseEvent)
|
||||
);
|
||||
|
||||
// Primary view icons
|
||||
this.primaryItems.forEach((item) => {
|
||||
const btn = this.railEl!.createDiv({
|
||||
cls: "v2-rail-btn",
|
||||
attr: { "aria-label": item.label, "data-view-id": item.id },
|
||||
});
|
||||
setIcon(btn, item.icon);
|
||||
btn.addEventListener("click", () => {
|
||||
this.setActiveItem(item.id);
|
||||
this.onNavigate(item.id);
|
||||
});
|
||||
// Add context menu handler for rail button
|
||||
btn.addEventListener("contextmenu", (e) => {
|
||||
this.showViewContextMenu(e as MouseEvent, item.id);
|
||||
});
|
||||
});
|
||||
|
||||
// Other view icons with overflow menu when > 5
|
||||
const allOtherItems = this.computeOtherItems();
|
||||
const visibleCount = 5;
|
||||
const visibleCount =
|
||||
this.plugin?.settings?.experimental?.v2Config
|
||||
?.maxOtherViewsBeforeOverflow ?? 5;
|
||||
const displayedOther: V2NavigationItem[] = allOtherItems.slice(
|
||||
0,
|
||||
visibleCount
|
||||
);
|
||||
const remainingOther: V2NavigationItem[] =
|
||||
allOtherItems.slice(visibleCount);
|
||||
|
||||
displayedOther.forEach((item: V2NavigationItem) => {
|
||||
const btn = this.railEl!.createDiv({
|
||||
cls: "v2-rail-btn",
|
||||
attr: { "aria-label": item.label, "data-view-id": item.id },
|
||||
});
|
||||
setIcon(btn, item.icon);
|
||||
btn.addEventListener("click", () => {
|
||||
this.setActiveItem(item.id);
|
||||
this.onNavigate(item.id);
|
||||
});
|
||||
// Add context menu handler for rail button
|
||||
btn.addEventListener("contextmenu", (e) => {
|
||||
this.showViewContextMenu(e as MouseEvent, item.id);
|
||||
});
|
||||
});
|
||||
|
||||
if (remainingOther.length > 0) {
|
||||
const moreBtn = this.railEl!.createDiv({
|
||||
cls: "v2-rail-btn",
|
||||
attr: { "aria-label": t("More views") },
|
||||
});
|
||||
setIcon(moreBtn, "more-horizontal");
|
||||
moreBtn.addEventListener("click", (e) =>
|
||||
this.showOtherViewsMenu(e as MouseEvent, remainingOther)
|
||||
);
|
||||
}
|
||||
|
||||
// Projects menu button
|
||||
const projBtn = this.railEl!.createDiv({
|
||||
cls: "v2-rail-btn",
|
||||
attr: { "aria-label": t("Projects") },
|
||||
});
|
||||
setIcon(projBtn, "folder");
|
||||
projBtn.addEventListener("click", (e) =>
|
||||
this.showProjectMenu(e as MouseEvent)
|
||||
);
|
||||
|
||||
// Add (New Task) button
|
||||
const addBtn = this.railEl!.createDiv({
|
||||
cls: "v2-rail-btn",
|
||||
attr: { "aria-label": t("New Task") },
|
||||
});
|
||||
setIcon(addBtn, "plus");
|
||||
addBtn.addEventListener("click", () => this.onNavigate("new-task"));
|
||||
}
|
||||
|
||||
private renderOtherViewsSection() {
|
||||
if (!this.otherViewsSection || this.collapsed) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing content
|
||||
this.otherViewsSection.empty();
|
||||
|
||||
// Create header
|
||||
const otherHeader = this.otherViewsSection.createDiv({
|
||||
cls: "v2-section-header",
|
||||
});
|
||||
|
||||
const allOtherItems = this.computeOtherItems();
|
||||
const visibleCount =
|
||||
this.plugin?.settings?.experimental?.v2Config
|
||||
?.maxOtherViewsBeforeOverflow ?? 5;
|
||||
const displayedOther: V2NavigationItem[] = allOtherItems.slice(
|
||||
0,
|
||||
visibleCount
|
||||
);
|
||||
const remainingOther: V2NavigationItem[] =
|
||||
allOtherItems.slice(visibleCount);
|
||||
|
||||
otherHeader.createSpan({ text: t("Other Views") });
|
||||
|
||||
if (remainingOther.length > 0) {
|
||||
const moreBtn = otherHeader.createDiv({
|
||||
cls: "v2-section-action",
|
||||
|
|
@ -288,7 +323,8 @@ export class V2Sidebar extends Component {
|
|||
this.showOtherViewsMenu(e as MouseEvent, remainingOther)
|
||||
);
|
||||
}
|
||||
this.renderNavigationItems(otherSection, displayedOther);
|
||||
|
||||
this.renderNavigationItems(this.otherViewsSection, displayedOther);
|
||||
}
|
||||
|
||||
private computeOtherItems(): V2NavigationItem[] {
|
||||
|
|
@ -323,7 +359,19 @@ export class V2Sidebar extends Component {
|
|||
}
|
||||
|
||||
onload() {
|
||||
this.render();
|
||||
// On mobile, ensure we render the full sidebar content
|
||||
// even though it starts "collapsed" (hidden off-screen)
|
||||
if (Platform.isPhone && this.collapsed) {
|
||||
// Temporarily set to not collapsed to render full content
|
||||
const wasCollapsed = this.collapsed;
|
||||
this.collapsed = false;
|
||||
this.render();
|
||||
this.collapsed = wasCollapsed;
|
||||
// Apply the collapsed class for CSS positioning
|
||||
this.containerEl.addClass("is-collapsed");
|
||||
} else {
|
||||
this.render();
|
||||
}
|
||||
|
||||
// Subscribe to workspace events
|
||||
if (this.plugin.workspaceManager) {
|
||||
|
|
@ -355,7 +403,14 @@ export class V2Sidebar extends Component {
|
|||
|
||||
public setCollapsed(collapsed: boolean) {
|
||||
this.collapsed = collapsed;
|
||||
this.render();
|
||||
// On mobile, don't re-render when toggling collapse
|
||||
// The CSS will handle the drawer animation
|
||||
if (!Platform.isPhone) {
|
||||
this.render();
|
||||
} else {
|
||||
// Just toggle the class for mobile
|
||||
this.containerEl.toggleClass("is-collapsed", collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleWorkspaceChange(workspaceId: string) {
|
||||
|
|
@ -676,12 +731,11 @@ export class V2Sidebar extends Component {
|
|||
}
|
||||
view.visible = false;
|
||||
this.plugin.saveSettings();
|
||||
// Remove the element from DOM instead of full re-render
|
||||
const element = this.containerEl.querySelector(
|
||||
`[data-view-id="${viewId}"]`
|
||||
);
|
||||
if (element) {
|
||||
element.remove();
|
||||
// Re-render based on current mode
|
||||
if (this.collapsed) {
|
||||
this.renderRailMode();
|
||||
} else {
|
||||
this.renderOtherViewsSection();
|
||||
}
|
||||
// Trigger view config changed event
|
||||
this.plugin.app.workspace.trigger(
|
||||
|
|
@ -708,12 +762,11 @@ export class V2Sidebar extends Component {
|
|||
(v) => v.id !== viewId
|
||||
);
|
||||
this.plugin.saveSettings();
|
||||
// Remove the element from DOM instead of full re-render
|
||||
const element = this.containerEl.querySelector(
|
||||
`[data-view-id="${viewId}"]`
|
||||
);
|
||||
if (element) {
|
||||
element.remove();
|
||||
// Re-render based on current mode
|
||||
if (this.collapsed) {
|
||||
this.renderRailMode();
|
||||
} else {
|
||||
this.renderOtherViewsSection();
|
||||
}
|
||||
// Trigger view config changed event
|
||||
this.plugin.app.workspace.trigger(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
import { setIcon, Menu, Notice, SearchComponent } from "obsidian";
|
||||
import {
|
||||
setIcon,
|
||||
Menu,
|
||||
Notice,
|
||||
SearchComponent,
|
||||
Platform,
|
||||
Component,
|
||||
} from "obsidian";
|
||||
import TaskProgressBarPlugin from "@/index";
|
||||
import { Task } from "@/types/task";
|
||||
import { t } from "@/translations/helper";
|
||||
|
||||
export type ViewMode = "list" | "kanban" | "tree" | "calendar";
|
||||
|
||||
export class TopNavigation {
|
||||
export class TopNavigation extends Component {
|
||||
private containerEl: HTMLElement;
|
||||
private plugin: TaskProgressBarPlugin;
|
||||
private searchInput: HTMLInputElement;
|
||||
|
|
@ -22,8 +29,10 @@ export class TopNavigation {
|
|||
private onFilterClick: () => void,
|
||||
private onSortClick: () => void,
|
||||
private onSettingsClick: () => void,
|
||||
availableModes?: ViewMode[]
|
||||
availableModes?: ViewMode[],
|
||||
private onToggleSidebar?: () => void
|
||||
) {
|
||||
super();
|
||||
this.containerEl = containerEl;
|
||||
this.plugin = plugin;
|
||||
if (availableModes) {
|
||||
|
|
@ -73,8 +82,9 @@ export class TopNavigation {
|
|||
// Show navigation when modes are available
|
||||
this.containerEl.style.display = "";
|
||||
|
||||
// Left section - Search
|
||||
// Left section - Hamburger menu (mobile) and Search
|
||||
const leftSection = this.containerEl.createDiv({ cls: "v2-nav-left" });
|
||||
|
||||
const searchContainer = leftSection.createDiv({
|
||||
cls: "v2-search-container",
|
||||
});
|
||||
|
|
@ -91,7 +101,9 @@ export class TopNavigation {
|
|||
});
|
||||
|
||||
// Render view tabs (we know modes are available at this point)
|
||||
this.viewTabsContainer = centerSection.createDiv({ cls: "v2-view-tabs" });
|
||||
this.viewTabsContainer = centerSection.createDiv({
|
||||
cls: "v2-view-tabs",
|
||||
});
|
||||
this.renderViewTabs();
|
||||
|
||||
// Right section - Notifications and Settings
|
||||
|
|
@ -203,7 +215,11 @@ export class TopNavigation {
|
|||
item.setTitle(task.content || t("Untitled task"))
|
||||
.setIcon("alert-circle")
|
||||
.onClick(() => {
|
||||
new Notice(t("Task: {{content}}", { content: task.content || "" }));
|
||||
new Notice(
|
||||
t("Task: {{content}}", {
|
||||
content: task.content || "",
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -254,7 +270,12 @@ export class TopNavigation {
|
|||
for (const mode of this.availableModes) {
|
||||
const config = modeConfig[mode];
|
||||
if (config) {
|
||||
this.createViewTab(this.viewTabsContainer, mode, config.icon, config.label);
|
||||
this.createViewTab(
|
||||
this.viewTabsContainer,
|
||||
mode,
|
||||
config.icon,
|
||||
config.label
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -279,12 +300,16 @@ export class TopNavigation {
|
|||
}
|
||||
|
||||
// Update center section visibility (this should always be visible now since we handle empty modes above)
|
||||
const centerSection = this.containerEl.querySelector(".v2-nav-center") as HTMLElement;
|
||||
const centerSection = this.containerEl.querySelector(
|
||||
".v2-nav-center"
|
||||
) as HTMLElement;
|
||||
if (centerSection) {
|
||||
centerSection.style.display = "";
|
||||
// Re-render the view tabs
|
||||
if (!this.viewTabsContainer) {
|
||||
this.viewTabsContainer = centerSection.createDiv({ cls: "v2-view-tabs" });
|
||||
this.viewTabsContainer = centerSection.createDiv({
|
||||
cls: "v2-view-tabs",
|
||||
});
|
||||
}
|
||||
this.renderViewTabs();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -406,28 +406,9 @@
|
|||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.v2-search-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.v2-search-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px 8px 36px;
|
||||
background-color: var(--background-modifier-form-field);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.v2-search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent);
|
||||
max-width: 400px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Center Navigation */
|
||||
|
|
@ -564,7 +545,7 @@
|
|||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.tg-v2-sidebar-container {
|
||||
.tg-v2-sidebar-container:not(.is-mobile-drawer) {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
|
|
@ -581,6 +562,37 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Mobile-specific media query */
|
||||
@media (max-width: 480px) {
|
||||
/* Ensure drawer takes appropriate width on small screens */
|
||||
.tg-v2-sidebar-container.is-mobile-drawer {
|
||||
width: 75vw !important;
|
||||
max-width: 320px !important;
|
||||
}
|
||||
|
||||
/* Make hamburger button more prominent on mobile */
|
||||
.v2-hamburger-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
/* Adjust top navigation spacing on mobile */
|
||||
.v2-top-navigation {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
/* Hide view tabs on very small screens */
|
||||
.v2-view-tabs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Optimize search container on mobile */
|
||||
.v2-search-container {
|
||||
flex: 1;
|
||||
max-width: calc(100vw - 180px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Collapsed Sidebar (Rail) ===== */
|
||||
.tg-v2-sidebar-container {
|
||||
transition: width 0.2s ease, min-width 0.2s ease, max-width 0.2s ease;
|
||||
|
|
@ -653,3 +665,190 @@
|
|||
text-transform: uppercase;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
/* ===== Mobile Drawer Styles ===== */
|
||||
.drawer-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.drawer-open .drawer-overlay {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* Mobile drawer sidebar */
|
||||
.tg-v2-sidebar-container.is-mobile-drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 280px !important;
|
||||
max-width: 85vw !important;
|
||||
min-width: 280px !important;
|
||||
z-index: 1000;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 4px 0 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* When drawer is open */
|
||||
.drawer-open .tg-v2-sidebar-container.is-mobile-drawer {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Ensure sidebar content is visible on mobile even when "collapsed" */
|
||||
.tg-v2-sidebar-container.is-mobile-drawer .v2-sidebar.is-collapsed {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Hide rail mode on mobile */
|
||||
.tg-v2-sidebar-container.is-mobile-drawer .v2-sidebar-rail {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Mobile-specific sidebar content adjustments */
|
||||
.is-mobile .v2-sidebar {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.is-mobile .v2-sidebar-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
/* Ensure all sections are visible on mobile */
|
||||
.is-mobile .v2-sidebar-content {
|
||||
overflow-y: auto;
|
||||
max-height: calc(
|
||||
100vh - 150px
|
||||
); /* Account for header and new task button */
|
||||
}
|
||||
|
||||
.is-mobile .v2-sidebar-section {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
/* Ensure section headers are visible on mobile */
|
||||
.is-mobile .v2-section-header {
|
||||
display: flex !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.is-mobile .v2-navigation-item,
|
||||
.is-mobile .v2-project-item {
|
||||
padding: 10px 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.is-mobile .v2-new-task-btn {
|
||||
padding: 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.is-mobile .v2-navigation-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.is-mobile span.v2-navigation-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Touch-friendly sizing */
|
||||
.is-mobile .v2-navigation-item,
|
||||
.is-mobile .v2-project-item,
|
||||
.is-mobile .v2-add-project {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/* Adjust main container on mobile when drawer is open */
|
||||
.drawer-open .tg-v2-main-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Hamburger menu button for mobile */
|
||||
.v2-hamburger-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.v2-hamburger-button:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.v2-hamburger-button svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Adjust search container on mobile when hamburger is present */
|
||||
.v2-nav-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Hide sidebar toggle on desktop when in mobile drawer mode */
|
||||
.tg-v2-sidebar-container.is-mobile-drawer
|
||||
~ .tg-v2-main-container
|
||||
.sidebar-toggle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Animation for drawer from right (optional - can be configured) */
|
||||
.tg-v2-sidebar-container.is-mobile-drawer.drawer-right {
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: translateX(100%);
|
||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.drawer-open .tg-v2-sidebar-container.is-mobile-drawer.drawer-right {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Smooth animations for all transitions */
|
||||
@keyframes slideInFromLeft {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
opacity: 0.8;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInFromRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0.8;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@
|
|||
}
|
||||
|
||||
/* Mobile view - slide from right */
|
||||
.is-phone .task-details {
|
||||
.is-phone .task-details,
|
||||
.is-mobile .task-details {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
|
@ -45,19 +46,25 @@
|
|||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.is-phone .task-genius-container.details-hidden .task-details {
|
||||
.is-phone .task-genius-container.details-hidden .task-details,
|
||||
.is-mobile .task-genius-container.details-hidden .task-details,
|
||||
.is-mobile .tg-v2-container.details-hidden .task-details {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
.is-phone .task-genius-container.details-visible .task-details {
|
||||
.is-phone .task-genius-container.details-visible .task-details,
|
||||
.is-mobile .task-genius-container.details-visible .task-details,
|
||||
.is-mobile .tg-v2-container.details-visible .task-details {
|
||||
width: calc(100% - var(--size-4-12));
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Add overlay when details are visible on mobile */
|
||||
.is-phone .task-genius-container.details-visible::before {
|
||||
.is-phone .task-genius-container.details-visible::before,
|
||||
.is-mobile .task-genius-container.details-visible::before,
|
||||
.is-mobile .tg-v2-container.details-visible::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -70,7 +77,8 @@
|
|||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.is-phone .details-close-btn {
|
||||
.is-phone .details-close-btn,
|
||||
.is-mobile .details-close-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
|
|
@ -78,7 +86,8 @@
|
|||
justify-content: center;
|
||||
}
|
||||
|
||||
.is-phone .details-header {
|
||||
.is-phone .details-header,
|
||||
.is-mobile .details-header {
|
||||
padding: var(--size-4-4);
|
||||
}
|
||||
|
||||
|
|
@ -341,6 +350,28 @@
|
|||
background-color: var(--interactive-hover);
|
||||
}
|
||||
|
||||
/* V2 Container specific styles for mobile */
|
||||
.is-mobile .tg-v2-container .task-details {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
height: 100vh;
|
||||
width: 85vw;
|
||||
max-width: 400px;
|
||||
z-index: 1001; /* Above drawer overlay */
|
||||
background-color: var(--background-secondary);
|
||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.is-mobile .task-details .details-close-btn:hover {
|
||||
background-color: var(--background-modifier-active-hover);
|
||||
}
|
||||
|
||||
.is-mobile .details-content {
|
||||
padding-bottom: var(--size-4-12);
|
||||
}
|
||||
|
||||
/* Responsive design for mobile */
|
||||
@media screen and (max-width: 768px) {
|
||||
.task-omnifocus-container {
|
||||
|
|
|
|||
226
styles.css
226
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue