pawmode 1.2.0 → 1.3.0
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.md
CHANGED
|
@@ -74,7 +74,7 @@ npx pawmode --preset developer --yes
|
|
|
74
74
|
|
|
75
75
|
## What is OpenPaw?
|
|
76
76
|
|
|
77
|
-
OpenPaw turns **Claude Code** into a full personal assistant. One command,
|
|
77
|
+
OpenPaw turns **Claude Code** into a full personal assistant. One command, 38 skills, and a really good boy who fetches your emails, plays your music, and controls your smart home.
|
|
78
78
|
|
|
79
79
|
```
|
|
80
80
|
npx pawmode
|
|
@@ -211,10 +211,10 @@ Get started fast with a preset, or choose `Custom` to sniff through skills one b
|
|
|
211
211
|
|
|
212
212
|
| Preset | Skills |
|
|
213
213
|
|---|---|
|
|
214
|
-
| **Everything** | All
|
|
214
|
+
| **Everything** | All 38 skills for your platform |
|
|
215
215
|
| **Essentials** | Email, calendar, notes, music, weather, clipboard, browser, system, notifications |
|
|
216
216
|
| **Productivity** | Notes, Obsidian, tasks, email, calendar, Slack, cloud files, notifications |
|
|
217
|
-
| **Developer** | GitHub, Linear, Jira, browser, network,
|
|
217
|
+
| **Developer** | GitHub, Linear, Jira, browser, network, cron |
|
|
218
218
|
| **Creative & Media** | Music, video, screen, voice, browser, research |
|
|
219
219
|
| **Smart Home** | Lights, speakers, Bluetooth, system, display, notifications |
|
|
220
220
|
|
|
@@ -222,7 +222,7 @@ Get started fast with a preset, or choose `Custom` to sniff through skills one b
|
|
|
222
222
|
|
|
223
223
|
## Skills
|
|
224
224
|
|
|
225
|
-
|
|
225
|
+
38 capabilities across 8 categories. Install only what you need.
|
|
226
226
|
|
|
227
227
|
### Productivity
|
|
228
228
|
|
|
@@ -231,7 +231,7 @@ Get started fast with a preset, or choose `Custom` to sniff through skills one b
|
|
|
231
231
|
| `c-notes` | Apple Notes + Reminders | [`memo`](https://github.com/antoniorodr/memo) [`remindctl`](https://github.com/nicklama/remindctl) |
|
|
232
232
|
| `c-obsidian` | Obsidian vault management | [`obsidian-cli`](https://github.com/yakitrak/obsidian-cli) |
|
|
233
233
|
| `c-notion` | Notion pages + databases | [`notion-cli`](https://github.com/litencatt/notion-cli) |
|
|
234
|
-
| `c-tasks` | Todoist /
|
|
234
|
+
| `c-tasks` | Todoist / Taskwarrior | [`todoist-cli`](https://github.com/sachaos/todoist) [`taskwarrior`](https://github.com/GothenburgBitFactory/taskwarrior) |
|
|
235
235
|
|
|
236
236
|
### Communication
|
|
237
237
|
|
|
@@ -249,7 +249,7 @@ Get started fast with a preset, or choose `Custom` to sniff through skills one b
|
|
|
249
249
|
|---|---|---|
|
|
250
250
|
| `c-music` | Spotify playback + search | [`spogo`](https://github.com/steipete/spogo) |
|
|
251
251
|
| `c-video` | YouTube download + convert | [`yt-dlp`](https://github.com/yt-dlp/yt-dlp) [`ffmpeg`](https://ffmpeg.org/) |
|
|
252
|
-
| `c-video-edit` | Programmatic video creation | [`remotion`](https://github.com/remotion-dev/remotion)
|
|
252
|
+
| `c-video-edit` | Programmatic video creation | [`remotion`](https://github.com/remotion-dev/remotion) |
|
|
253
253
|
| `c-screen` | Screenshots, OCR, webcam | [`peekaboo`](https://github.com/steipete/peekaboo) [`camsnap`](https://github.com/nicklama/camsnap) |
|
|
254
254
|
| `c-voice` | Speech-to-text + TTS | [`sag`](https://github.com/steipete/sag) |
|
|
255
255
|
|
|
@@ -293,7 +293,6 @@ Get started fast with a preset, or choose `Custom` to sniff through skills one b
|
|
|
293
293
|
| `c-tracking` | Package tracking (UPS, FedEx, etc.) | [`ordercli`](https://github.com/steipete/ordercli) |
|
|
294
294
|
| `c-secrets` | 1Password / Bitwarden | [`op`](https://developer.1password.com/docs/cli/) [`bw`](https://github.com/bitwarden/clients) |
|
|
295
295
|
| `c-network` | DNS lookups + HTTP client | [`doggo`](https://github.com/mr-karan/doggo) [`httpie`](https://github.com/httpie/cli) |
|
|
296
|
-
| `c-ai` | Query LLMs — pipe text, chat, summarize | [`llm`](https://github.com/simonw/llm) [`aichat`](https://github.com/sigoden/aichat) |
|
|
297
296
|
|
|
298
297
|
### Developer
|
|
299
298
|
|
|
@@ -321,6 +320,7 @@ Get started fast with a preset, or choose `Custom` to sniff through skills one b
|
|
|
321
320
|
| `openpaw schedule list` | List scheduled jobs |
|
|
322
321
|
| `openpaw schedule costs` | View cost usage and daily cap |
|
|
323
322
|
| `openpaw schedule set-cap <usd>` | Set daily cost cap |
|
|
323
|
+
| `openpaw configure` | Configure your setup — add skills, personality, dashboard |
|
|
324
324
|
| `openpaw list` | Show all available skills |
|
|
325
325
|
| `openpaw add <skills>` | Add skills — `openpaw add notes music email` |
|
|
326
326
|
| `openpaw remove <skills>` | Remove skills |
|
|
@@ -452,7 +452,7 @@ Instructions for Claude on how to use the CLI tool...
|
|
|
452
452
|
- **5-minute setup** — from zero to personal assistant
|
|
453
453
|
- **Telegram built-in** — talk to Claude from your phone
|
|
454
454
|
- **Task dashboard** — local kanban board with 3 themes
|
|
455
|
-
- **
|
|
455
|
+
- **38 skills** — email, music, smart home, GitHub, scheduling, video editing, and more
|
|
456
456
|
- **Cost-controlled scheduling** — automate tasks without runaway bills
|
|
457
457
|
- **Open source** — MIT license, community-driven
|
|
458
458
|
|
|
@@ -48,12 +48,13 @@ function generateDashboardHTML(theme, botName) {
|
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
const t = themes[theme];
|
|
51
|
+
const safeBotName = botName.replace(/[&<>"']/g, "");
|
|
51
52
|
return `<!DOCTYPE html>
|
|
52
53
|
<html lang="en">
|
|
53
54
|
<head>
|
|
54
55
|
<meta charset="UTF-8">
|
|
55
56
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
56
|
-
<title>${
|
|
57
|
+
<title>${safeBotName} — Task Dashboard</title>
|
|
57
58
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
58
59
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
59
60
|
<style>
|
|
@@ -113,168 +114,273 @@ header{display:flex;align-items:center;justify-content:space-between;padding:20p
|
|
|
113
114
|
.form-actions .save{background:var(--accent);color:var(--bg);border-color:var(--accent);font-weight:600}
|
|
114
115
|
.form-actions .save:hover{opacity:.9}
|
|
115
116
|
.form-actions .cancel:hover{border-color:var(--text-dim)}
|
|
117
|
+
.toast{position:fixed;bottom:20px;right:20px;padding:10px 16px;border-radius:8px;font-size:12px;opacity:0;transition:opacity .3s;pointer-events:none;z-index:100}
|
|
118
|
+
.toast.show{opacity:1}
|
|
119
|
+
.toast.error{background:var(--high);color:#fff}
|
|
120
|
+
.toast.success{background:var(--done);color:#fff}
|
|
116
121
|
</style>
|
|
117
122
|
</head>
|
|
118
123
|
<body>
|
|
119
124
|
<header>
|
|
120
|
-
<div class="logo"><span>🐾</span> ${
|
|
125
|
+
<div class="logo"><span>🐾</span> ${safeBotName}</div>
|
|
121
126
|
<div class="theme-switcher">
|
|
122
127
|
<div class="theme-dot${theme === "paw" ? " active" : ""}" data-theme="paw" title="Paw"></div>
|
|
123
128
|
<div class="theme-dot${theme === "midnight" ? " active" : ""}" data-theme="midnight" title="Midnight"></div>
|
|
124
129
|
<div class="theme-dot${theme === "neon" ? " active" : ""}" data-theme="neon" title="Neon"></div>
|
|
125
130
|
</div>
|
|
126
131
|
</header>
|
|
127
|
-
<div class="board">
|
|
128
|
-
<div class="
|
|
129
|
-
<div class="col-header"><span class="col-title">Todo</span><span class="col-count" id="count-todo">0</span></div>
|
|
130
|
-
<div class="cards" id="cards-todo"></div>
|
|
131
|
-
<button class="add-btn" onclick="showForm('todo')">+ Add task</button>
|
|
132
|
-
<div class="add-form" id="form-todo">
|
|
133
|
-
<input type="text" id="input-todo" placeholder="Task title..." onkeydown="if(event.key==='Enter')saveNew('todo')">
|
|
134
|
-
<textarea id="desc-todo" placeholder="Description (optional)" rows="2"></textarea>
|
|
135
|
-
<div class="form-actions">
|
|
136
|
-
<button class="cancel" onclick="hideForm('todo')">Cancel</button>
|
|
137
|
-
<button class="save" onclick="saveNew('todo')">Add</button>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
<div class="column col-progress" data-status="in-progress">
|
|
142
|
-
<div class="col-header"><span class="col-title">In Progress</span><span class="col-count" id="count-in-progress">0</span></div>
|
|
143
|
-
<div class="cards" id="cards-in-progress"></div>
|
|
144
|
-
<button class="add-btn" onclick="showForm('in-progress')">+ Add task</button>
|
|
145
|
-
<div class="add-form" id="form-in-progress">
|
|
146
|
-
<input type="text" id="input-in-progress" placeholder="Task title..." onkeydown="if(event.key==='Enter')saveNew('in-progress')">
|
|
147
|
-
<textarea id="desc-in-progress" placeholder="Description (optional)" rows="2"></textarea>
|
|
148
|
-
<div class="form-actions">
|
|
149
|
-
<button class="cancel" onclick="hideForm('in-progress')">Cancel</button>
|
|
150
|
-
<button class="save" onclick="saveNew('in-progress')">Add</button>
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
<div class="column col-done" data-status="done">
|
|
155
|
-
<div class="col-header"><span class="col-title">Done</span><span class="col-count" id="count-done">0</span></div>
|
|
156
|
-
<div class="cards" id="cards-done"></div>
|
|
157
|
-
<button class="add-btn" onclick="showForm('done')">+ Add task</button>
|
|
158
|
-
<div class="add-form" id="form-done">
|
|
159
|
-
<input type="text" id="input-done" placeholder="Task title..." onkeydown="if(event.key==='Enter')saveNew('done')">
|
|
160
|
-
<textarea id="desc-done" placeholder="Description (optional)" rows="2"></textarea>
|
|
161
|
-
<div class="form-actions">
|
|
162
|
-
<button class="cancel" onclick="hideForm('done')">Cancel</button>
|
|
163
|
-
<button class="save" onclick="saveNew('done')">Add</button>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
132
|
+
<div class="board" id="board"></div>
|
|
133
|
+
<div class="toast" id="toast"></div>
|
|
168
134
|
<script>
|
|
169
|
-
|
|
170
|
-
|
|
135
|
+
var BOTNAME = "${safeBotName}";
|
|
136
|
+
var tasks = [];
|
|
137
|
+
var dragId = null;
|
|
171
138
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
139
|
+
function toast(msg, type) {
|
|
140
|
+
var el = document.getElementById("toast");
|
|
141
|
+
el.textContent = msg;
|
|
142
|
+
el.className = "toast show " + (type || "success");
|
|
143
|
+
setTimeout(function() { el.className = "toast"; }, 2000);
|
|
175
144
|
}
|
|
176
145
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
146
|
+
function api(path, opts) {
|
|
147
|
+
return fetch("/api/" + path, Object.assign({ headers: {"Content-Type": "application/json"} }, opts || {}))
|
|
148
|
+
.then(function(r) {
|
|
149
|
+
if (!r.ok) throw new Error("Request failed: " + r.status);
|
|
150
|
+
return r.json();
|
|
151
|
+
})
|
|
152
|
+
.catch(function(err) {
|
|
153
|
+
toast(err.message, "error");
|
|
154
|
+
throw err;
|
|
155
|
+
});
|
|
180
156
|
}
|
|
181
157
|
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if(filtered.length===0){
|
|
188
|
-
container.innerHTML='<div class="empty">No tasks here yet.<br>${botName} is waiting for work! 🐾</div>';
|
|
189
|
-
}else{
|
|
190
|
-
container.innerHTML=filtered.map(t=>\`
|
|
191
|
-
<div class="card" draggable="true" data-id="\${t.id}" ondragstart="onDragStart(event)" ondragend="onDragEnd(event)">
|
|
192
|
-
<div class="card-title" contenteditable="true" onblur="updateTitle('\${t.id}',this.textContent)">\${esc(t.title)}</div>
|
|
193
|
-
\${t.description?'<div class="card-desc" contenteditable="true" onblur="updateDesc(\\''+t.id+'\\',this.textContent)">'+esc(t.description)+'</div>':''}
|
|
194
|
-
<div class="card-footer">
|
|
195
|
-
<div class="priority">
|
|
196
|
-
<div class="priority-dot high\${t.priority==='high'?' active':''}" onclick="setPriority('\${t.id}','high')" title="High"></div>
|
|
197
|
-
<div class="priority-dot normal\${t.priority==='normal'?' active':''}" onclick="setPriority('\${t.id}','normal')" title="Normal"></div>
|
|
198
|
-
<div class="priority-dot low\${t.priority==='low'?' active':''}" onclick="setPriority('\${t.id}','low')" title="Low"></div>
|
|
199
|
-
</div>
|
|
200
|
-
<span class="card-delete" onclick="del('\${t.id}')">✕</span>
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
\`).join('');
|
|
204
|
-
}
|
|
158
|
+
function load() {
|
|
159
|
+
api("tasks").then(function(data) {
|
|
160
|
+
tasks = data;
|
|
161
|
+
render();
|
|
162
|
+
});
|
|
205
163
|
}
|
|
164
|
+
|
|
165
|
+
function esc(s) {
|
|
166
|
+
var d = document.createElement("div");
|
|
167
|
+
d.textContent = s;
|
|
168
|
+
return d.innerHTML;
|
|
206
169
|
}
|
|
207
170
|
|
|
208
|
-
function
|
|
171
|
+
function render() {
|
|
172
|
+
var statuses = ["todo", "in-progress", "done"];
|
|
173
|
+
var labels = { "todo": "Todo", "in-progress": "In Progress", "done": "Done" };
|
|
174
|
+
var colClass = { "todo": "col-todo", "in-progress": "col-progress", "done": "col-done" };
|
|
175
|
+
var board = document.getElementById("board");
|
|
176
|
+
board.innerHTML = "";
|
|
209
177
|
|
|
210
|
-
function
|
|
211
|
-
|
|
178
|
+
statuses.forEach(function(status) {
|
|
179
|
+
var filtered = tasks.filter(function(t) { return t.status === status; }).sort(function(a, b) { return a.order - b.order; });
|
|
212
180
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const desc=document.getElementById('desc-'+status).value.trim();
|
|
217
|
-
const task=await api('tasks',{method:'POST',body:JSON.stringify({title,description:desc||undefined,status,priority:'normal'})});
|
|
218
|
-
tasks.push(task);
|
|
219
|
-
render();
|
|
220
|
-
hideForm(status);
|
|
221
|
-
}
|
|
181
|
+
var col = document.createElement("div");
|
|
182
|
+
col.className = "column " + colClass[status];
|
|
183
|
+
col.setAttribute("data-status", status);
|
|
222
184
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
185
|
+
// Header
|
|
186
|
+
var header = document.createElement("div");
|
|
187
|
+
header.className = "col-header";
|
|
188
|
+
header.innerHTML = '<span class="col-title">' + labels[status] + '</span><span class="col-count">' + filtered.length + '</span>';
|
|
189
|
+
col.appendChild(header);
|
|
228
190
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const t=tasks.find(x=>x.id===id);if(t)t.description=desc||undefined;
|
|
233
|
-
}
|
|
191
|
+
// Cards container
|
|
192
|
+
var cardsDiv = document.createElement("div");
|
|
193
|
+
cardsDiv.className = "cards";
|
|
234
194
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
195
|
+
if (filtered.length === 0) {
|
|
196
|
+
cardsDiv.innerHTML = '<div class="empty">No tasks here yet.<br>' + BOTNAME + ' is waiting for work! 🐾</div>';
|
|
197
|
+
} else {
|
|
198
|
+
filtered.forEach(function(task) {
|
|
199
|
+
var card = document.createElement("div");
|
|
200
|
+
card.className = "card";
|
|
201
|
+
card.draggable = true;
|
|
202
|
+
card.setAttribute("data-id", task.id);
|
|
240
203
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
204
|
+
var titleDiv = document.createElement("div");
|
|
205
|
+
titleDiv.className = "card-title";
|
|
206
|
+
titleDiv.contentEditable = "true";
|
|
207
|
+
titleDiv.textContent = task.title;
|
|
208
|
+
titleDiv.addEventListener("blur", function() {
|
|
209
|
+
var newTitle = this.textContent.trim();
|
|
210
|
+
if (newTitle && newTitle !== task.title) {
|
|
211
|
+
api("tasks/" + task.id, { method: "PUT", body: JSON.stringify({ title: newTitle }) });
|
|
212
|
+
task.title = newTitle;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
card.appendChild(titleDiv);
|
|
246
216
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
217
|
+
if (task.description) {
|
|
218
|
+
var descDiv = document.createElement("div");
|
|
219
|
+
descDiv.className = "card-desc";
|
|
220
|
+
descDiv.contentEditable = "true";
|
|
221
|
+
descDiv.textContent = task.description;
|
|
222
|
+
descDiv.addEventListener("blur", function() {
|
|
223
|
+
var newDesc = this.textContent.trim();
|
|
224
|
+
api("tasks/" + task.id, { method: "PUT", body: JSON.stringify({ description: newDesc || undefined }) });
|
|
225
|
+
task.description = newDesc || undefined;
|
|
226
|
+
});
|
|
227
|
+
card.appendChild(descDiv);
|
|
228
|
+
}
|
|
252
229
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
dragId=null;
|
|
256
|
-
document.querySelectorAll('.column').forEach(c=>c.classList.remove('drag-over'));
|
|
257
|
-
}
|
|
230
|
+
var footer = document.createElement("div");
|
|
231
|
+
footer.className = "card-footer";
|
|
258
232
|
|
|
259
|
-
document.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
272
|
-
|
|
233
|
+
var priorityDiv = document.createElement("div");
|
|
234
|
+
priorityDiv.className = "priority";
|
|
235
|
+
["high", "normal", "low"].forEach(function(p) {
|
|
236
|
+
var dot = document.createElement("div");
|
|
237
|
+
dot.className = "priority-dot " + p + (task.priority === p ? " active" : "");
|
|
238
|
+
dot.title = p.charAt(0).toUpperCase() + p.slice(1);
|
|
239
|
+
dot.addEventListener("click", function(e) {
|
|
240
|
+
e.stopPropagation();
|
|
241
|
+
api("tasks/" + task.id, { method: "PUT", body: JSON.stringify({ priority: p }) }).then(function() {
|
|
242
|
+
task.priority = p;
|
|
243
|
+
render();
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
priorityDiv.appendChild(dot);
|
|
247
|
+
});
|
|
248
|
+
footer.appendChild(priorityDiv);
|
|
273
249
|
|
|
274
|
-
document.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
250
|
+
var delBtn = document.createElement("span");
|
|
251
|
+
delBtn.className = "card-delete";
|
|
252
|
+
delBtn.innerHTML = "✕";
|
|
253
|
+
delBtn.addEventListener("click", function(e) {
|
|
254
|
+
e.stopPropagation();
|
|
255
|
+
api("tasks/" + task.id, { method: "DELETE" }).then(function() {
|
|
256
|
+
tasks = tasks.filter(function(t) { return t.id !== task.id; });
|
|
257
|
+
render();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
footer.appendChild(delBtn);
|
|
261
|
+
card.appendChild(footer);
|
|
262
|
+
|
|
263
|
+
// Drag events
|
|
264
|
+
card.addEventListener("dragstart", function(e) {
|
|
265
|
+
dragId = task.id;
|
|
266
|
+
this.classList.add("dragging");
|
|
267
|
+
e.dataTransfer.effectAllowed = "move";
|
|
268
|
+
});
|
|
269
|
+
card.addEventListener("dragend", function() {
|
|
270
|
+
this.classList.remove("dragging");
|
|
271
|
+
dragId = null;
|
|
272
|
+
document.querySelectorAll(".column").forEach(function(c) { c.classList.remove("drag-over"); });
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
cardsDiv.appendChild(card);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
col.appendChild(cardsDiv);
|
|
279
|
+
|
|
280
|
+
// Add button
|
|
281
|
+
var addBtn = document.createElement("button");
|
|
282
|
+
addBtn.type = "button";
|
|
283
|
+
addBtn.className = "add-btn";
|
|
284
|
+
addBtn.textContent = "+ Add task";
|
|
285
|
+
addBtn.addEventListener("click", function() {
|
|
286
|
+
formDiv.classList.add("show");
|
|
287
|
+
titleInput.focus();
|
|
288
|
+
});
|
|
289
|
+
col.appendChild(addBtn);
|
|
290
|
+
|
|
291
|
+
// Add form
|
|
292
|
+
var formDiv = document.createElement("div");
|
|
293
|
+
formDiv.className = "add-form";
|
|
294
|
+
|
|
295
|
+
var titleInput = document.createElement("input");
|
|
296
|
+
titleInput.type = "text";
|
|
297
|
+
titleInput.placeholder = "Task title...";
|
|
298
|
+
titleInput.addEventListener("keydown", function(e) {
|
|
299
|
+
if (e.key === "Enter") doSave();
|
|
300
|
+
});
|
|
301
|
+
formDiv.appendChild(titleInput);
|
|
302
|
+
|
|
303
|
+
var descInput = document.createElement("textarea");
|
|
304
|
+
descInput.placeholder = "Description (optional)";
|
|
305
|
+
descInput.rows = 2;
|
|
306
|
+
formDiv.appendChild(descInput);
|
|
307
|
+
|
|
308
|
+
var actions = document.createElement("div");
|
|
309
|
+
actions.className = "form-actions";
|
|
310
|
+
|
|
311
|
+
var cancelBtn = document.createElement("button");
|
|
312
|
+
cancelBtn.type = "button";
|
|
313
|
+
cancelBtn.className = "cancel";
|
|
314
|
+
cancelBtn.textContent = "Cancel";
|
|
315
|
+
cancelBtn.addEventListener("click", function() {
|
|
316
|
+
formDiv.classList.remove("show");
|
|
317
|
+
titleInput.value = "";
|
|
318
|
+
descInput.value = "";
|
|
319
|
+
});
|
|
320
|
+
actions.appendChild(cancelBtn);
|
|
321
|
+
|
|
322
|
+
var saveBtn = document.createElement("button");
|
|
323
|
+
saveBtn.type = "button";
|
|
324
|
+
saveBtn.className = "save";
|
|
325
|
+
saveBtn.textContent = "Add";
|
|
326
|
+
|
|
327
|
+
function doSave() {
|
|
328
|
+
var title = titleInput.value.trim();
|
|
329
|
+
if (!title) return;
|
|
330
|
+
var desc = descInput.value.trim();
|
|
331
|
+
saveBtn.disabled = true;
|
|
332
|
+
saveBtn.textContent = "...";
|
|
333
|
+
var body = { title: title, status: status, priority: "normal" };
|
|
334
|
+
if (desc) body.description = desc;
|
|
335
|
+
api("tasks", { method: "POST", body: JSON.stringify(body) })
|
|
336
|
+
.then(function(task) {
|
|
337
|
+
tasks.push(task);
|
|
338
|
+
render();
|
|
339
|
+
toast("Task added!");
|
|
340
|
+
})
|
|
341
|
+
.catch(function() {
|
|
342
|
+
saveBtn.disabled = false;
|
|
343
|
+
saveBtn.textContent = "Add";
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
saveBtn.addEventListener("click", doSave);
|
|
348
|
+
actions.appendChild(saveBtn);
|
|
349
|
+
formDiv.appendChild(actions);
|
|
350
|
+
col.appendChild(formDiv);
|
|
351
|
+
|
|
352
|
+
// Drop events on column
|
|
353
|
+
col.addEventListener("dragover", function(e) {
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
e.dataTransfer.dropEffect = "move";
|
|
356
|
+
this.classList.add("drag-over");
|
|
357
|
+
});
|
|
358
|
+
col.addEventListener("dragleave", function() {
|
|
359
|
+
this.classList.remove("drag-over");
|
|
360
|
+
});
|
|
361
|
+
col.addEventListener("drop", function(e) {
|
|
362
|
+
e.preventDefault();
|
|
363
|
+
this.classList.remove("drag-over");
|
|
364
|
+
if (!dragId) return;
|
|
365
|
+
var newStatus = this.getAttribute("data-status");
|
|
366
|
+
var order = tasks.filter(function(t) { return t.status === newStatus; }).length;
|
|
367
|
+
api("tasks/" + dragId, { method: "PUT", body: JSON.stringify({ status: newStatus, order: order }) })
|
|
368
|
+
.then(function() {
|
|
369
|
+
var task = tasks.find(function(t) { return t.id === dragId; });
|
|
370
|
+
if (task) { task.status = newStatus; task.order = order; }
|
|
371
|
+
render();
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
board.appendChild(col);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Theme switcher
|
|
380
|
+
document.querySelectorAll(".theme-dot").forEach(function(dot) {
|
|
381
|
+
dot.addEventListener("click", function() {
|
|
382
|
+
window.location.href = "/?theme=" + this.getAttribute("data-theme");
|
|
383
|
+
});
|
|
278
384
|
});
|
|
279
385
|
|
|
280
386
|
load();
|