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:
Wonhee Han
2026-08-20 14:34:17 +09:00
parent 1fa94b5227
commit 81ddb9cba7
6 changed files with 308 additions and 48 deletions
+52
View File
@@ -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" />