yarlo-plugin-board 0.2.0 → 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.
@@ -132,12 +132,33 @@
132
132
  .card:active { cursor: grabbing; }
133
133
  .card:hover { border-color: var(--accent-dim); }
134
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
+ }
135
142
  .card-title {
136
143
  font-size: 0.9rem;
137
144
  font-weight: 600;
138
145
  line-height: 1.35;
139
- margin: 0 0 0.45rem;
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;
140
160
  }
161
+ button.card-open:hover { border-color: var(--accent-dim); }
141
162
  .badges {
142
163
  display: flex;
143
164
  flex-wrap: wrap;
@@ -158,6 +179,160 @@
158
179
  text-align: center;
159
180
  color: var(--muted);
160
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; }
161
336
  </style>
162
337
  </head>
163
338
  <body>
@@ -172,6 +347,27 @@
172
347
  <div id="board" class="board" hidden></div>
173
348
  <div id="empty" hidden>No tasks yet. Use <code>yarlo add</code> to create one.</div>
174
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
+
175
371
  <script>
176
372
  (function () {
177
373
  const ORPHAN_KEY = "__orphan__";
@@ -180,15 +376,69 @@
180
376
  const emptyEl = document.getElementById("empty");
181
377
  const statusEl = document.getElementById("status");
182
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");
183
390
 
184
391
  let columns = [];
185
392
  let tasks = [];
393
+ let fieldDefinitions = [];
394
+ let panelMode = "view";
395
+ let panelDetail = null;
396
+ let panelLoading = false;
186
397
 
187
398
  function setStatus(msg, isError) {
188
399
  statusEl.textContent = msg || "";
189
400
  statusEl.classList.toggle("error", !!isError);
190
401
  }
191
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
+
192
442
  function priorityClass(p) {
193
443
  if (!p || typeof p !== "string") return "";
194
444
  return "priority-" + p.replace(/_/g, "-");
@@ -199,6 +449,40 @@
199
449
  return String(key).replace(/_/g, " ");
200
450
  }
201
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
+
202
486
  function bucketTasks(cols, taskList) {
203
487
  const colSet = new Set(cols);
204
488
  const buckets = new Map();
@@ -222,10 +506,30 @@
222
506
  card.dataset.taskId = task.id;
223
507
  card.dataset.status = typeof task.status === "string" ? task.status : "";
224
508
 
509
+ const head = document.createElement("div");
510
+ head.className = "card-head";
511
+
225
512
  const title = document.createElement("h2");
226
513
  title.className = "card-title";
227
514
  title.textContent = task.title || "(no title)";
228
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
+
229
533
  const badges = document.createElement("div");
230
534
  badges.className = "badges";
231
535
  if (task.priority) {
@@ -249,7 +553,7 @@
249
553
  badges.appendChild(b);
250
554
  }
251
555
 
252
- card.appendChild(title);
556
+ card.appendChild(head);
253
557
  if (badges.children.length) card.appendChild(badges);
254
558
 
255
559
  card.addEventListener("dragstart", function (e) {
@@ -345,9 +649,7 @@
345
649
  }
346
650
  const updated = data.task;
347
651
  if (updated && updated.id) {
348
- tasks = tasks.map(function (t) {
349
- return t.id === updated.id ? updated : t;
350
- });
652
+ mergeTaskIntoBoard(updated);
351
653
  }
352
654
  setStatus("Saved");
353
655
  render();
@@ -356,6 +658,374 @@
356
658
  }
357
659
  }
358
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
+
359
1029
  async function load() {
360
1030
  setStatus("");
361
1031
  loadingEl.hidden = false;
@@ -367,9 +1037,15 @@
367
1037
  if (!res.ok) throw new Error(data.error || res.statusText);
368
1038
  columns = data.columns || [];
369
1039
  tasks = data.tasks || [];
1040
+ fieldDefinitions = data.fieldDefinitions || [];
370
1041
  loadingEl.hidden = true;
371
1042
  render();
372
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
+ }
373
1049
  } catch (err) {
374
1050
  loadingEl.hidden = true;
375
1051
  setStatus(err.message || "Failed to load", true);