yarlo-plugin-board 0.1.2 → 0.2.1

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.
@@ -0,0 +1,1060 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>yarlo board</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
10
+ <style>
11
+ :root {
12
+ --bg: #0f1419;
13
+ --surface: #1a222c;
14
+ --border: #2d3a47;
15
+ --text: #e7ecf1;
16
+ --muted: #8b9aab;
17
+ --accent: #3d9ee5;
18
+ --accent-dim: #2a6a9a;
19
+ --card: #232d38;
20
+ --danger: #e85d5d;
21
+ }
22
+ * { box-sizing: border-box; }
23
+ body {
24
+ margin: 0;
25
+ min-height: 100vh;
26
+ font-family: "DM Sans", system-ui, sans-serif;
27
+ background: var(--bg);
28
+ color: var(--text);
29
+ background-image:
30
+ radial-gradient(ellipse 120% 80% at 10% -20%, rgba(61, 158, 229, 0.12), transparent),
31
+ radial-gradient(ellipse 80% 60% at 100% 100%, rgba(45, 58, 71, 0.4), transparent);
32
+ }
33
+ header {
34
+ padding: 1.25rem 1.5rem;
35
+ border-bottom: 1px solid var(--border);
36
+ display: flex;
37
+ align-items: center;
38
+ justify-content: space-between;
39
+ flex-wrap: wrap;
40
+ gap: 0.75rem;
41
+ }
42
+ header h1 {
43
+ margin: 0;
44
+ font-size: 1.15rem;
45
+ font-weight: 700;
46
+ letter-spacing: -0.02em;
47
+ }
48
+ header h1 span { color: var(--accent); font-weight: 600; }
49
+ .toolbar {
50
+ display: flex;
51
+ align-items: center;
52
+ gap: 0.75rem;
53
+ }
54
+ button#refresh {
55
+ font-family: inherit;
56
+ font-size: 0.85rem;
57
+ padding: 0.45rem 0.9rem;
58
+ border-radius: 6px;
59
+ border: 1px solid var(--border);
60
+ background: var(--surface);
61
+ color: var(--text);
62
+ cursor: pointer;
63
+ }
64
+ button#refresh:hover { border-color: var(--accent-dim); color: var(--accent); }
65
+ #status {
66
+ font-family: "JetBrains Mono", monospace;
67
+ font-size: 0.75rem;
68
+ color: var(--muted);
69
+ }
70
+ #status.error { color: var(--danger); }
71
+ .board {
72
+ display: flex;
73
+ gap: 1rem;
74
+ padding: 1.25rem 1.5rem 2rem;
75
+ overflow-x: auto;
76
+ align-items: flex-start;
77
+ }
78
+ .column {
79
+ flex: 0 0 280px;
80
+ min-height: 200px;
81
+ background: var(--surface);
82
+ border: 1px solid var(--border);
83
+ border-radius: 10px;
84
+ display: flex;
85
+ flex-direction: column;
86
+ max-height: calc(100vh - 120px);
87
+ }
88
+ .column.orphan {
89
+ border-style: dashed;
90
+ opacity: 0.95;
91
+ }
92
+ .column-header {
93
+ padding: 0.75rem 1rem;
94
+ font-size: 0.8rem;
95
+ font-weight: 600;
96
+ text-transform: uppercase;
97
+ letter-spacing: 0.06em;
98
+ color: var(--muted);
99
+ border-bottom: 1px solid var(--border);
100
+ display: flex;
101
+ justify-content: space-between;
102
+ align-items: center;
103
+ }
104
+ .column-header .count {
105
+ font-family: "JetBrains Mono", monospace;
106
+ font-size: 0.7rem;
107
+ background: var(--bg);
108
+ padding: 0.15rem 0.45rem;
109
+ border-radius: 4px;
110
+ }
111
+ .column-body {
112
+ padding: 0.65rem;
113
+ overflow-y: auto;
114
+ flex: 1;
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: 0.5rem;
118
+ }
119
+ .column-body.drag-over {
120
+ outline: 2px dashed var(--accent);
121
+ outline-offset: -4px;
122
+ border-radius: 6px;
123
+ }
124
+ .card {
125
+ background: var(--card);
126
+ border: 1px solid var(--border);
127
+ border-radius: 8px;
128
+ padding: 0.65rem 0.75rem;
129
+ cursor: grab;
130
+ transition: border-color 0.15s, transform 0.1s;
131
+ }
132
+ .card:active { cursor: grabbing; }
133
+ .card:hover { border-color: var(--accent-dim); }
134
+ .card.dragging { opacity: 0.45; }
135
+ .card-head {
136
+ display: flex;
137
+ align-items: flex-start;
138
+ justify-content: space-between;
139
+ gap: 0.5rem;
140
+ margin-bottom: 0.45rem;
141
+ }
142
+ .card-title {
143
+ font-size: 0.9rem;
144
+ font-weight: 600;
145
+ line-height: 1.35;
146
+ margin: 0;
147
+ flex: 1;
148
+ min-width: 0;
149
+ }
150
+ button.card-open {
151
+ font-family: inherit;
152
+ font-size: 0.7rem;
153
+ padding: 0.25rem 0.5rem;
154
+ border-radius: 4px;
155
+ border: 1px solid var(--border);
156
+ background: var(--bg);
157
+ color: var(--accent);
158
+ cursor: pointer;
159
+ flex-shrink: 0;
160
+ }
161
+ button.card-open:hover { border-color: var(--accent-dim); }
162
+ .badges {
163
+ display: flex;
164
+ flex-wrap: wrap;
165
+ gap: 0.35rem;
166
+ }
167
+ .badge {
168
+ font-family: "JetBrains Mono", monospace;
169
+ font-size: 0.65rem;
170
+ padding: 0.2rem 0.4rem;
171
+ border-radius: 4px;
172
+ background: var(--bg);
173
+ color: var(--muted);
174
+ }
175
+ .badge.priority-high, .badge.priority-urgent { color: #ffb088; }
176
+ .badge.priority-medium { color: #c9b87c; }
177
+ #loading, #empty {
178
+ padding: 2rem 1.5rem;
179
+ text-align: center;
180
+ color: var(--muted);
181
+ }
182
+ .overlay {
183
+ position: fixed;
184
+ inset: 0;
185
+ background: rgba(0, 0, 0, 0.55);
186
+ z-index: 100;
187
+ display: flex;
188
+ align-items: flex-start;
189
+ justify-content: center;
190
+ padding: 1.5rem;
191
+ overflow-y: auto;
192
+ }
193
+ .overlay[hidden] { display: none !important; }
194
+ .panel {
195
+ width: min(560px, 100%);
196
+ max-height: calc(100vh - 3rem);
197
+ margin-top: 0.5rem;
198
+ background: var(--surface);
199
+ border: 1px solid var(--border);
200
+ border-radius: 12px;
201
+ display: flex;
202
+ flex-direction: column;
203
+ box-shadow: 0 24px 48px rgba(0, 0, 0, 0.35);
204
+ }
205
+ .panel-header {
206
+ padding: 1rem 1.25rem;
207
+ border-bottom: 1px solid var(--border);
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: space-between;
211
+ gap: 0.75rem;
212
+ }
213
+ .panel-header h2 {
214
+ margin: 0;
215
+ font-size: 1rem;
216
+ font-weight: 700;
217
+ }
218
+ .panel-body {
219
+ padding: 1rem 1.25rem;
220
+ overflow-y: auto;
221
+ flex: 1;
222
+ }
223
+ .panel-footer {
224
+ padding: 0.85rem 1.25rem;
225
+ border-top: 1px solid var(--border);
226
+ display: flex;
227
+ flex-wrap: wrap;
228
+ gap: 0.5rem;
229
+ justify-content: flex-end;
230
+ }
231
+ .panel-footer button {
232
+ font-family: inherit;
233
+ font-size: 0.85rem;
234
+ padding: 0.45rem 0.9rem;
235
+ border-radius: 6px;
236
+ border: 1px solid var(--border);
237
+ background: var(--card);
238
+ color: var(--text);
239
+ cursor: pointer;
240
+ }
241
+ .panel-footer button.primary {
242
+ background: var(--accent-dim);
243
+ border-color: var(--accent);
244
+ color: #fff;
245
+ }
246
+ .panel-footer button.primary:hover { filter: brightness(1.08); }
247
+ .panel-footer button:disabled { opacity: 0.5; cursor: not-allowed; }
248
+ .view-title {
249
+ font-size: 1.25rem;
250
+ font-weight: 700;
251
+ margin: 0 0 0.75rem;
252
+ line-height: 1.3;
253
+ }
254
+ .view-meta {
255
+ font-family: "JetBrains Mono", monospace;
256
+ font-size: 0.7rem;
257
+ color: var(--muted);
258
+ margin-bottom: 1rem;
259
+ }
260
+ .view-content {
261
+ white-space: pre-wrap;
262
+ word-break: break-word;
263
+ font-size: 0.9rem;
264
+ line-height: 1.5;
265
+ color: var(--text);
266
+ margin: 0 0 1rem;
267
+ padding: 0.75rem;
268
+ background: var(--bg);
269
+ border-radius: 8px;
270
+ border: 1px solid var(--border);
271
+ min-height: 3rem;
272
+ }
273
+ .view-content.empty { color: var(--muted); font-style: italic; }
274
+ dl.field-list { margin: 0; }
275
+ dl.field-list dt {
276
+ font-size: 0.7rem;
277
+ text-transform: uppercase;
278
+ letter-spacing: 0.05em;
279
+ color: var(--muted);
280
+ margin-top: 0.65rem;
281
+ }
282
+ dl.field-list dt:first-child { margin-top: 0; }
283
+ dl.field-list dd { margin: 0.2rem 0 0; font-size: 0.9rem; }
284
+ .form-group { margin-bottom: 1rem; }
285
+ .form-group label {
286
+ display: block;
287
+ font-size: 0.75rem;
288
+ font-weight: 600;
289
+ text-transform: uppercase;
290
+ letter-spacing: 0.04em;
291
+ color: var(--muted);
292
+ margin-bottom: 0.35rem;
293
+ }
294
+ .form-group input[type="text"],
295
+ .form-group input[type="url"],
296
+ .form-group input[type="number"],
297
+ .form-group input[type="date"],
298
+ .form-group input[type="datetime-local"],
299
+ .form-group select,
300
+ .form-group textarea {
301
+ width: 100%;
302
+ font-family: inherit;
303
+ font-size: 0.9rem;
304
+ padding: 0.5rem 0.65rem;
305
+ border-radius: 6px;
306
+ border: 1px solid var(--border);
307
+ background: var(--bg);
308
+ color: var(--text);
309
+ }
310
+ .form-group textarea { min-height: 140px; resize: vertical; }
311
+ .form-group .checkbox-row {
312
+ display: flex;
313
+ align-items: center;
314
+ gap: 0.5rem;
315
+ }
316
+ .form-group .checkbox-row input { width: auto; }
317
+ .multi-options {
318
+ display: flex;
319
+ flex-direction: column;
320
+ gap: 0.35rem;
321
+ }
322
+ .multi-options label {
323
+ text-transform: none;
324
+ font-weight: 400;
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 0.45rem;
328
+ color: var(--text);
329
+ }
330
+ #panel-error {
331
+ font-size: 0.85rem;
332
+ color: var(--danger);
333
+ margin-bottom: 0.75rem;
334
+ }
335
+ #panel-error[hidden] { display: none; }
336
+ </style>
337
+ </head>
338
+ <body>
339
+ <header>
340
+ <h1><span>yarlo</span> board</h1>
341
+ <div class="toolbar">
342
+ <span id="status"></span>
343
+ <button type="button" id="refresh">Refresh</button>
344
+ </div>
345
+ </header>
346
+ <div id="loading">Loading tasks…</div>
347
+ <div id="board" class="board" hidden></div>
348
+ <div id="empty" hidden>No tasks yet. Use <code>yarlo add</code> to create one.</div>
349
+
350
+ <div id="overlay" class="overlay" hidden>
351
+ <div class="panel" role="dialog" aria-modal="true" aria-labelledby="panel-heading">
352
+ <div class="panel-header">
353
+ <h2 id="panel-heading">Task</h2>
354
+ <button type="button" id="panel-close" aria-label="Close">✕</button>
355
+ </div>
356
+ <div class="panel-body">
357
+ <div id="panel-error" hidden></div>
358
+ <div id="panel-content"></div>
359
+ </div>
360
+ <div class="panel-footer" id="panel-footer-view">
361
+ <button type="button" id="btn-edit">Edit</button>
362
+ <button type="button" id="btn-close-footer">Close</button>
363
+ </div>
364
+ <div class="panel-footer" id="panel-footer-edit" hidden>
365
+ <button type="button" id="btn-cancel">Cancel</button>
366
+ <button type="button" class="primary" id="btn-save">Save</button>
367
+ </div>
368
+ </div>
369
+ </div>
370
+
371
+ <script>
372
+ (function () {
373
+ const ORPHAN_KEY = "__orphan__";
374
+ const loadingEl = document.getElementById("loading");
375
+ const boardEl = document.getElementById("board");
376
+ const emptyEl = document.getElementById("empty");
377
+ const statusEl = document.getElementById("status");
378
+ const refreshBtn = document.getElementById("refresh");
379
+ const overlayEl = document.getElementById("overlay");
380
+ const panelContentEl = document.getElementById("panel-content");
381
+ const panelErrorEl = document.getElementById("panel-error");
382
+ const panelHeadingEl = document.getElementById("panel-heading");
383
+ const panelFooterView = document.getElementById("panel-footer-view");
384
+ const panelFooterEdit = document.getElementById("panel-footer-edit");
385
+ const btnEdit = document.getElementById("btn-edit");
386
+ const btnCloseFooter = document.getElementById("btn-close-footer");
387
+ const btnCloseX = document.getElementById("panel-close");
388
+ const btnCancel = document.getElementById("btn-cancel");
389
+ const btnSave = document.getElementById("btn-save");
390
+
391
+ let columns = [];
392
+ let tasks = [];
393
+ let fieldDefinitions = [];
394
+ let panelMode = "view";
395
+ let panelDetail = null;
396
+ let panelLoading = false;
397
+
398
+ function setStatus(msg, isError) {
399
+ statusEl.textContent = msg || "";
400
+ statusEl.classList.toggle("error", !!isError);
401
+ }
402
+
403
+ function setPanelError(msg) {
404
+ if (!msg) {
405
+ panelErrorEl.hidden = true;
406
+ panelErrorEl.textContent = "";
407
+ return;
408
+ }
409
+ panelErrorEl.hidden = false;
410
+ panelErrorEl.textContent = msg;
411
+ }
412
+
413
+ function summaryFromDetail(d) {
414
+ const f = d.fields || {};
415
+ return {
416
+ id: d.id,
417
+ title: d.title,
418
+ status: f.status != null ? f.status : null,
419
+ priority: f.priority != null ? f.priority : null,
420
+ labels: f.labels != null ? f.labels : null,
421
+ assignee: f.assignee != null ? f.assignee : null,
422
+ due_date: f.due_date != null ? f.due_date : null,
423
+ estimate: f.estimate != null ? f.estimate : null,
424
+ updated_at: d.updated_at,
425
+ };
426
+ }
427
+
428
+ function mergeTaskIntoBoard(detail) {
429
+ const s = summaryFromDetail(detail);
430
+ const idx = tasks.findIndex(function (t) { return t.id === s.id; });
431
+ if (idx >= 0) tasks[idx] = s;
432
+ else tasks.push(s);
433
+ }
434
+
435
+ function syncTaskUrl(id) {
436
+ const u = new URL(window.location.href);
437
+ if (id) u.searchParams.set("task", id);
438
+ else u.searchParams.delete("task");
439
+ window.history.replaceState({}, "", u.pathname + u.search);
440
+ }
441
+
442
+ function priorityClass(p) {
443
+ if (!p || typeof p !== "string") return "";
444
+ return "priority-" + p.replace(/_/g, "-");
445
+ }
446
+
447
+ function formatColumnTitle(key) {
448
+ if (key === ORPHAN_KEY) return "Other";
449
+ return String(key).replace(/_/g, " ");
450
+ }
451
+
452
+ function formatFieldValueForView(def, value) {
453
+ if (value === undefined || value === null) return "—";
454
+ if (def.type === "boolean") return value ? "Yes" : "No";
455
+ if (def.type === "multi_select" && Array.isArray(value)) {
456
+ return value.length ? value.join(", ") : "—";
457
+ }
458
+ return String(value);
459
+ }
460
+
461
+ function isoToDateInput(iso) {
462
+ if (typeof iso !== "string" || !iso) return "";
463
+ const d = new Date(iso);
464
+ if (isNaN(d.getTime())) return "";
465
+ return d.toISOString().slice(0, 10);
466
+ }
467
+
468
+ function isoToDatetimeLocal(iso) {
469
+ if (typeof iso !== "string" || !iso) return "";
470
+ const d = new Date(iso);
471
+ if (isNaN(d.getTime())) return "";
472
+ const pad = function (n) { return String(n).padStart(2, "0"); };
473
+ return (
474
+ d.getFullYear() +
475
+ "-" +
476
+ pad(d.getMonth() + 1) +
477
+ "-" +
478
+ pad(d.getDate()) +
479
+ "T" +
480
+ pad(d.getHours()) +
481
+ ":" +
482
+ pad(d.getMinutes())
483
+ );
484
+ }
485
+
486
+ function bucketTasks(cols, taskList) {
487
+ const colSet = new Set(cols);
488
+ const buckets = new Map();
489
+ cols.forEach(function (c) { buckets.set(c, []); });
490
+ const orphans = [];
491
+ taskList.forEach(function (t) {
492
+ const s = t.status;
493
+ if (typeof s === "string" && colSet.has(s)) {
494
+ buckets.get(s).push(t);
495
+ } else {
496
+ orphans.push(t);
497
+ }
498
+ });
499
+ return { buckets, orphans };
500
+ }
501
+
502
+ function renderCard(task) {
503
+ const card = document.createElement("article");
504
+ card.className = "card";
505
+ card.draggable = true;
506
+ card.dataset.taskId = task.id;
507
+ card.dataset.status = typeof task.status === "string" ? task.status : "";
508
+
509
+ const head = document.createElement("div");
510
+ head.className = "card-head";
511
+
512
+ const title = document.createElement("h2");
513
+ title.className = "card-title";
514
+ title.textContent = task.title || "(no title)";
515
+
516
+ const openBtn = document.createElement("button");
517
+ openBtn.type = "button";
518
+ openBtn.className = "card-open";
519
+ openBtn.textContent = "Open";
520
+ openBtn.draggable = false;
521
+ openBtn.addEventListener("click", function (e) {
522
+ e.stopPropagation();
523
+ e.preventDefault();
524
+ openTaskPanel(task.id);
525
+ });
526
+ openBtn.addEventListener("mousedown", function (e) {
527
+ e.stopPropagation();
528
+ });
529
+
530
+ head.appendChild(title);
531
+ head.appendChild(openBtn);
532
+
533
+ const badges = document.createElement("div");
534
+ badges.className = "badges";
535
+ if (task.priority) {
536
+ const b = document.createElement("span");
537
+ b.className = "badge " + priorityClass(task.priority);
538
+ b.textContent = String(task.priority);
539
+ badges.appendChild(b);
540
+ }
541
+ if (Array.isArray(task.labels) && task.labels.length) {
542
+ task.labels.forEach(function (lb) {
543
+ const b = document.createElement("span");
544
+ b.className = "badge";
545
+ b.textContent = String(lb);
546
+ badges.appendChild(b);
547
+ });
548
+ }
549
+ if (task.assignee) {
550
+ const b = document.createElement("span");
551
+ b.className = "badge";
552
+ b.textContent = "@" + String(task.assignee);
553
+ badges.appendChild(b);
554
+ }
555
+
556
+ card.appendChild(head);
557
+ if (badges.children.length) card.appendChild(badges);
558
+
559
+ card.addEventListener("dragstart", function (e) {
560
+ card.classList.add("dragging");
561
+ e.dataTransfer.setData("text/plain", task.id);
562
+ e.dataTransfer.effectAllowed = "move";
563
+ });
564
+ card.addEventListener("dragend", function () {
565
+ card.classList.remove("dragging");
566
+ });
567
+
568
+ return card;
569
+ }
570
+
571
+ function renderColumn(key, taskList) {
572
+ const col = document.createElement("section");
573
+ col.className = "column" + (key === ORPHAN_KEY ? " orphan" : "");
574
+ col.dataset.columnKey = key;
575
+
576
+ const head = document.createElement("div");
577
+ head.className = "column-header";
578
+ const label = document.createElement("span");
579
+ label.textContent = formatColumnTitle(key);
580
+ const count = document.createElement("span");
581
+ count.className = "count";
582
+ count.textContent = String(taskList.length);
583
+ head.appendChild(label);
584
+ head.appendChild(count);
585
+
586
+ const body = document.createElement("div");
587
+ body.className = "column-body";
588
+ taskList.forEach(function (t) {
589
+ body.appendChild(renderCard(t));
590
+ });
591
+
592
+ col.appendChild(head);
593
+ col.appendChild(body);
594
+
595
+ body.addEventListener("dragover", function (e) {
596
+ if (key === ORPHAN_KEY) return;
597
+ e.preventDefault();
598
+ e.dataTransfer.dropEffect = "move";
599
+ body.classList.add("drag-over");
600
+ });
601
+ body.addEventListener("dragleave", function () {
602
+ body.classList.remove("drag-over");
603
+ });
604
+ body.addEventListener("drop", function (e) {
605
+ body.classList.remove("drag-over");
606
+ if (key === ORPHAN_KEY) return;
607
+ e.preventDefault();
608
+ const id = e.dataTransfer.getData("text/plain");
609
+ if (!id) return;
610
+ moveTask(id, key);
611
+ });
612
+
613
+ return col;
614
+ }
615
+
616
+ function render() {
617
+ boardEl.innerHTML = "";
618
+ const { buckets, orphans } = bucketTasks(columns, tasks);
619
+
620
+ columns.forEach(function (colKey) {
621
+ boardEl.appendChild(renderColumn(colKey, buckets.get(colKey) || []));
622
+ });
623
+ if (orphans.length > 0) {
624
+ boardEl.appendChild(renderColumn(ORPHAN_KEY, orphans));
625
+ }
626
+
627
+ const total = tasks.length;
628
+ if (total === 0) {
629
+ boardEl.hidden = true;
630
+ emptyEl.hidden = false;
631
+ } else {
632
+ boardEl.hidden = false;
633
+ emptyEl.hidden = true;
634
+ }
635
+ }
636
+
637
+ async function moveTask(id, newStatus) {
638
+ setStatus("Saving…");
639
+ try {
640
+ const res = await fetch("/api/tasks/" + encodeURIComponent(id), {
641
+ method: "PATCH",
642
+ headers: { "Content-Type": "application/json" },
643
+ body: JSON.stringify({ status: newStatus }),
644
+ });
645
+ const data = await res.json().catch(function () { return {}; });
646
+ if (!res.ok) {
647
+ setStatus(data.error || res.statusText, true);
648
+ return;
649
+ }
650
+ const updated = data.task;
651
+ if (updated && updated.id) {
652
+ mergeTaskIntoBoard(updated);
653
+ }
654
+ setStatus("Saved");
655
+ render();
656
+ } catch (err) {
657
+ setStatus(err.message || "Request failed", true);
658
+ }
659
+ }
660
+
661
+ function renderPanelView() {
662
+ const d = panelDetail;
663
+ if (!d) return;
664
+ panelContentEl.innerHTML = "";
665
+ const titleEl = document.createElement("h3");
666
+ titleEl.className = "view-title";
667
+ titleEl.textContent = d.title || "(no title)";
668
+
669
+ const meta = document.createElement("div");
670
+ meta.className = "view-meta";
671
+ meta.textContent =
672
+ d.id +
673
+ " · updated " +
674
+ (d.updated_at || "—") +
675
+ " · created " +
676
+ (d.created_at || "—");
677
+
678
+ const contentEl = document.createElement("div");
679
+ const body = (d.content || "").trim();
680
+ contentEl.className = "view-content" + (body ? "" : " empty");
681
+ contentEl.textContent = body || "No description.";
682
+
683
+ panelContentEl.appendChild(titleEl);
684
+ panelContentEl.appendChild(meta);
685
+ panelContentEl.appendChild(contentEl);
686
+
687
+ const dl = document.createElement("dl");
688
+ dl.className = "field-list";
689
+ fieldDefinitions.forEach(function (def) {
690
+ const dt = document.createElement("dt");
691
+ dt.textContent = def.name.replace(/_/g, " ");
692
+ const dd = document.createElement("dd");
693
+ dd.textContent = formatFieldValueForView(def, d.fields[def.name]);
694
+ dl.appendChild(dt);
695
+ dl.appendChild(dd);
696
+ });
697
+ panelContentEl.appendChild(dl);
698
+ }
699
+
700
+ function renderFieldEditor(def, fields) {
701
+ const wrap = document.createElement("div");
702
+ wrap.className = "form-group";
703
+ const label = document.createElement("label");
704
+ label.setAttribute("for", "fld-" + def.name);
705
+ label.textContent = (def.description || def.name).replace(/_/g, " ");
706
+ wrap.appendChild(label);
707
+
708
+ const raw = fields[def.name];
709
+ const id = "fld-" + def.name;
710
+
711
+ if (def.type === "single_select") {
712
+ const sel = document.createElement("select");
713
+ sel.id = id;
714
+ sel.dataset.fieldName = def.name;
715
+ sel.dataset.fieldType = def.type;
716
+ const opts = def.options || [];
717
+ if (!def.required) {
718
+ const o = document.createElement("option");
719
+ o.value = "";
720
+ o.textContent = "—";
721
+ sel.appendChild(o);
722
+ }
723
+ opts.forEach(function (op) {
724
+ const o = document.createElement("option");
725
+ o.value = op;
726
+ o.textContent = op;
727
+ sel.appendChild(o);
728
+ });
729
+ sel.value = typeof raw === "string" ? raw : "";
730
+ wrap.appendChild(sel);
731
+ return wrap;
732
+ }
733
+
734
+ if (def.type === "multi_select") {
735
+ const box = document.createElement("div");
736
+ box.className = "multi-options";
737
+ box.dataset.fieldName = def.name;
738
+ box.dataset.fieldType = def.type;
739
+ const selected = Array.isArray(raw) ? raw.map(String) : [];
740
+ (def.options || []).forEach(function (op) {
741
+ const row = document.createElement("label");
742
+ const cb = document.createElement("input");
743
+ cb.type = "checkbox";
744
+ cb.value = op;
745
+ cb.checked = selected.indexOf(op) >= 0;
746
+ row.appendChild(cb);
747
+ row.appendChild(document.createTextNode(" " + op));
748
+ box.appendChild(row);
749
+ });
750
+ wrap.appendChild(box);
751
+ return wrap;
752
+ }
753
+
754
+ if (def.type === "boolean") {
755
+ const row = document.createElement("div");
756
+ row.className = "checkbox-row";
757
+ const cb = document.createElement("input");
758
+ cb.type = "checkbox";
759
+ cb.id = id;
760
+ cb.dataset.fieldName = def.name;
761
+ cb.dataset.fieldType = def.type;
762
+ cb.checked = !!raw;
763
+ row.appendChild(cb);
764
+ row.appendChild(document.createTextNode(" Enabled"));
765
+ wrap.appendChild(row);
766
+ return wrap;
767
+ }
768
+
769
+ if (def.type === "number") {
770
+ const inp = document.createElement("input");
771
+ inp.type = "number";
772
+ inp.id = id;
773
+ inp.step = "any";
774
+ inp.dataset.fieldName = def.name;
775
+ inp.dataset.fieldType = def.type;
776
+ inp.value = raw != null && raw !== "" ? String(raw) : "";
777
+ wrap.appendChild(inp);
778
+ return wrap;
779
+ }
780
+
781
+ if (def.type === "date") {
782
+ const inp = document.createElement("input");
783
+ inp.type = "datetime-local";
784
+ inp.id = id;
785
+ inp.dataset.fieldName = def.name;
786
+ inp.dataset.fieldType = def.type;
787
+ inp.value = isoToDatetimeLocal(typeof raw === "string" ? raw : "");
788
+ wrap.appendChild(inp);
789
+ return wrap;
790
+ }
791
+
792
+ const inp = document.createElement("input");
793
+ inp.type = def.type === "url" ? "url" : "text";
794
+ inp.id = id;
795
+ inp.dataset.fieldName = def.name;
796
+ inp.dataset.fieldType = def.type;
797
+ inp.value = raw != null && raw !== undefined ? String(raw) : "";
798
+ wrap.appendChild(inp);
799
+ return wrap;
800
+ }
801
+
802
+ function renderPanelEdit() {
803
+ const d = panelDetail;
804
+ if (!d) return;
805
+ panelContentEl.innerHTML = "";
806
+
807
+ const titleGroup = document.createElement("div");
808
+ titleGroup.className = "form-group";
809
+ const tl = document.createElement("label");
810
+ tl.setAttribute("for", "edit-title");
811
+ tl.textContent = "Title";
812
+ const titleInp = document.createElement("input");
813
+ titleInp.type = "text";
814
+ titleInp.id = "edit-title";
815
+ titleInp.value = d.title || "";
816
+ titleGroup.appendChild(tl);
817
+ titleGroup.appendChild(titleInp);
818
+ panelContentEl.appendChild(titleGroup);
819
+
820
+ const contentGroup = document.createElement("div");
821
+ contentGroup.className = "form-group";
822
+ const cl = document.createElement("label");
823
+ cl.setAttribute("for", "edit-content");
824
+ cl.textContent = "Content (markdown)";
825
+ const ta = document.createElement("textarea");
826
+ ta.id = "edit-content";
827
+ ta.value = d.content || "";
828
+ contentGroup.appendChild(cl);
829
+ contentGroup.appendChild(ta);
830
+ panelContentEl.appendChild(contentGroup);
831
+
832
+ const f = { ...d.fields };
833
+ fieldDefinitions.forEach(function (def) {
834
+ panelContentEl.appendChild(renderFieldEditor(def, f));
835
+ });
836
+ }
837
+
838
+ function collectEditPayload() {
839
+ const titleInp = document.getElementById("edit-title");
840
+ const contentTa = document.getElementById("edit-content");
841
+ const payload = {
842
+ title: titleInp ? titleInp.value : "",
843
+ content: contentTa ? contentTa.value : "",
844
+ fields: {},
845
+ };
846
+
847
+ fieldDefinitions.forEach(function (def) {
848
+ if (def.type === "single_select") {
849
+ const sel = panelContentEl.querySelector(
850
+ 'select[data-field-name="' + def.name + '"]',
851
+ );
852
+ if (!sel) return;
853
+ const v = sel.value;
854
+ if (v === "" && !def.required) {
855
+ return;
856
+ }
857
+ payload.fields[def.name] = v;
858
+ return;
859
+ }
860
+ if (def.type === "multi_select") {
861
+ const box = panelContentEl.querySelector(
862
+ 'div[data-field-name="' + def.name + '"][data-field-type="multi_select"]',
863
+ );
864
+ if (!box) return;
865
+ const checked = [];
866
+ box.querySelectorAll('input[type="checkbox"]').forEach(function (cb) {
867
+ if (cb.checked) checked.push(cb.value);
868
+ });
869
+ payload.fields[def.name] = checked;
870
+ return;
871
+ }
872
+ if (def.type === "boolean") {
873
+ const cb = panelContentEl.querySelector(
874
+ 'input[data-field-name="' + def.name + '"][data-field-type="boolean"]',
875
+ );
876
+ if (cb) payload.fields[def.name] = !!cb.checked;
877
+ return;
878
+ }
879
+ if (def.type === "number") {
880
+ const inp = panelContentEl.querySelector(
881
+ 'input[data-field-name="' + def.name + '"][data-field-type="number"]',
882
+ );
883
+ if (!inp || inp.value === "") return;
884
+ const n = Number.parseFloat(inp.value);
885
+ if (!Number.isNaN(n)) payload.fields[def.name] = n;
886
+ return;
887
+ }
888
+ if (def.type === "date") {
889
+ const inp = panelContentEl.querySelector(
890
+ 'input[data-field-name="' + def.name + '"][data-field-type="date"]',
891
+ );
892
+ if (!inp || !inp.value) return;
893
+ const iso = new Date(inp.value);
894
+ if (!isNaN(iso.getTime())) {
895
+ payload.fields[def.name] = iso.toISOString();
896
+ }
897
+ return;
898
+ }
899
+ const inp = panelContentEl.querySelector(
900
+ 'input[data-field-name="' + def.name + '"]',
901
+ );
902
+ if (inp) payload.fields[def.name] = inp.value;
903
+ });
904
+
905
+ return payload;
906
+ }
907
+
908
+ function setPanelMode(mode) {
909
+ panelMode = mode;
910
+ if (mode === "view") {
911
+ panelFooterView.hidden = false;
912
+ panelFooterEdit.hidden = true;
913
+ renderPanelView();
914
+ } else {
915
+ panelFooterView.hidden = true;
916
+ panelFooterEdit.hidden = false;
917
+ renderPanelEdit();
918
+ }
919
+ }
920
+
921
+ function closeTaskPanel() {
922
+ overlayEl.hidden = true;
923
+ panelDetail = null;
924
+ setPanelError("");
925
+ syncTaskUrl(null);
926
+ }
927
+
928
+ async function openTaskPanel(id) {
929
+ setPanelError("");
930
+ overlayEl.hidden = false;
931
+ panelHeadingEl.textContent = "Task";
932
+ panelContentEl.innerHTML =
933
+ '<p class="view-meta">Loading…</p>';
934
+ panelFooterView.hidden = false;
935
+ panelFooterEdit.hidden = true;
936
+ btnEdit.disabled = true;
937
+ panelLoading = true;
938
+ syncTaskUrl(id);
939
+
940
+ try {
941
+ const res = await fetch("/api/tasks/" + encodeURIComponent(id));
942
+ const data = await res.json().catch(function () { return {}; });
943
+ if (!res.ok) {
944
+ panelContentEl.innerHTML = "";
945
+ setPanelError(data.error || res.statusText);
946
+ btnEdit.disabled = true;
947
+ panelLoading = false;
948
+ return;
949
+ }
950
+ panelDetail = data.task;
951
+ panelHeadingEl.textContent = panelDetail.title || panelDetail.id;
952
+ btnEdit.disabled = false;
953
+ setPanelMode("view");
954
+ } catch (err) {
955
+ panelContentEl.innerHTML = "";
956
+ setPanelError(err.message || "Request failed");
957
+ btnEdit.disabled = true;
958
+ }
959
+ panelLoading = false;
960
+ }
961
+
962
+ async function saveTask() {
963
+ if (!panelDetail) return;
964
+ setPanelError("");
965
+ const p = collectEditPayload();
966
+ btnSave.disabled = true;
967
+ try {
968
+ const res = await fetch(
969
+ "/api/tasks/" + encodeURIComponent(panelDetail.id),
970
+ {
971
+ method: "PATCH",
972
+ headers: { "Content-Type": "application/json" },
973
+ body: JSON.stringify({
974
+ title: p.title,
975
+ content: p.content,
976
+ fields: p.fields,
977
+ }),
978
+ },
979
+ );
980
+ const data = await res.json().catch(function () { return {}; });
981
+ if (!res.ok) {
982
+ setPanelError(data.error || res.statusText);
983
+ btnSave.disabled = false;
984
+ return;
985
+ }
986
+ panelDetail = data.task;
987
+ panelHeadingEl.textContent = panelDetail.title || panelDetail.id;
988
+ mergeTaskIntoBoard(panelDetail);
989
+ render();
990
+ setPanelMode("view");
991
+ } catch (err) {
992
+ setPanelError(err.message || "Request failed");
993
+ }
994
+ btnSave.disabled = false;
995
+ }
996
+
997
+ btnEdit.addEventListener("click", function () {
998
+ if (panelLoading || !panelDetail) return;
999
+ setPanelMode("edit");
1000
+ });
1001
+
1002
+ btnCancel.addEventListener("click", function () {
1003
+ setPanelMode("view");
1004
+ setPanelError("");
1005
+ });
1006
+
1007
+ btnSave.addEventListener("click", saveTask);
1008
+
1009
+ function closePanelClick(e) {
1010
+ if (e.target === overlayEl) closeTaskPanel();
1011
+ }
1012
+ overlayEl.addEventListener("click", closePanelClick);
1013
+ btnCloseX.addEventListener("click", closeTaskPanel);
1014
+ btnCloseFooter.addEventListener("click", closeTaskPanel);
1015
+
1016
+ document.addEventListener("keydown", function (e) {
1017
+ if (e.key === "Escape" && !overlayEl.hidden) {
1018
+ closeTaskPanel();
1019
+ }
1020
+ });
1021
+
1022
+ const panelInner = overlayEl.querySelector(".panel");
1023
+ if (panelInner) {
1024
+ panelInner.addEventListener("click", function (e) {
1025
+ e.stopPropagation();
1026
+ });
1027
+ }
1028
+
1029
+ async function load() {
1030
+ setStatus("");
1031
+ loadingEl.hidden = false;
1032
+ boardEl.hidden = true;
1033
+ emptyEl.hidden = true;
1034
+ try {
1035
+ const res = await fetch("/api/board");
1036
+ const data = await res.json();
1037
+ if (!res.ok) throw new Error(data.error || res.statusText);
1038
+ columns = data.columns || [];
1039
+ tasks = data.tasks || [];
1040
+ fieldDefinitions = data.fieldDefinitions || [];
1041
+ loadingEl.hidden = true;
1042
+ render();
1043
+ setStatus(columns.length + " columns · " + tasks.length + " tasks");
1044
+
1045
+ const qTask = new URLSearchParams(window.location.search).get("task");
1046
+ if (qTask) {
1047
+ openTaskPanel(qTask);
1048
+ }
1049
+ } catch (err) {
1050
+ loadingEl.hidden = true;
1051
+ setStatus(err.message || "Failed to load", true);
1052
+ }
1053
+ }
1054
+
1055
+ refreshBtn.addEventListener("click", load);
1056
+ load();
1057
+ })();
1058
+ </script>
1059
+ </body>
1060
+ </html>