Feature: Smooth sidebar animation, fixed theme button clipping, moved import to profile menu, added custom webapp context menu, and polished TickTick style detail panel
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { ContextMenu, MenuItem } from "@/components/ui/ContextMenu";
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
@@ -36,6 +37,7 @@ interface TaskItemProps {
|
||||
onToggle: (id: string, completed: boolean) => void;
|
||||
onUpdateTitle: (id: string, title: string) => void;
|
||||
onAddSubtask: (title: string, parentId: string) => void;
|
||||
onContextMenu: (x: number, y: number, task: Task) => void;
|
||||
onDeleteTask?: (id: string) => void;
|
||||
}
|
||||
|
||||
@@ -46,6 +48,7 @@ function TaskItem({
|
||||
onToggle,
|
||||
onUpdateTitle,
|
||||
onAddSubtask,
|
||||
onContextMenu,
|
||||
}: TaskItemProps) {
|
||||
const { t, lang } = useI18n();
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
@@ -116,6 +119,10 @@ function TaskItem({
|
||||
<div
|
||||
className={`task-item${task.completed ? " completed" : ""}${isSelected ? " selected" : ""}`}
|
||||
onClick={() => onSelect(task)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
onContextMenu(e.clientX, e.clientY, task);
|
||||
}}
|
||||
id={`task-${task.id}`}
|
||||
style={{ group: "item" } as React.CSSProperties}
|
||||
>
|
||||
@@ -406,6 +413,7 @@ export function TaskList({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editingHeader, setEditingHeader] = useState(false);
|
||||
const [headerTitle, setHeaderTitle] = useState("");
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; task: Task } | null>(null);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const headerInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -652,6 +660,7 @@ export function TaskList({
|
||||
onToggle={handleToggle}
|
||||
onUpdateTitle={(id, title) => onUpdateTaskTitle && onUpdateTaskTitle(id, title)}
|
||||
onAddSubtask={handleAddSubtaskInline}
|
||||
onContextMenu={(x, y, tItem) => setContextMenu({ x, y, task: tItem })}
|
||||
/>
|
||||
))}
|
||||
{showCompleted && completedTasks.length > 0 && (
|
||||
@@ -677,12 +686,55 @@ export function TaskList({
|
||||
onToggle={handleToggle}
|
||||
onUpdateTitle={(id, title) => onUpdateTaskTitle && onUpdateTaskTitle(id, title)}
|
||||
onAddSubtask={handleAddSubtaskInline}
|
||||
onContextMenu={(x, y, tItem) => setContextMenu({ x, y, task: tItem })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Context Menu on Task */}
|
||||
{contextMenu && (
|
||||
<ContextMenu
|
||||
x={contextMenu.x}
|
||||
y={contextMenu.y}
|
||||
items={[
|
||||
{
|
||||
label: contextMenu.task.completed ? t("showDone") : t("completedSection"),
|
||||
icon: contextMenu.task.completed ? "↩️" : "✅",
|
||||
onClick: () => handleToggle(contextMenu.task.id, !contextMenu.task.completed),
|
||||
},
|
||||
{
|
||||
label: t("taskTitlePlaceholder"),
|
||||
icon: "✏️",
|
||||
onClick: () => onTaskSelect(contextMenu.task),
|
||||
},
|
||||
{
|
||||
label: t("subtasks"),
|
||||
icon: "↳",
|
||||
onClick: () => {
|
||||
onTaskSelect(contextMenu.task);
|
||||
},
|
||||
},
|
||||
{ divider: true, label: "" },
|
||||
{
|
||||
label: t("deleteTaskConfirm").split("?")[0],
|
||||
icon: "🗑️",
|
||||
danger: true,
|
||||
onClick: async () => {
|
||||
if (isDemo) {
|
||||
setTasks((prev) => prev.filter((t) => t.id !== contextMenu.task.id));
|
||||
} else {
|
||||
await fetch(`/api/tasks/${contextMenu.task.id}`, { method: "DELETE" });
|
||||
onRefresh();
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
onClose={() => setContextMenu(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Add task bar */}
|
||||
<div className="add-task-bar">
|
||||
<button className="task-check-btn" style={{ opacity: 0.4, flexShrink: 0 }} aria-hidden="true" />
|
||||
|
||||
Reference in New Issue
Block a user