idea-manager 0.7.6 → 0.7.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +127 -0
- package/README.ja.md +127 -0
- package/README.md +2 -0
- package/README.zh.md +127 -0
- package/package.json +4 -1
- package/public/sw.js +31 -9
- package/src/app/api/projects/[id]/sub-projects/route.ts +17 -1
- package/src/components/task/ProjectTree.tsx +259 -137
- package/src/components/task/TaskDetail.tsx +2 -2
- package/src/components/workspace/WorkspacePanel.tsx +17 -2
package/README.en.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# IM (Idea Manager)
|
|
2
|
+
|
|
3
|
+
[한국어](README.md) | **English** | [日本語](README.ja.md) | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
> From ideas to executable prompts — a multi-project workflow manager
|
|
6
|
+
|
|
7
|
+
A task management tool for developers juggling multiple projects simultaneously. Organize ideas into sub-projects and tasks, refine prompts for each task, and hand them off to AI agents like Claude Code. With a built-in MCP Server, AI agents can autonomously pick up and execute tasks.
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Core Workflow
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Brainstorming → Organize into Sub-projects/Tasks → Refine Prompts → Execute via MCP
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Hierarchy
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Project
|
|
21
|
+
├── Sub-project A
|
|
22
|
+
│ ├── Task 1 → Prompt
|
|
23
|
+
│ ├── Task 2 → Prompt
|
|
24
|
+
│ └── Task 3 → Prompt
|
|
25
|
+
└── Sub-project B
|
|
26
|
+
├── Task 4 → Prompt
|
|
27
|
+
└── Task 5 → Prompt
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Task Status Flow
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
💡 Idea → ✏️ Writing → 🚀 Submitted → 🧪 Testing → ✅ Done
|
|
34
|
+
🔴 Problem
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g idea-manager
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Start Web UI
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
im start
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Opens the web UI at `http://localhost:3456`.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Custom port
|
|
55
|
+
im start -p 4000
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Start MCP Server
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
im mcp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Claude Desktop Configuration (claude_desktop_config.json)
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"idea-manager": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["-y", "idea-manager", "mcp"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Claude Code Configuration
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
claude mcp add idea-manager -- npx -y idea-manager mcp
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### MCP Tools
|
|
84
|
+
|
|
85
|
+
| Tool | Description |
|
|
86
|
+
|------|-------------|
|
|
87
|
+
| `list-projects` | List all projects |
|
|
88
|
+
| `get-project-context` | Get full sub-project + task tree |
|
|
89
|
+
| `get-next-task` | Get next task to execute (status=submitted) |
|
|
90
|
+
| `get-task-prompt` | Get prompt for a specific task |
|
|
91
|
+
| `update-status` | Change task status (idea/writing/submitted/testing/done/problem) |
|
|
92
|
+
| `report-completion` | Report task completion |
|
|
93
|
+
|
|
94
|
+
## Key Features
|
|
95
|
+
|
|
96
|
+
- **Tab-based Multi-project** — Open multiple projects in tabs like a browser/IDE, state preserved on tab switch
|
|
97
|
+
- **3-Panel Workspace** — Brainstorming | Project Tree | Task Detail, drag to resize panels
|
|
98
|
+
- **Tree-structured Projects** — Tasks displayed hierarchically under sub-projects
|
|
99
|
+
- **Brainstorming Panel** — Free-form notes, collapsible
|
|
100
|
+
- **Prompt Editor** — Write/edit/copy prompts per task, AI refinement
|
|
101
|
+
- **AI Chat** — Per-task AI conversations to refine prompts
|
|
102
|
+
- **3-Tab Dashboard** — Active / All / Today
|
|
103
|
+
- **Keyboard Shortcuts** — Ctrl+Tab/Ctrl+Shift+Tab for tab navigation, B: toggle brainstorm, N: add sub-project, T: add task, Cmd+1~6: change status
|
|
104
|
+
- **PWA Support** — Install as an app for a standalone window experience
|
|
105
|
+
- **Watch Mode** — Auto-execute submitted tasks via Claude CLI with real-time progress
|
|
106
|
+
- **Built-in MCP Server** — Supports autonomous AI agent execution
|
|
107
|
+
- **Local-first** — SQLite-based, data stored in `~/.idea-manager/`
|
|
108
|
+
|
|
109
|
+
## Tech Stack
|
|
110
|
+
|
|
111
|
+
| Area | Technology |
|
|
112
|
+
|------|------------|
|
|
113
|
+
| Frontend | Next.js 15, React 19, TypeScript, Tailwind CSS 4 |
|
|
114
|
+
| Backend | Next.js API Routes |
|
|
115
|
+
| Database | SQLite (better-sqlite3) |
|
|
116
|
+
| AI | Claude CLI (subscription-based, no API key needed) |
|
|
117
|
+
| MCP | Model Context Protocol (stdio) |
|
|
118
|
+
| CLI | Commander.js |
|
|
119
|
+
|
|
120
|
+
## Requirements
|
|
121
|
+
|
|
122
|
+
- **Node.js** 18+
|
|
123
|
+
- **Claude CLI** — Required for AI chat/refinement features (Claude subscription needed). Core features like task management and prompt editing work without it.
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
package/README.ja.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# IM (Idea Manager)
|
|
2
|
+
|
|
3
|
+
[한국어](README.md) | [English](README.en.md) | **日本語** | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
> アイデアから実行可能なプロンプトまで — マルチプロジェクトワークフローマネージャー
|
|
6
|
+
|
|
7
|
+
複数のプロジェクトを同時に進めるデベロッパー向けのタスク管理ツールです。アイデアをサブプロジェクトとタスクに整理し、各タスクごとにプロンプトを磨いてClaude CodeなどのAIエージェントに渡すことができます。MCP Serverを内蔵しており、AIエージェントが自律的にタスクを取得して実行できます。
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## コアワークフロー
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
ブレインストーミング → サブプロジェクト/タスクに整理 → プロンプト精製 → MCPでAI実行
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 階層構造
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
プロジェクト
|
|
21
|
+
├── サブプロジェクト A
|
|
22
|
+
│ ├── タスク 1 → プロンプト
|
|
23
|
+
│ ├── タスク 2 → プロンプト
|
|
24
|
+
│ └── タスク 3 → プロンプト
|
|
25
|
+
└── サブプロジェクト B
|
|
26
|
+
├── タスク 4 → プロンプト
|
|
27
|
+
└── タスク 5 → プロンプト
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### タスクステータスフロー
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
💡 Idea → ✏️ Writing → 🚀 Submitted → 🧪 Testing → ✅ Done
|
|
34
|
+
🔴 Problem
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## インストール
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g idea-manager
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 使い方
|
|
44
|
+
|
|
45
|
+
### Web UIの起動
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
im start
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`http://localhost:3456`でWeb UIが開きます。
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# ポート変更
|
|
55
|
+
im start -p 4000
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### MCP Serverの起動
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
im mcp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Claude Desktop設定 (claude_desktop_config.json)
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"idea-manager": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["-y", "idea-manager", "mcp"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Claude Code設定
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
claude mcp add idea-manager -- npx -y idea-manager mcp
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### MCPツール
|
|
84
|
+
|
|
85
|
+
| ツール | 説明 |
|
|
86
|
+
|--------|------|
|
|
87
|
+
| `list-projects` | プロジェクト一覧の取得 |
|
|
88
|
+
| `get-project-context` | サブプロジェクト+タスクツリー全体の取得 |
|
|
89
|
+
| `get-next-task` | 次に実行するタスクとプロンプトの取得(status=submitted) |
|
|
90
|
+
| `get-task-prompt` | 特定タスクのプロンプト取得 |
|
|
91
|
+
| `update-status` | タスクステータスの変更(idea/writing/submitted/testing/done/problem) |
|
|
92
|
+
| `report-completion` | タスク完了の報告 |
|
|
93
|
+
|
|
94
|
+
## 主な機能
|
|
95
|
+
|
|
96
|
+
- **タブベースのマルチプロジェクト** — ブラウザ/IDEのように複数プロジェクトをタブで同時に開き、タブ切替時に状態を保持
|
|
97
|
+
- **3パネルワークスペース** — ブレインストーミング | プロジェクトツリー | タスク詳細、パネル間ドラッグでサイズ調整
|
|
98
|
+
- **ツリー型プロジェクト構造** — サブプロジェクト配下にタスクを階層的に表示
|
|
99
|
+
- **ブレインストーミングパネル** — 自由形式メモ、折りたたみ/展開可能
|
|
100
|
+
- **プロンプトエディタ** — タスクごとにプロンプトを作成/編集/コピー、AIによる磨き上げ
|
|
101
|
+
- **AIチャット** — タスクごとのAI対話でプロンプトを具体化
|
|
102
|
+
- **3タブダッシュボード** — 進行中 / 全体 / 今日のタスク
|
|
103
|
+
- **キーボードショートカット** — Ctrl+Tab/Ctrl+Shift+Tabでタブ移動、B: ブレインストーミング切替、N: サブプロジェクト追加、T: タスク追加、Cmd+1~6: ステータス変更
|
|
104
|
+
- **PWA対応** — アプリとしてインストールして独立ウィンドウで使用可能
|
|
105
|
+
- **Watchモード** — submittedタスクをClaude CLIで自動実行、リアルタイム進捗表示
|
|
106
|
+
- **MCP Server内蔵** — AIエージェントの自律実行をサポート
|
|
107
|
+
- **ローカルファースト** — SQLiteベース、データは`~/.idea-manager/`に保存
|
|
108
|
+
|
|
109
|
+
## 技術スタック
|
|
110
|
+
|
|
111
|
+
| 領域 | 技術 |
|
|
112
|
+
|------|------|
|
|
113
|
+
| フロントエンド | Next.js 15, React 19, TypeScript, Tailwind CSS 4 |
|
|
114
|
+
| バックエンド | Next.js API Routes |
|
|
115
|
+
| データベース | SQLite (better-sqlite3) |
|
|
116
|
+
| AI | Claude CLI(サブスクリプションベース、APIキー不要) |
|
|
117
|
+
| MCP | Model Context Protocol (stdio) |
|
|
118
|
+
| CLI | Commander.js |
|
|
119
|
+
|
|
120
|
+
## 必要条件
|
|
121
|
+
|
|
122
|
+
- **Node.js** 18+
|
|
123
|
+
- **Claude CLI** — AIチャット/磨き上げ機能に必要(Claudeサブスクリプション必要)。なくてもタスク管理やプロンプト作成などの基本機能は正常に動作します。
|
|
124
|
+
|
|
125
|
+
## ライセンス
|
|
126
|
+
|
|
127
|
+
MIT
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# IM (Idea Manager)
|
|
2
2
|
|
|
3
|
+
**한국어** | [English](README.en.md) | [日本語](README.ja.md) | [中文](README.zh.md)
|
|
4
|
+
|
|
3
5
|
> 아이디어에서 실행 가능한 프롬프트까지, 멀티 프로젝트 워크플로우 매니저
|
|
4
6
|
|
|
5
7
|
여러 프로젝트를 동시에 진행하는 개발자를 위한 태스크 관리 도구입니다. 아이디어를 서브 프로젝트와 태스크로 조직화하고, 각 태스크별 프롬프트를 정제하여 Claude Code 등 AI 에이전트에게 전달할 수 있습니다. MCP Server를 내장하고 있어 AI 에이전트가 자율적으로 태스크를 가져가 실행할 수 있습니다.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# IM (Idea Manager)
|
|
2
|
+
|
|
3
|
+
[한국어](README.md) | [English](README.en.md) | [日本語](README.ja.md) | **中文**
|
|
4
|
+
|
|
5
|
+
> 从创意到可执行提示词 — 多项目工作流管理器
|
|
6
|
+
|
|
7
|
+
专为同时管理多个项目的开发者设计的任务管理工具。将创意组织成子项目和任务,为每个任务精炼提示词,并交给 Claude Code 等 AI 代理执行。内置 MCP Server,AI 代理可以自主获取并执行任务。
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## 核心工作流
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
头脑风暴 → 组织成子项目/任务 → 精炼提示词 → 通过 MCP 执行 AI
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 层级结构
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
项目
|
|
21
|
+
├── 子项目 A
|
|
22
|
+
│ ├── 任务 1 → 提示词
|
|
23
|
+
│ ├── 任务 2 → 提示词
|
|
24
|
+
│ └── 任务 3 → 提示词
|
|
25
|
+
└── 子项目 B
|
|
26
|
+
├── 任务 4 → 提示词
|
|
27
|
+
└── 任务 5 → 提示词
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 任务状态流
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
💡 Idea → ✏️ Writing → 🚀 Submitted → 🧪 Testing → ✅ Done
|
|
34
|
+
🔴 Problem
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 安装
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g idea-manager
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 使用方法
|
|
44
|
+
|
|
45
|
+
### 启动 Web UI
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
im start
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
在 `http://localhost:3456` 打开 Web UI。
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 自定义端口
|
|
55
|
+
im start -p 4000
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 启动 MCP Server
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
im mcp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Claude Desktop 配置 (claude_desktop_config.json)
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"idea-manager": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["-y", "idea-manager", "mcp"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### Claude Code 配置
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
claude mcp add idea-manager -- npx -y idea-manager mcp
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### MCP 工具
|
|
84
|
+
|
|
85
|
+
| 工具 | 描述 |
|
|
86
|
+
|------|------|
|
|
87
|
+
| `list-projects` | 获取项目列表 |
|
|
88
|
+
| `get-project-context` | 获取完整的子项目+任务树 |
|
|
89
|
+
| `get-next-task` | 获取下一个待执行的任务和提示词(status=submitted) |
|
|
90
|
+
| `get-task-prompt` | 获取指定任务的提示词 |
|
|
91
|
+
| `update-status` | 更改任务状态(idea/writing/submitted/testing/done/problem) |
|
|
92
|
+
| `report-completion` | 报告任务完成 |
|
|
93
|
+
|
|
94
|
+
## 主要功能
|
|
95
|
+
|
|
96
|
+
- **标签式多项目** — 像浏览器/IDE一样用标签页同时打开多个项目,切换标签时保持状态
|
|
97
|
+
- **三栏工作区** — 头脑风暴 | 项目树 | 任务详情,拖拽调整面板大小
|
|
98
|
+
- **树形项目结构** — 任务在子项目下层级展示
|
|
99
|
+
- **头脑风暴面板** — 自由格式笔记,可折叠/展开
|
|
100
|
+
- **提示词编辑器** — 按任务编写/编辑/复制提示词,AI 润色
|
|
101
|
+
- **AI 聊天** — 按任务进行 AI 对话以细化提示词
|
|
102
|
+
- **三标签仪表盘** — 进行中 / 全部 / 今日待办
|
|
103
|
+
- **键盘快捷键** — Ctrl+Tab/Ctrl+Shift+Tab 切换标签,B:切换头脑风暴,N:添加子项目,T:添加任务,Cmd+1~6:更改状态
|
|
104
|
+
- **PWA 支持** — 可安装为应用,在独立窗口中使用
|
|
105
|
+
- **Watch 模式** — 通过 Claude CLI 自动执行已提交的任务,实时显示进度
|
|
106
|
+
- **内置 MCP Server** — 支持 AI 代理自主执行
|
|
107
|
+
- **本地优先** — 基于 SQLite,数据存储在 `~/.idea-manager/`
|
|
108
|
+
|
|
109
|
+
## 技术栈
|
|
110
|
+
|
|
111
|
+
| 领域 | 技术 |
|
|
112
|
+
|------|------|
|
|
113
|
+
| 前端 | Next.js 15, React 19, TypeScript, Tailwind CSS 4 |
|
|
114
|
+
| 后端 | Next.js API Routes |
|
|
115
|
+
| 数据库 | SQLite (better-sqlite3) |
|
|
116
|
+
| AI | Claude CLI(基于订阅,无需 API 密钥) |
|
|
117
|
+
| MCP | Model Context Protocol (stdio) |
|
|
118
|
+
| CLI | Commander.js |
|
|
119
|
+
|
|
120
|
+
## 系统要求
|
|
121
|
+
|
|
122
|
+
- **Node.js** 18+
|
|
123
|
+
- **Claude CLI** — AI 聊天/润色功能需要(需要 Claude 订阅)。即使没有,任务管理和提示词编写等基本功能也可正常使用。
|
|
124
|
+
|
|
125
|
+
## 许可证
|
|
126
|
+
|
|
127
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idea-manager",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "AI 기반 브레인스토밍 → 구조화 → 프롬프트 생성 도구. MCP Server 내장.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"brainstorm",
|
|
@@ -39,6 +39,9 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
42
|
+
"@dnd-kit/core": "^6.3.1",
|
|
43
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
44
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
42
45
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
43
46
|
"better-sqlite3": "^12.6.2",
|
|
44
47
|
"commander": "^14.0.3",
|
package/public/sw.js
CHANGED
|
@@ -1,27 +1,49 @@
|
|
|
1
|
-
const CACHE_NAME = 'im-
|
|
1
|
+
const CACHE_NAME = 'im-v3';
|
|
2
2
|
|
|
3
3
|
self.addEventListener('install', (event) => {
|
|
4
4
|
self.skipWaiting();
|
|
5
5
|
});
|
|
6
6
|
|
|
7
7
|
self.addEventListener('activate', (event) => {
|
|
8
|
-
event.waitUntil(
|
|
8
|
+
event.waitUntil(
|
|
9
|
+
caches.keys().then((keys) =>
|
|
10
|
+
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
|
|
11
|
+
).then(() => clients.claim())
|
|
12
|
+
);
|
|
9
13
|
});
|
|
10
14
|
|
|
11
15
|
self.addEventListener('fetch', (event) => {
|
|
12
|
-
|
|
16
|
+
const url = new URL(event.request.url);
|
|
17
|
+
|
|
18
|
+
// Skip non-GET and API requests
|
|
19
|
+
if (event.request.method !== 'GET' || url.pathname.startsWith('/api/')) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Navigation requests — network-first, fallback to cached root
|
|
24
|
+
if (event.request.mode === 'navigate') {
|
|
25
|
+
event.respondWith(
|
|
26
|
+
fetch(event.request).catch(async () => {
|
|
27
|
+
const cached = await caches.match('/');
|
|
28
|
+
return cached || new Response('Offline', { status: 503, headers: { 'Content-Type': 'text/html' } });
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Static assets — network-first with cache fallback
|
|
13
35
|
event.respondWith(
|
|
14
36
|
fetch(event.request)
|
|
15
37
|
.then((response) => {
|
|
16
|
-
|
|
17
|
-
if (event.request.method === 'GET' && response.status === 200) {
|
|
38
|
+
if (response.status === 200) {
|
|
18
39
|
const clone = response.clone();
|
|
19
|
-
caches.open(CACHE_NAME).then((cache) =>
|
|
20
|
-
cache.put(event.request, clone);
|
|
21
|
-
});
|
|
40
|
+
caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
|
|
22
41
|
}
|
|
23
42
|
return response;
|
|
24
43
|
})
|
|
25
|
-
.catch(() =>
|
|
44
|
+
.catch(async () => {
|
|
45
|
+
const cached = await caches.match(event.request);
|
|
46
|
+
return cached || new Response('', { status: 404 });
|
|
47
|
+
})
|
|
26
48
|
);
|
|
27
49
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { getSubProjectsWithStats, createSubProject } from '@/lib/db/queries/sub-projects';
|
|
2
|
+
import { getSubProjectsWithStats, createSubProject, reorderSubProjects } from '@/lib/db/queries/sub-projects';
|
|
3
3
|
|
|
4
4
|
export async function GET(
|
|
5
5
|
_request: NextRequest,
|
|
@@ -29,3 +29,19 @@ export async function POST(
|
|
|
29
29
|
});
|
|
30
30
|
return NextResponse.json(sp, { status: 201 });
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
export async function PUT(
|
|
34
|
+
request: NextRequest,
|
|
35
|
+
{ params }: { params: Promise<{ id: string }> },
|
|
36
|
+
) {
|
|
37
|
+
const { id } = await params;
|
|
38
|
+
const body = await request.json();
|
|
39
|
+
|
|
40
|
+
if (!Array.isArray(body.orderedIds)) {
|
|
41
|
+
return NextResponse.json({ error: 'orderedIds array is required' }, { status: 400 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
reorderSubProjects(id, body.orderedIds);
|
|
45
|
+
const subProjects = getSubProjectsWithStats(id);
|
|
46
|
+
return NextResponse.json(subProjects);
|
|
47
|
+
}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useState } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
DndContext,
|
|
6
|
+
closestCenter,
|
|
7
|
+
KeyboardSensor,
|
|
8
|
+
PointerSensor,
|
|
9
|
+
useSensor,
|
|
10
|
+
useSensors,
|
|
11
|
+
type DragEndEvent,
|
|
12
|
+
} from '@dnd-kit/core';
|
|
13
|
+
import {
|
|
14
|
+
SortableContext,
|
|
15
|
+
sortableKeyboardCoordinates,
|
|
16
|
+
verticalListSortingStrategy,
|
|
17
|
+
useSortable,
|
|
18
|
+
} from '@dnd-kit/sortable';
|
|
19
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
4
20
|
import type { ITask, ISubProjectWithStats, TaskStatus } from '@/types';
|
|
5
21
|
import { statusIcon } from './StatusFlow';
|
|
6
22
|
|
|
@@ -23,6 +39,7 @@ export default function ProjectTree({
|
|
|
23
39
|
onStatusChange,
|
|
24
40
|
onTodayToggle,
|
|
25
41
|
onDeleteTask,
|
|
42
|
+
onReorderSubs,
|
|
26
43
|
}: {
|
|
27
44
|
subProjects: ISubProjectWithStats[];
|
|
28
45
|
tasks: ITask[];
|
|
@@ -36,11 +53,31 @@ export default function ProjectTree({
|
|
|
36
53
|
onStatusChange: (taskId: string, status: TaskStatus) => void;
|
|
37
54
|
onTodayToggle: (taskId: string, isToday: boolean) => void;
|
|
38
55
|
onDeleteTask: (taskId: string) => void;
|
|
56
|
+
onReorderSubs?: (orderedIds: string[]) => void;
|
|
39
57
|
}) {
|
|
40
58
|
const [collapsedSubs, setCollapsedSubs] = useState<Set<string>>(new Set());
|
|
41
59
|
const [addingTaskFor, setAddingTaskFor] = useState<string | null>(null);
|
|
42
60
|
const [newTaskTitle, setNewTaskTitle] = useState('');
|
|
43
61
|
|
|
62
|
+
const sensors = useSensors(
|
|
63
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
|
64
|
+
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const handleDragEnd = (event: DragEndEvent) => {
|
|
68
|
+
const { active, over } = event;
|
|
69
|
+
if (!over || active.id === over.id || !onReorderSubs) return;
|
|
70
|
+
|
|
71
|
+
const oldIndex = subProjects.findIndex(sp => sp.id === active.id);
|
|
72
|
+
const newIndex = subProjects.findIndex(sp => sp.id === over.id);
|
|
73
|
+
if (oldIndex === -1 || newIndex === -1) return;
|
|
74
|
+
|
|
75
|
+
const newOrder = [...subProjects];
|
|
76
|
+
const [moved] = newOrder.splice(oldIndex, 1);
|
|
77
|
+
newOrder.splice(newIndex, 0, moved);
|
|
78
|
+
onReorderSubs(newOrder.map(sp => sp.id));
|
|
79
|
+
};
|
|
80
|
+
|
|
44
81
|
const toggleCollapse = (subId: string) => {
|
|
45
82
|
setCollapsedSubs(prev => {
|
|
46
83
|
const next = new Set(prev);
|
|
@@ -79,151 +116,236 @@ export default function ProjectTree({
|
|
|
79
116
|
</div>
|
|
80
117
|
)}
|
|
81
118
|
|
|
82
|
-
{
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
119
|
+
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
|
120
|
+
<SortableContext items={subProjects.map(sp => sp.id)} strategy={verticalListSortingStrategy}>
|
|
121
|
+
{subProjects.map((sp) => (
|
|
122
|
+
<SortableSubProject
|
|
123
|
+
key={sp.id}
|
|
124
|
+
sp={sp}
|
|
125
|
+
isSelected={selectedSubId === sp.id}
|
|
126
|
+
isCollapsed={collapsedSubs.has(sp.id)}
|
|
127
|
+
tasks={selectedSubId === sp.id ? tasks : []}
|
|
128
|
+
selectedTaskId={selectedTaskId}
|
|
129
|
+
addingTaskFor={addingTaskFor}
|
|
130
|
+
newTaskTitle={newTaskTitle}
|
|
131
|
+
onSelectSub={onSelectSub}
|
|
132
|
+
onSelectTask={onSelectTask}
|
|
133
|
+
onToggleCollapse={toggleCollapse}
|
|
134
|
+
onDeleteSub={onDeleteSub}
|
|
135
|
+
onStatusChange={onStatusChange}
|
|
136
|
+
onTodayToggle={onTodayToggle}
|
|
137
|
+
onDeleteTask={onDeleteTask}
|
|
138
|
+
onAddTask={handleAddTask}
|
|
139
|
+
onSetAddingTaskFor={setAddingTaskFor}
|
|
140
|
+
onSetNewTaskTitle={setNewTaskTitle}
|
|
141
|
+
/>
|
|
142
|
+
))}
|
|
143
|
+
</SortableContext>
|
|
144
|
+
</DndContext>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function SortableSubProject({
|
|
151
|
+
sp,
|
|
152
|
+
isSelected,
|
|
153
|
+
isCollapsed,
|
|
154
|
+
tasks: subTasks,
|
|
155
|
+
selectedTaskId,
|
|
156
|
+
addingTaskFor,
|
|
157
|
+
newTaskTitle,
|
|
158
|
+
onSelectSub,
|
|
159
|
+
onSelectTask,
|
|
160
|
+
onToggleCollapse,
|
|
161
|
+
onDeleteSub,
|
|
162
|
+
onStatusChange,
|
|
163
|
+
onTodayToggle,
|
|
164
|
+
onDeleteTask,
|
|
165
|
+
onAddTask,
|
|
166
|
+
onSetAddingTaskFor,
|
|
167
|
+
onSetNewTaskTitle,
|
|
168
|
+
}: {
|
|
169
|
+
sp: ISubProjectWithStats;
|
|
170
|
+
isSelected: boolean;
|
|
171
|
+
isCollapsed: boolean;
|
|
172
|
+
tasks: ITask[];
|
|
173
|
+
selectedTaskId: string | null;
|
|
174
|
+
addingTaskFor: string | null;
|
|
175
|
+
newTaskTitle: string;
|
|
176
|
+
onSelectSub: (subId: string) => void;
|
|
177
|
+
onSelectTask: (taskId: string) => void;
|
|
178
|
+
onToggleCollapse: (subId: string) => void;
|
|
179
|
+
onDeleteSub: (subId: string) => void;
|
|
180
|
+
onStatusChange: (taskId: string, status: TaskStatus) => void;
|
|
181
|
+
onTodayToggle: (taskId: string, isToday: boolean) => void;
|
|
182
|
+
onDeleteTask: (taskId: string) => void;
|
|
183
|
+
onAddTask: (subId: string) => void;
|
|
184
|
+
onSetAddingTaskFor: (subId: string | null) => void;
|
|
185
|
+
onSetNewTaskTitle: (title: string) => void;
|
|
186
|
+
}) {
|
|
187
|
+
const {
|
|
188
|
+
attributes,
|
|
189
|
+
listeners,
|
|
190
|
+
setNodeRef,
|
|
191
|
+
transform,
|
|
192
|
+
transition,
|
|
193
|
+
isDragging,
|
|
194
|
+
} = useSortable({ id: sp.id });
|
|
195
|
+
|
|
196
|
+
const style = {
|
|
197
|
+
transform: CSS.Transform.toString(transform),
|
|
198
|
+
transition,
|
|
199
|
+
opacity: isDragging ? 0.5 : 1,
|
|
200
|
+
zIndex: isDragging ? 10 : undefined,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div ref={setNodeRef} style={style} className="mb-0.5">
|
|
205
|
+
{/* Sub-project node */}
|
|
206
|
+
<div
|
|
207
|
+
onClick={() => {
|
|
208
|
+
onSelectSub(sp.id);
|
|
209
|
+
if (isCollapsed) onToggleCollapse(sp.id);
|
|
210
|
+
}}
|
|
211
|
+
className={`flex items-center gap-1.5 px-2 py-1.5 cursor-pointer transition-colors group text-sm ${
|
|
212
|
+
isSelected
|
|
213
|
+
? 'text-foreground'
|
|
214
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
215
|
+
}`}
|
|
216
|
+
>
|
|
217
|
+
{/* Drag handle */}
|
|
218
|
+
<span
|
|
219
|
+
{...attributes}
|
|
220
|
+
{...listeners}
|
|
221
|
+
className="w-4 h-4 flex items-center justify-center text-xs text-muted-foreground/40 hover:text-muted-foreground cursor-grab active:cursor-grabbing flex-shrink-0"
|
|
222
|
+
title="Drag to reorder"
|
|
223
|
+
onClick={(e) => e.stopPropagation()}
|
|
224
|
+
>
|
|
225
|
+
⠿
|
|
226
|
+
</span>
|
|
227
|
+
<button
|
|
228
|
+
onClick={(e) => { e.stopPropagation(); onToggleCollapse(sp.id); }}
|
|
229
|
+
className="w-4 h-4 flex items-center justify-center text-xs text-muted-foreground flex-shrink-0"
|
|
230
|
+
>
|
|
231
|
+
{isCollapsed ? '\u25B6' : '\u25BC'}
|
|
232
|
+
</button>
|
|
233
|
+
<span className={`flex-1 truncate font-medium ${isSelected ? 'text-primary' : ''}`}>
|
|
234
|
+
{sp.name}
|
|
235
|
+
</span>
|
|
236
|
+
<div className="flex items-center gap-1.5">
|
|
237
|
+
{sp.task_count > 0 && (
|
|
238
|
+
<span className="text-xs text-muted-foreground tabular-nums">{sp.task_count}</span>
|
|
239
|
+
)}
|
|
240
|
+
<button
|
|
241
|
+
onClick={(e) => { e.stopPropagation(); onDeleteSub(sp.id); }}
|
|
242
|
+
className="text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity text-xs"
|
|
243
|
+
>
|
|
244
|
+
x
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{/* Tasks (children) */}
|
|
250
|
+
{!isCollapsed && isSelected && (
|
|
251
|
+
<div className="ml-3 border-l border-border/50">
|
|
252
|
+
{subTasks.length === 0 && !addingTaskFor && (
|
|
253
|
+
<div className="text-xs text-muted-foreground py-2 pl-4">
|
|
254
|
+
No tasks
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
{subTasks.map((task) => (
|
|
258
|
+
<div
|
|
259
|
+
key={task.id}
|
|
260
|
+
onClick={() => onSelectTask(task.id)}
|
|
261
|
+
className={`group/task flex items-center gap-1.5 pl-4 pr-2 py-1.5 cursor-pointer transition-colors text-sm border-l-2 ${
|
|
262
|
+
selectedTaskId === task.id
|
|
263
|
+
? 'bg-card-hover border-l-primary'
|
|
264
|
+
: 'border-l-transparent hover:bg-card-hover/50'
|
|
265
|
+
}`}
|
|
266
|
+
>
|
|
267
|
+
<button
|
|
268
|
+
onClick={(e) => {
|
|
269
|
+
e.stopPropagation();
|
|
270
|
+
const nextStatus = getNextStatus(task.status);
|
|
271
|
+
onStatusChange(task.id, nextStatus);
|
|
94
272
|
}}
|
|
95
|
-
className=
|
|
96
|
-
|
|
97
|
-
? 'text-foreground'
|
|
98
|
-
: 'text-muted-foreground hover:text-foreground'
|
|
99
|
-
}`}
|
|
273
|
+
className="flex-shrink-0 text-sm"
|
|
274
|
+
title={`Status: ${task.status}`}
|
|
100
275
|
>
|
|
276
|
+
{statusIcon(task.status)}
|
|
277
|
+
</button>
|
|
278
|
+
<span className={`tree-priority-dot ${PRIORITY_COLORS[task.priority]}`} />
|
|
279
|
+
<span className={`flex-1 truncate ${task.status === 'done' ? 'text-muted-foreground line-through' : ''}`}>
|
|
280
|
+
{task.title}
|
|
281
|
+
</span>
|
|
282
|
+
{task.is_today && (
|
|
101
283
|
<button
|
|
102
|
-
onClick={(e) => { e.stopPropagation();
|
|
103
|
-
className="
|
|
284
|
+
onClick={(e) => { e.stopPropagation(); onTodayToggle(task.id, false); }}
|
|
285
|
+
className="text-xs flex-shrink-0 text-primary" title="Remove from today"
|
|
104
286
|
>
|
|
105
|
-
|
|
287
|
+
*
|
|
106
288
|
</button>
|
|
107
|
-
<span className={`flex-1 truncate font-medium ${isSelected ? 'text-primary' : ''}`}>
|
|
108
|
-
{sp.name}
|
|
109
|
-
</span>
|
|
110
|
-
<div className="flex items-center gap-1.5">
|
|
111
|
-
{sp.task_count > 0 && (
|
|
112
|
-
<span className="text-xs text-muted-foreground tabular-nums">{sp.task_count}</span>
|
|
113
|
-
)}
|
|
114
|
-
<button
|
|
115
|
-
onClick={(e) => { e.stopPropagation(); onDeleteSub(sp.id); }}
|
|
116
|
-
className="text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity text-xs"
|
|
117
|
-
>
|
|
118
|
-
x
|
|
119
|
-
</button>
|
|
120
|
-
</div>
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
{/* Tasks (children) */}
|
|
124
|
-
{!isCollapsed && isSelected && (
|
|
125
|
-
<div className="ml-3 border-l border-border/50">
|
|
126
|
-
{subTasks.length === 0 && !addingTaskFor && (
|
|
127
|
-
<div className="text-xs text-muted-foreground py-2 pl-4">
|
|
128
|
-
No tasks
|
|
129
|
-
</div>
|
|
130
|
-
)}
|
|
131
|
-
{subTasks.map((task) => (
|
|
132
|
-
<div
|
|
133
|
-
key={task.id}
|
|
134
|
-
onClick={() => onSelectTask(task.id)}
|
|
135
|
-
className={`group/task flex items-center gap-1.5 pl-4 pr-2 py-1.5 cursor-pointer transition-colors text-sm border-l-2 ${
|
|
136
|
-
selectedTaskId === task.id
|
|
137
|
-
? 'bg-card-hover border-l-primary'
|
|
138
|
-
: 'border-l-transparent hover:bg-card-hover/50'
|
|
139
|
-
}`}
|
|
140
|
-
>
|
|
141
|
-
<button
|
|
142
|
-
onClick={(e) => {
|
|
143
|
-
e.stopPropagation();
|
|
144
|
-
const nextStatus = getNextStatus(task.status);
|
|
145
|
-
onStatusChange(task.id, nextStatus);
|
|
146
|
-
}}
|
|
147
|
-
className="flex-shrink-0 text-sm"
|
|
148
|
-
title={`Status: ${task.status}`}
|
|
149
|
-
>
|
|
150
|
-
{statusIcon(task.status)}
|
|
151
|
-
</button>
|
|
152
|
-
<span className={`tree-priority-dot ${PRIORITY_COLORS[task.priority]}`} />
|
|
153
|
-
<span className={`flex-1 truncate ${task.status === 'done' ? 'text-muted-foreground line-through' : ''}`}>
|
|
154
|
-
{task.title}
|
|
155
|
-
</span>
|
|
156
|
-
{task.is_today && (
|
|
157
|
-
<button
|
|
158
|
-
onClick={(e) => { e.stopPropagation(); onTodayToggle(task.id, false); }}
|
|
159
|
-
className="text-xs flex-shrink-0 text-primary" title="Remove from today"
|
|
160
|
-
>
|
|
161
|
-
*
|
|
162
|
-
</button>
|
|
163
|
-
)}
|
|
164
|
-
<button
|
|
165
|
-
onClick={(e) => {
|
|
166
|
-
e.stopPropagation();
|
|
167
|
-
onDeleteTask(task.id);
|
|
168
|
-
}}
|
|
169
|
-
className="flex-shrink-0 text-muted-foreground/0 group-hover/task:text-muted-foreground
|
|
170
|
-
hover:!text-destructive transition-colors text-xs px-0.5"
|
|
171
|
-
title="Delete task"
|
|
172
|
-
>
|
|
173
|
-
×
|
|
174
|
-
</button>
|
|
175
|
-
</div>
|
|
176
|
-
))}
|
|
177
|
-
|
|
178
|
-
{/* Add task input */}
|
|
179
|
-
{addingTaskFor === sp.id ? (
|
|
180
|
-
<div className="pl-4 pr-2 py-1">
|
|
181
|
-
<input
|
|
182
|
-
type="text"
|
|
183
|
-
value={newTaskTitle}
|
|
184
|
-
onChange={(e) => setNewTaskTitle(e.target.value)}
|
|
185
|
-
onKeyDown={(e) => {
|
|
186
|
-
if (e.key === 'Enter') handleAddTask(sp.id);
|
|
187
|
-
if (e.key === 'Escape') { setNewTaskTitle(''); setAddingTaskFor(null); }
|
|
188
|
-
}}
|
|
189
|
-
placeholder="Task title..."
|
|
190
|
-
className="w-full bg-input border border-border rounded px-2 py-1 text-sm
|
|
191
|
-
focus:border-primary focus:outline-none text-foreground"
|
|
192
|
-
autoFocus
|
|
193
|
-
/>
|
|
194
|
-
</div>
|
|
195
|
-
) : (
|
|
196
|
-
<button
|
|
197
|
-
data-add-task
|
|
198
|
-
onClick={() => { onSelectSub(sp.id); setAddingTaskFor(sp.id); }}
|
|
199
|
-
className="pl-4 pr-2 py-1 text-xs text-muted-foreground hover:text-foreground
|
|
200
|
-
transition-colors text-left w-full"
|
|
201
|
-
>
|
|
202
|
-
+ Add task <span className="text-muted-foreground/50 ml-1">T</span>
|
|
203
|
-
</button>
|
|
204
|
-
)}
|
|
205
|
-
</div>
|
|
206
289
|
)}
|
|
290
|
+
<button
|
|
291
|
+
onClick={(e) => {
|
|
292
|
+
e.stopPropagation();
|
|
293
|
+
onDeleteTask(task.id);
|
|
294
|
+
}}
|
|
295
|
+
className="flex-shrink-0 text-muted-foreground/0 group-hover/task:text-muted-foreground
|
|
296
|
+
hover:!text-destructive transition-colors text-xs px-0.5"
|
|
297
|
+
title="Delete task"
|
|
298
|
+
>
|
|
299
|
+
×
|
|
300
|
+
</button>
|
|
301
|
+
</div>
|
|
302
|
+
))}
|
|
207
303
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
304
|
+
{/* Add task input */}
|
|
305
|
+
{addingTaskFor === sp.id ? (
|
|
306
|
+
<div className="pl-4 pr-2 py-1">
|
|
307
|
+
<input
|
|
308
|
+
type="text"
|
|
309
|
+
value={newTaskTitle}
|
|
310
|
+
onChange={(e) => onSetNewTaskTitle(e.target.value)}
|
|
311
|
+
onKeyDown={(e) => {
|
|
312
|
+
if (e.key === 'Enter') onAddTask(sp.id);
|
|
313
|
+
if (e.key === 'Escape') { onSetNewTaskTitle(''); onSetAddingTaskFor(null); }
|
|
314
|
+
}}
|
|
315
|
+
placeholder="Task title..."
|
|
316
|
+
className="w-full bg-input border border-border rounded px-2 py-1 text-sm
|
|
317
|
+
focus:border-primary focus:outline-none text-foreground"
|
|
318
|
+
autoFocus
|
|
319
|
+
/>
|
|
223
320
|
</div>
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
|
|
321
|
+
) : (
|
|
322
|
+
<button
|
|
323
|
+
data-add-task
|
|
324
|
+
onClick={() => { onSelectSub(sp.id); onSetAddingTaskFor(sp.id); }}
|
|
325
|
+
className="pl-4 pr-2 py-1 text-xs text-muted-foreground hover:text-foreground
|
|
326
|
+
transition-colors text-left w-full"
|
|
327
|
+
>
|
|
328
|
+
+ Add task <span className="text-muted-foreground/50 ml-1">T</span>
|
|
329
|
+
</button>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
|
|
334
|
+
{/* Show task previews for non-selected sub-projects */}
|
|
335
|
+
{!isCollapsed && !isSelected && sp.preview_tasks && sp.preview_tasks.length > 0 && (
|
|
336
|
+
<div className="ml-3 border-l border-border/50">
|
|
337
|
+
{sp.preview_tasks.map((pt, i) => (
|
|
338
|
+
<div
|
|
339
|
+
key={i}
|
|
340
|
+
onClick={() => onSelectSub(sp.id)}
|
|
341
|
+
className="flex items-center gap-1.5 pl-4 pr-2 py-1 cursor-pointer text-xs text-muted-foreground hover:text-foreground transition-colors"
|
|
342
|
+
>
|
|
343
|
+
<span className="flex-shrink-0">{statusIcon(pt.status)}</span>
|
|
344
|
+
<span className="truncate">{pt.title}</span>
|
|
345
|
+
</div>
|
|
346
|
+
))}
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
227
349
|
</div>
|
|
228
350
|
);
|
|
229
351
|
}
|
|
@@ -160,9 +160,9 @@ export default function TaskDetail({
|
|
|
160
160
|
onBlur={saveDescription}
|
|
161
161
|
placeholder="Background, conditions, notes..."
|
|
162
162
|
className="w-full bg-input border border-border rounded-lg px-3 py-2.5 text-sm
|
|
163
|
-
focus:border-primary focus:outline-none text-foreground resize-
|
|
163
|
+
focus:border-primary focus:outline-none text-foreground resize-y min-h-[60px]
|
|
164
164
|
leading-relaxed"
|
|
165
|
-
rows={
|
|
165
|
+
rows={3}
|
|
166
166
|
/>
|
|
167
167
|
</div>
|
|
168
168
|
|
|
@@ -78,7 +78,7 @@ export default function WorkspacePanel({
|
|
|
78
78
|
const handleMouseMove = (e: MouseEvent) => {
|
|
79
79
|
if (!draggingRef.current) return;
|
|
80
80
|
const delta = e.clientX - startXRef.current;
|
|
81
|
-
const newWidth = Math.max(180, Math.min(
|
|
81
|
+
const newWidth = Math.max(180, Math.min(900, startWidthRef.current + delta));
|
|
82
82
|
if (draggingRef.current === 'left') setLeftWidth(newWidth);
|
|
83
83
|
else setCenterWidth(newWidth);
|
|
84
84
|
};
|
|
@@ -217,6 +217,20 @@ export default function WorkspacePanel({
|
|
|
217
217
|
}
|
|
218
218
|
};
|
|
219
219
|
|
|
220
|
+
const handleReorderSubs = async (orderedIds: string[]) => {
|
|
221
|
+
// Optimistic update
|
|
222
|
+
setSubProjects(prev => {
|
|
223
|
+
const map = new Map(prev.map(sp => [sp.id, sp]));
|
|
224
|
+
return orderedIds.map(id => map.get(id)!).filter(Boolean);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await fetch(`/api/projects/${id}/sub-projects`, {
|
|
228
|
+
method: 'PUT',
|
|
229
|
+
headers: { 'Content-Type': 'application/json' },
|
|
230
|
+
body: JSON.stringify({ orderedIds }),
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
|
|
220
234
|
const handleTaskDelete = (taskId?: string) => {
|
|
221
235
|
const tid = taskId || selectedTaskId;
|
|
222
236
|
if (!tid) return;
|
|
@@ -403,7 +417,8 @@ export default function WorkspacePanel({
|
|
|
403
417
|
onSelectTask={setSelectedTaskId}
|
|
404
418
|
onCreateSub={() => setShowAddSub(true)} onDeleteSub={handleDeleteSubProject}
|
|
405
419
|
onCreateTask={handleCreateTask} onStatusChange={handleTaskStatusChange}
|
|
406
|
-
onTodayToggle={handleTaskTodayToggle} onDeleteTask={handleTaskDelete}
|
|
420
|
+
onTodayToggle={handleTaskTodayToggle} onDeleteTask={handleTaskDelete}
|
|
421
|
+
onReorderSubs={handleReorderSubs} />
|
|
407
422
|
</div>
|
|
408
423
|
|
|
409
424
|
<div className="panel-resize-handle" onMouseDown={(e) => handleMouseDown('center', e)}>
|