onSelect(task)}
tabIndex={0}
onKeyDown={(e) => {
@@ -183,29 +183,25 @@ function RecursiveTaskItem({
onDragOver={(e) => {
if (isTrashMode) return;
e.preventDefault();
+ e.stopPropagation();
e.dataTransfer.dropEffect = "move";
- const rect = e.currentTarget.getBoundingClientRect();
- const relY = e.clientY - rect.top;
- const h = rect.height;
- if (relY < h * 0.25) {
- setDragOverPos("top");
- } else if (relY > h * 0.75) {
- setDragOverPos("bottom");
- } else {
- setDragOverPos("inside");
- }
+ if (!isDragOver) setIsDragOver(true);
+ }}
+ onDragLeave={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setIsDragOver(false);
}}
- onDragLeave={() => setDragOverPos(null)}
onDrop={(e) => {
if (isTrashMode) return;
e.preventDefault();
+ e.stopPropagation();
+ setIsDragOver(false);
const draggedId = e.dataTransfer.getData("text/plain");
if (draggedId && draggedId !== task.id && onDragTask) {
- const pos: "before" | "after" | "inside" =
- dragOverPos === "top" ? "before" : dragOverPos === "bottom" ? "after" : "inside";
- onDragTask(draggedId, task.id, pos);
+ // Unified interaction: drop on task to nest as subtask
+ onDragTask(draggedId, task.id, "inside");
}
- setDragOverPos(null);
}}
id={`task-${task.id}`}
style={{
@@ -213,9 +209,9 @@ function RecursiveTaskItem({
marginLeft: depth > 0 ? 8 : 0,
transform: `translateX(${swipeOffset}px)`,
transition: swipeOffset === 0 ? "transform 0.2s cubic-bezier(0.16, 1, 0.3, 1)" : "none",
- outline: dragOverPos === "inside" ? "2px dashed var(--accent)" : undefined,
+ outline: isDragOver ? "2px dashed var(--accent)" : undefined,
outlineOffset: -2,
- background: dragOverPos === "inside" ? "var(--accent-light, rgba(75, 123, 245, 0.12))" : undefined,
+ background: isDragOver ? "var(--accent-light, rgba(75, 123, 245, 0.12))" : undefined,
}}
>
{/* Notion-style 6-dot Drag Handle */}
@@ -307,17 +303,24 @@ function RecursiveTaskItem({
style={{ padding: "2px 6px", fontSize: depth > 0 ? 13 : 14, fontWeight: 500, height: 26, width: "100%" }}
/>
) : (
-
{
- if (isTrashMode) return;
- e.stopPropagation();
- setEditingTitle(true);
- }}
- title="Double click or press F2 to edit"
- style={{ fontSize: depth > 0 ? 13 : 14, fontWeight: depth === 0 ? 600 : 500 }}
- >
- {task.title}
+
+ {
+ if (isTrashMode) return;
+ e.stopPropagation();
+ setEditingTitle(true);
+ }}
+ title="Double click or press F2 to edit"
+ style={{
+ fontSize: depth > 0 ? 13 : 14,
+ fontWeight: depth === 0 ? 600 : 500,
+ cursor: "text",
+ display: "inline-block",
+ }}
+ >
+ {task.title}
+
)}
@@ -804,9 +807,9 @@ export function TaskList({
};
// Robust Tree-aware reordering function (1st, 2nd, 3rd depth task reordering, promoting to top-level, and demoting into subtask)
- const handleReorderTasks = useCallback((draggedId: string, targetId: string, position: "before" | "after" | "inside") => {
+ const handleReorderTasks = useCallback((draggedId: string, targetId: string, position: "before" | "after" | "inside" | "root") => {
// Prevent dropping onto itself or into its own subtree (which causes loops/disappearing tasks)
- if (draggedId === targetId) return;
+ if (draggedId === targetId && position !== "root") return;
setTasks((prev) => {
if (isDescendantNode(prev, draggedId, targetId)) {
@@ -837,6 +840,25 @@ export function TaskList({
let determinedParentId: string | null = null;
+ if (position === "root") {
+ // Promote directly to top-level (1st depth root task)
+ determinedParentId = null;
+ const nodeAsRoot: Task = { ...safeExtractedNode, parentId: null };
+ const finalTree = [...treeWithoutDragged, nodeAsRoot];
+
+ if (isDemo && typeof window !== "undefined") {
+ const store = getDemoStore();
+ saveDemoStore(store.lists, finalTree as unknown as MockTask[]);
+ } else {
+ fetch(`/api/tasks/${draggedId}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ parentId: null }),
+ }).catch((err) => console.error("Failed to promote task to root", err));
+ }
+ return finalTree;
+ }
+
// 2. Insert into the target position
if (position === "inside") {
determinedParentId = targetId;
@@ -1100,17 +1122,38 @@ export function TaskList({
/>
) : (
- /* Task list container */
+ /* Task list container with background drop support for promoting subtask to 1st depth */
{
+ if (e.target === e.currentTarget) {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = "move";
+ }
+ }}
+ onDrop={(e) => {
+ if (e.target === e.currentTarget) {
+ e.preventDefault();
+ const draggedId = e.dataTransfer.getData("text/plain");
+ if (draggedId) {
+ // Promote to 1st depth root task
+ handleReorderTasks(draggedId, "", "root");
+ }
+ }
+ }}
onContextMenu={(e) => {
if (e.target === e.currentTarget) {
e.preventDefault();
}
}}
>
- {loading && (
-
{t("loading")}
+ {loading && tasks.length === 0 && (
+
)}
{!loading && tasks.length === 0 && (
diff --git a/src/lib/i18n/locales/en.ts b/src/lib/i18n/locales/en.ts
index ac4bd48..0cac99f 100644
--- a/src/lib/i18n/locales/en.ts
+++ b/src/lib/i18n/locales/en.ts
@@ -155,16 +155,16 @@ export const en: TranslationDict = {
hideDone: "Hide Done",
showDone: "Show Done",
completedSection: "Completed",
- addTaskPlaceholder: "Add a task, press Enter...",
+ addTaskPlaceholder: "What would you like to accomplish?",
add: "Add",
- noTasksYet: "No tasks in this list yet",
+ noTasksYet: "No tasks in this list",
selectListToStart: "Select a list to get started",
- loading: "Loading...",
+ loading: "Loading tasks...",
today: "Today",
tomorrow: "Tomorrow",
// Task Detail
- taskTitlePlaceholder: "Task title...",
+ taskTitlePlaceholder: "Task title",
priority: "Priority",
priorityNone: "None",
priorityLow: "Low",
@@ -173,10 +173,10 @@ export const en: TranslationDict = {
dueDate: "Due date",
clear: "Clear",
notes: "Notes",
- notesPlaceholder: "Add detailed notes, markdown supported...",
+ notesPlaceholder: "Add details, markdown supported...",
subtasks: "Subtasks",
- addSubtaskPlaceholder: "Add a subtask, press Enter...",
- deleteTaskConfirm: "Are you sure you want to delete this task?",
+ addSubtaskPlaceholder: "Add a subtask...",
+ deleteTaskConfirm: "Delete this task?",
autoSaved: "Saved",
saving: "Saving...",
created: "Created",
diff --git a/src/lib/i18n/locales/ja.ts b/src/lib/i18n/locales/ja.ts
index 2423306..688f1e3 100644
--- a/src/lib/i18n/locales/ja.ts
+++ b/src/lib/i18n/locales/ja.ts
@@ -155,28 +155,28 @@ export const ja: TranslationDict = {
hideDone: "完了項目を非表示",
showDone: "完了項目を表示",
completedSection: "完了済み",
- addTaskPlaceholder: "新しいタスクを入力してEnter...",
+ addTaskPlaceholder: "新しいタスクを書き留めましょう",
add: "追加",
noTasksYet: "このリストにはタスクがありません",
- selectListToStart: "リストを選択して始めましょう",
+ selectListToStart: "リストを選択して開始してください",
loading: "読み込み中...",
today: "今日",
tomorrow: "明日",
// Task Detail
- taskTitlePlaceholder: "タスク名を入力...",
+ taskTitlePlaceholder: "タスク名",
priority: "優先度",
priorityNone: "なし",
priorityLow: "低",
priorityMedium: "中",
priorityHigh: "高",
- dueDate: "期日",
+ dueDate: "期限日",
clear: "クリア",
notes: "メモ",
- notesPlaceholder: "詳細なメモを入力 (Markdown対応)...",
+ notesPlaceholder: "詳細を入力 (Markdown対応)...",
subtasks: "サブタスク",
- addSubtaskPlaceholder: "サブタスクを入力してEnter...",
- deleteTaskConfirm: "このタスクを削除してもよろしいですか?",
+ addSubtaskPlaceholder: "サブタスクを追加...",
+ deleteTaskConfirm: "タスクを削除しますか?",
autoSaved: "保存済み",
saving: "保存中...",
created: "作成日時",
diff --git a/src/lib/i18n/locales/ko.ts b/src/lib/i18n/locales/ko.ts
index ee0b7ca..b61571b 100644
--- a/src/lib/i18n/locales/ko.ts
+++ b/src/lib/i18n/locales/ko.ts
@@ -155,7 +155,7 @@ export const ko: TranslationDict = {
hideDone: "완료항목 숨기기",
showDone: "완료항목 표시",
completedSection: "완료됨",
- addTaskPlaceholder: "새 할 일 입력 후 Enter...",
+ addTaskPlaceholder: "새로운 할 일을 적어볼까요?",
add: "추가",
noTasksYet: "이 목록에 등록된 할 일이 없습니다",
selectListToStart: "시작할 목록을 선택하세요",
@@ -164,7 +164,7 @@ export const ko: TranslationDict = {
tomorrow: "내일",
// Task Detail
- taskTitlePlaceholder: "작업 제목 입력...",
+ taskTitlePlaceholder: "할 일 제목",
priority: "우선순위",
priorityNone: "없음",
priorityLow: "낮음",
@@ -175,8 +175,8 @@ export const ko: TranslationDict = {
notes: "메모",
notesPlaceholder: "상세 내용을 입력하세요 (마크다운 지원)...",
subtasks: "하위작업",
- addSubtaskPlaceholder: "하위작업 추가 후 Enter...",
- deleteTaskConfirm: "이 작업을 정말 삭제하시겠습니까?",
+ addSubtaskPlaceholder: "하위 작업을 적어보세요...",
+ deleteTaskConfirm: "태스크를 삭제하시겠습니까?",
autoSaved: "저장됨",
saving: "저장 중...",
created: "생성일",