@lchase/kiln 0.15.0 → 0.16.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/assets/serve.js CHANGED
@@ -59,7 +59,7 @@
59
59
  if (slimCount) slimCount.textContent = String(visible);
60
60
  var empty = col.querySelector(".empty");
61
61
  if (empty) empty.hidden = visible !== 0;
62
- tracks.push(slim ? "40px" : "minmax(0, 1fr)");
62
+ tracks.push(slim ? "var(--col-slim)" : "minmax(var(--col-min), 1fr)");
63
63
  }
64
64
  board.style.setProperty("--cols", tracks.join(" "));
65
65
  }
@@ -284,16 +284,204 @@
284
284
  var ticketsDir = "";
285
285
  var fsPath = "";
286
286
  var sending = false;
287
+ var intakeTouched = {};
288
+ var intakeSubmitTried = false;
287
289
  var FS_KEY = "kiln-fs-dir";
288
290
 
291
+ function pickLimit() {
292
+ var n = Number(body.getAttribute("data-pick-limit") || "5");
293
+ return n > 0 ? n : 5;
294
+ }
295
+
289
296
  function selectedRepo() {
290
- var pressed = document.querySelector("#repo-pick .pick[aria-pressed='true']");
291
- return pressed ? pressed.getAttribute("data-repo") || "" : "";
297
+ var box = document.getElementById("repo-pick");
298
+ return box ? box.getAttribute("data-value") || "" : "";
292
299
  }
293
300
 
294
301
  function intakeRepoPath() {
295
- var pressed = document.querySelector("#repo-pick .pick[aria-pressed='true']");
296
- return pressed ? pressed.getAttribute("data-path") || "" : "";
302
+ var box = document.getElementById("repo-pick");
303
+ return box ? box.getAttribute("data-path") || "" : "";
304
+ }
305
+
306
+ function selectedWorkflow() {
307
+ var select = document.getElementById("workflow");
308
+ if (select) return select.value || "";
309
+ var host = document.getElementById("workflow-pick");
310
+ return host ? host.getAttribute("data-workflow") || "" : "";
311
+ }
312
+
313
+ function setFieldError(id, message, reveal) {
314
+ var el = document.getElementById(id);
315
+ if (!el) return;
316
+ var on = Boolean(message) && reveal;
317
+ el.hidden = !on;
318
+ el.textContent = on ? message : "";
319
+ var field = el.closest(".field");
320
+ if (!field) return;
321
+ var control = field.querySelector("textarea, input, select, [role='combobox'], [role='group']");
322
+ if (!control) return;
323
+ if (on) control.setAttribute("aria-invalid", "true");
324
+ else control.removeAttribute("aria-invalid");
325
+ }
326
+
327
+ function showFieldError(name) {
328
+ return intakeSubmitTried || intakeTouched[name] === true;
329
+ }
330
+
331
+ function syncValidity() {
332
+ var repo = selectedRepo();
333
+ var workflow = selectedWorkflow();
334
+ var source = sourceBody();
335
+ var prompt = source.kind === "prompt" && !String(source.text || "").trim() ? "Prompt is required." : "";
336
+ var spec = source.kind === "spec" && !source.path ? "Choose a spec file." : "";
337
+ var tickets = source.kind === "tickets" && !source.dir ? "Choose a tickets folder." : "";
338
+ var jira = source.kind === "jira" && !source.key ? "Enter a Jira key." : "";
339
+ setFieldError("repo-error", repo ? "" : "Choose a repo.", showFieldError("repo"));
340
+ setFieldError("workflow-error", workflow ? "" : "Choose a workflow.", showFieldError("workflow"));
341
+ setFieldError("prompt-error", prompt, showFieldError("prompt"));
342
+ setFieldError("source-error", spec || tickets, showFieldError("source"));
343
+ setFieldError("jira-error", jira, showFieldError("jira"));
344
+ var blocked = !repo || !workflow || Boolean(prompt || spec || tickets || jira);
345
+ var submit = document.getElementById("add-submit");
346
+ if (submit) submit.disabled = blocked || sending;
347
+ var preview = document.getElementById("preview-job");
348
+ if (preview) preview.disabled = blocked || sending;
349
+ return !blocked;
350
+ }
351
+
352
+ function repoListKey(repos) {
353
+ var parts = [];
354
+ for (var i = 0; i < repos.length; i++) {
355
+ var repo = repos[i];
356
+ if (!repo || !repo.name) continue;
357
+ parts.push(repo.name + "\t" + (repo.path || ""));
358
+ }
359
+ return parts.join("\n");
360
+ }
361
+
362
+ function paintRepos(repos) {
363
+ var box = document.getElementById("repo-pick");
364
+ if (!box) return;
365
+ var items = [];
366
+ for (var i = 0; i < repos.length; i++) if (repos[i] && repos[i].name) items.push(repos[i]);
367
+ var key = repoListKey(items);
368
+ if (box.getAttribute("data-list") === key) return;
369
+ var current = box.getAttribute("data-value") || "";
370
+ var selected = null;
371
+ for (var s = 0; s < items.length; s++) if (items[s].name === current) selected = items[s];
372
+ if (!selected && items.length) selected = items[0];
373
+ var value = selected ? selected.name : "";
374
+ var path = selected ? (selected.path || "") : "";
375
+ var mode = !items.length ? "empty" : items.length > pickLimit() ? "select" : "chips";
376
+ var html = "";
377
+ if (mode === "empty") html = '<p class="empty">No repos are registered.</p>';
378
+ if (mode === "chips") {
379
+ for (var c = 0; c < items.length; c++) {
380
+ var chip = items[c];
381
+ var pressed = chip.name === value;
382
+ html += '<button type="button" class="pick" data-repo="' + esc(chip.name) + '" data-path="' + esc(chip.path || "") + '" aria-pressed="' + (pressed ? "true" : "false") + '">' + esc(chip.name) + "</button>";
383
+ }
384
+ }
385
+ if (mode === "select") {
386
+ html = '<input id="repo-pick-query" class="combo-input" type="text" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="repo-pick-list" aria-labelledby="repo-label" placeholder="Search repos" value="' + esc(value) + '" autocomplete="off" spellcheck="false">';
387
+ html += '<ul class="combo-list" id="repo-pick-list" role="listbox" hidden>';
388
+ for (var o = 0; o < items.length; o++) {
389
+ var opt = items[o];
390
+ html += '<li role="option" id="repo-opt-' + o + '" data-value="' + esc(opt.name) + '" data-path="' + esc(opt.path || "") + '" aria-selected="' + (opt.name === value ? "true" : "false") + '">' + esc(opt.name) + "</li>";
391
+ }
392
+ html += '<li class="empty" role="presentation" hidden>No matching repos.</li></ul>';
393
+ }
394
+ box.className = mode === "select" ? "combo" : "picks";
395
+ box.setAttribute("data-mode", mode);
396
+ box.setAttribute("data-value", value);
397
+ box.setAttribute("data-path", path);
398
+ box.setAttribute("data-list", key);
399
+ if (mode === "select") {
400
+ box.removeAttribute("role");
401
+ box.removeAttribute("aria-labelledby");
402
+ } else {
403
+ box.setAttribute("role", "group");
404
+ box.setAttribute("aria-labelledby", "repo-label");
405
+ }
406
+ box.innerHTML = html;
407
+ syncValidity();
408
+ }
409
+
410
+ function visibleOptions(box) {
411
+ var all = box.querySelectorAll("[role='option']");
412
+ var out = [];
413
+ for (var i = 0; i < all.length; i++) if (!all[i].hidden) out.push(all[i]);
414
+ return out;
415
+ }
416
+
417
+ function applyComboFilter(box, query) {
418
+ var q = String(query || "").trim().toLowerCase();
419
+ var options = box.querySelectorAll("[role='option']");
420
+ var shown = 0;
421
+ for (var i = 0; i < options.length; i++) {
422
+ var name = (options[i].getAttribute("data-value") || options[i].textContent || "").toLowerCase();
423
+ var hide = q.length > 0 && name.indexOf(q) === -1;
424
+ options[i].hidden = hide;
425
+ if (!hide) shown += 1;
426
+ }
427
+ var empty = box.querySelector(".combo-list .empty");
428
+ if (empty) empty.hidden = shown !== 0;
429
+ setComboActive(box, 0);
430
+ }
431
+
432
+ function setComboActive(box, index) {
433
+ var options = visibleOptions(box);
434
+ var input = box.querySelector("[role='combobox']");
435
+ if (!options.length) {
436
+ box.removeAttribute("data-active");
437
+ if (input) input.removeAttribute("aria-activedescendant");
438
+ return;
439
+ }
440
+ if (index < 0) index = options.length - 1;
441
+ if (index >= options.length) index = 0;
442
+ box.setAttribute("data-active", String(index));
443
+ for (var i = 0; i < options.length; i++) options[i].classList.toggle("active", i === index);
444
+ if (input && options[index].id) input.setAttribute("aria-activedescendant", options[index].id);
445
+ }
446
+
447
+ function openCombo(box, filter) {
448
+ if (!box) return;
449
+ var list = box.querySelector("[role='listbox']");
450
+ var input = box.querySelector("[role='combobox']");
451
+ if (!list || !input) return;
452
+ list.hidden = false;
453
+ input.setAttribute("aria-expanded", "true");
454
+ applyComboFilter(box, filter ? input.value : "");
455
+ }
456
+
457
+ function closeCombo(box) {
458
+ var list = box.querySelector("[role='listbox']");
459
+ var input = box.querySelector("[role='combobox']");
460
+ if (list) list.hidden = true;
461
+ if (input) {
462
+ input.setAttribute("aria-expanded", "false");
463
+ input.removeAttribute("aria-activedescendant");
464
+ input.value = box.getAttribute("data-value") || "";
465
+ }
466
+ box.removeAttribute("data-active");
467
+ }
468
+
469
+ function closeCombos(except) {
470
+ var boxes = document.querySelectorAll(".combo");
471
+ for (var i = 0; i < boxes.length; i++) if (boxes[i] !== except) closeCombo(boxes[i]);
472
+ }
473
+
474
+ function chooseComboOption(box, option) {
475
+ if (!box || !option) return;
476
+ var value = option.getAttribute("data-value") || "";
477
+ box.setAttribute("data-value", value);
478
+ if (option.hasAttribute("data-path")) box.setAttribute("data-path", option.getAttribute("data-path") || "");
479
+ var options = box.querySelectorAll("[role='option']");
480
+ for (var i = 0; i < options.length; i++) {
481
+ options[i].setAttribute("aria-selected", options[i] === option ? "true" : "false");
482
+ }
483
+ closeCombo(box);
484
+ syncValidity();
297
485
  }
298
486
 
299
487
  function showIntakeError(message) {
@@ -346,6 +534,7 @@
346
534
  if (sourceKind === "tickets") html += '<button type="button" class="fs-use" data-path="' + esc(fsPath) + '">Use this folder</button>';
347
535
  host.innerHTML = html;
348
536
  if (sourceKind === "tickets") setPicked(fsPath);
537
+ syncValidity();
349
538
  }
350
539
 
351
540
  function chooseSpec(data) {
@@ -356,6 +545,7 @@
356
545
  var input = document.getElementById("job-title");
357
546
  if (input && heading) input.value = heading;
358
547
  }
548
+ syncValidity();
359
549
  }
360
550
 
361
551
  function loadFs(path, fallback) {
@@ -417,6 +607,7 @@
417
607
  }
418
608
  syncIssueDefault();
419
609
  if (sourceKind === "spec" || sourceKind === "tickets") openBrowser();
610
+ syncValidity();
420
611
  }
421
612
 
422
613
  function syncIssueDefault() {
@@ -451,15 +642,12 @@
451
642
 
452
643
  function submitIntake(dryRun) {
453
644
  if (sending) return;
645
+ intakeSubmitTried = true;
646
+ if (!syncValidity()) return;
454
647
  var repo = selectedRepo();
455
- var workflow = document.getElementById("workflow");
648
+ var workflow = selectedWorkflow();
456
649
  var source = sourceBody();
457
- if (!repo) { showIntakeError("Name a repo."); return; }
458
- if (source.kind === "prompt" && !String(source.text || "").trim()) { showIntakeError("Prompt is empty."); return; }
459
- if (source.kind === "spec" && !source.path) { showIntakeError("Choose a spec file."); return; }
460
- if (source.kind === "tickets" && !source.dir) { showIntakeError("Choose a tickets folder."); return; }
461
- if (source.kind === "jira" && !source.key) { showIntakeError("Enter a Jira key."); return; }
462
- var payload = { repo: repo, workflow: workflow ? workflow.value : "", source: source };
650
+ var payload = { repo: repo, workflow: workflow, source: source };
463
651
  if (dryRun) payload.dryRun = true;
464
652
  if (sourceKind !== "tickets") {
465
653
  var issue = document.getElementById("job-issue");
@@ -473,6 +661,7 @@
473
661
  }
474
662
  }
475
663
  sending = true;
664
+ syncValidity();
476
665
  showIntakeError("");
477
666
  fetch("/api/jobs", {
478
667
  method: "POST",
@@ -488,6 +677,7 @@
488
677
  })
489
678
  .then(function (result) {
490
679
  sending = false;
680
+ syncValidity();
491
681
  if (!result.ok) {
492
682
  showIntakeError((result.data && result.data.error) || "Could not add the job.");
493
683
  return;
@@ -507,6 +697,7 @@
507
697
  })
508
698
  .catch(function () {
509
699
  sending = false;
700
+ syncValidity();
510
701
  showIntakeError("Could not add the job.");
511
702
  });
512
703
  }
@@ -523,28 +714,7 @@
523
714
  }
524
715
  }
525
716
  if (!data || !Array.isArray(data.repos)) return;
526
- var box = document.getElementById("repo-pick");
527
- if (!box) return;
528
- var current = selectedRepo();
529
- var still = false;
530
- var html = "";
531
- if (!data.repos.length) html = '<p class="empty">No repos are registered.</p>';
532
- for (var i = 0; i < data.repos.length; i++) {
533
- var repo = data.repos[i];
534
- if (!repo || !repo.name) continue;
535
- if (repo.name === current) still = true;
536
- }
537
- if (data.repos.length) {
538
- html = "";
539
- for (var j = 0; j < data.repos.length; j++) {
540
- var item = data.repos[j];
541
- var pressed = still ? item.name === current : j === 0;
542
- html += '<button type="button" class="pick" data-repo="' + esc(item.name) + '" data-path="' + esc(item.path || "") + '" aria-pressed="' + (pressed ? "true" : "false") + '">' + esc(item.name) + "</button>";
543
- }
544
- }
545
- if (box.getAttribute("data-repos") === html) return;
546
- box.setAttribute("data-repos", html);
547
- box.innerHTML = html;
717
+ paintRepos(data.repos);
548
718
  }
549
719
 
550
720
  function showFresh() {
@@ -598,17 +768,31 @@
598
768
  if (message && slot.scrollIntoView) slot.scrollIntoView({ block: "nearest" });
599
769
  }
600
770
 
771
+ var settingsEpoch = 0;
772
+ var themeApplied = document.documentElement.getAttribute("data-theme") || "kiln";
773
+ var themeQueued = null;
774
+ var themeSaving = false;
775
+
601
776
  function setSettingsTab(tab) {
602
777
  var repos = document.getElementById("settings-repos");
603
778
  var globalPane = document.getElementById("settings-global");
779
+ var appearance = document.getElementById("settings-appearance");
604
780
  if (repos) repos.hidden = tab !== "repos";
605
781
  if (globalPane) globalPane.hidden = tab !== "global";
782
+ if (appearance) appearance.hidden = tab !== "appearance";
606
783
  var buttons = document.querySelectorAll("#settings-form [data-settings-tab]");
607
784
  for (var i = 0; i < buttons.length; i++) {
608
785
  buttons[i].setAttribute("aria-pressed", buttons[i].getAttribute("data-settings-tab") === tab ? "true" : "false");
609
786
  }
610
787
  }
611
788
 
789
+ function markTheme(id) {
790
+ var buttons = document.querySelectorAll("#theme-pick [data-theme-choice]");
791
+ for (var i = 0; i < buttons.length; i++) {
792
+ buttons[i].setAttribute("aria-pressed", buttons[i].getAttribute("data-theme-choice") === id ? "true" : "false");
793
+ }
794
+ }
795
+
612
796
  function setPressed(selector, attr, value) {
613
797
  var buttons = document.querySelectorAll(selector);
614
798
  for (var i = 0; i < buttons.length; i++) {
@@ -868,9 +1052,87 @@
868
1052
  if (mode) sandbox.mode = mode;
869
1053
  if (domains.length) sandbox.allowDomains = domains;
870
1054
  if (mode || domains.length) body.sandbox = sandbox;
1055
+ var themeButton = document.querySelector('#theme-pick [aria-pressed="true"]');
1056
+ var themeId = themeButton ? themeButton.getAttribute("data-theme-choice") : "";
1057
+ if (themeId) body.board = { theme: themeId };
1058
+ return body;
1059
+ }
1060
+
1061
+ function preservedSettings(data) {
1062
+ var body = { revision: data.revision, repos: data.repos || {} };
1063
+ ["stallTimeoutMs", "maxAttempts", "maxFixRounds", "maxSameFingerprint", "dailyDollarCap"].forEach(function (key) {
1064
+ if (data[key] !== undefined && data[key] !== null) body[key] = data[key];
1065
+ });
1066
+ if (data.sandbox) body.sandbox = data.sandbox;
871
1067
  return body;
872
1068
  }
873
1069
 
1070
+ function chooseTheme(id) {
1071
+ if (!id) return;
1072
+ settingsEpoch += 1;
1073
+ document.documentElement.setAttribute("data-theme", id);
1074
+ markTheme(id);
1075
+ themeQueued = id;
1076
+ if (!themeSaving) pumpTheme();
1077
+ }
1078
+
1079
+ function pumpTheme() {
1080
+ if (!themeQueued) return;
1081
+ var id = themeQueued;
1082
+ themeQueued = null;
1083
+ themeSaving = true;
1084
+ fetch("/api/settings", { cache: "no-store", headers: { Accept: "application/json" } })
1085
+ .then(function (response) { return response.json().then(function (data) { return { ok: response.ok, data: data }; }); })
1086
+ .then(function (result) {
1087
+ if (!result.ok) throw new Error((result.data && result.data.error) || "Could not load settings.");
1088
+ var body = preservedSettings(result.data);
1089
+ body.board = { theme: id };
1090
+ return fetch("/api/settings", {
1091
+ method: "PUT",
1092
+ headers: {
1093
+ Accept: "application/json",
1094
+ "Content-Type": "application/json",
1095
+ "X-Kiln-CSRF": bodyCsrf(),
1096
+ },
1097
+ body: JSON.stringify(body),
1098
+ });
1099
+ })
1100
+ .then(function (response) { return response.json().then(function (data) { return { ok: response.ok, data: data }; }); })
1101
+ .then(function (result) {
1102
+ themeSaving = false;
1103
+ if (!result.ok) {
1104
+ if (!themeQueued) restoreTheme();
1105
+ showSettingsError((result.data && result.data.error) || "Could not save the theme.");
1106
+ var fields = result.data && result.data.fields;
1107
+ if (fields && fields["board.theme"]) showSlot("board.theme", fields["board.theme"]);
1108
+ if (themeQueued) pumpTheme();
1109
+ return;
1110
+ }
1111
+ themeApplied = (result.data.board && result.data.board.theme) || id;
1112
+ if (!settingsDoc) applySettingsDocument(result.data);
1113
+ else if (result.data.revision) {
1114
+ settingsDoc.revision = result.data.revision;
1115
+ settingsDoc.board = result.data.board;
1116
+ }
1117
+ if (themeQueued) pumpTheme();
1118
+ })
1119
+ .catch(function (err) {
1120
+ themeSaving = false;
1121
+ if (!themeQueued) restoreTheme();
1122
+ showSettingsError(err && err.message ? err.message : "Could not save the theme.");
1123
+ if (themeQueued) pumpTheme();
1124
+ });
1125
+ }
1126
+
1127
+ function restoreTheme() {
1128
+ document.documentElement.setAttribute("data-theme", themeApplied);
1129
+ markTheme(themeApplied);
1130
+ }
1131
+
1132
+ function bodyCsrf() {
1133
+ return body.getAttribute("data-csrf") || "";
1134
+ }
1135
+
874
1136
  function applyFieldErrors(fields) {
875
1137
  var keys = Object.keys(fields || {});
876
1138
  var lines = [];
@@ -896,6 +1158,11 @@
896
1158
  showSlot(key, message);
897
1159
  continue;
898
1160
  }
1161
+ if (key === "board.theme") {
1162
+ setSettingsTab("appearance");
1163
+ showSlot("board.theme", message);
1164
+ continue;
1165
+ }
899
1166
  if (key === "name" || key.indexOf("repos.") === 0) {
900
1167
  setSettingsTab("repos");
901
1168
  showSlot("name", message);
@@ -910,6 +1177,12 @@
910
1177
  var pathEl = document.getElementById("settings-path");
911
1178
  if (pathEl) pathEl.textContent = data.path || "";
912
1179
  fillGlobal(data);
1180
+ var themeId = data.board && data.board.theme ? data.board.theme : "kiln";
1181
+ if (!themeSaving && !themeQueued) {
1182
+ themeApplied = themeId;
1183
+ document.documentElement.setAttribute("data-theme", themeId);
1184
+ markTheme(themeId);
1185
+ }
913
1186
  if (editing && editing.original && Object.prototype.hasOwnProperty.call(repoDraft, editing.original)) {
914
1187
  fillEditor(editing.original, repoDraft[editing.original]);
915
1188
  } else {
@@ -922,11 +1195,13 @@
922
1195
  clearSettingsErrors();
923
1196
  var saved = document.getElementById("settings-saved");
924
1197
  if (saved) saved.hidden = true;
1198
+ var epoch = settingsEpoch;
925
1199
  fetch("/api/settings", { cache: "no-store", headers: { Accept: "application/json" } })
926
1200
  .then(function (response) {
927
1201
  return response.json().then(function (data) { return { ok: response.ok, status: response.status, data: data }; });
928
1202
  })
929
1203
  .then(function (result) {
1204
+ if (epoch !== settingsEpoch) return;
930
1205
  if (!result.ok) {
931
1206
  showSettingsError((result.data && result.data.error) || "Could not load settings.");
932
1207
  return;
@@ -959,11 +1234,8 @@
959
1234
  if (buttonId === id) on = enabled;
960
1235
  toggles.push({ id: buttonId, enabled: on });
961
1236
  }
962
- var body = { revision: data.revision, repos: data.repos || {}, triggers: toggles };
963
- ["stallTimeoutMs", "maxAttempts", "maxFixRounds", "maxSameFingerprint", "dailyDollarCap"].forEach(function (key) {
964
- if (data[key] !== undefined && data[key] !== null) body[key] = data[key];
965
- });
966
- if (data.sandbox) body.sandbox = data.sandbox;
1237
+ var body = preservedSettings(data);
1238
+ body.triggers = toggles;
967
1239
  return fetch("/api/settings", {
968
1240
  method: "PUT",
969
1241
  headers: {
@@ -999,7 +1271,7 @@
999
1271
  headers: {
1000
1272
  Accept: "application/json",
1001
1273
  "Content-Type": "application/json",
1002
- "X-Kiln-CSRF": body.getAttribute("data-csrf") || "",
1274
+ "X-Kiln-CSRF": bodyCsrf(),
1003
1275
  },
1004
1276
  body: JSON.stringify(payload),
1005
1277
  })
@@ -1088,6 +1360,17 @@
1088
1360
  document.addEventListener("click", function (event) {
1089
1361
  var target = event.target;
1090
1362
  if (!target || !target.closest) return;
1363
+ var option = target.closest(".combo [role='option']");
1364
+ if (option) {
1365
+ chooseComboOption(option.closest(".combo"), option);
1366
+ return;
1367
+ }
1368
+ var query = target.closest(".combo [role='combobox']");
1369
+ if (query) {
1370
+ openCombo(query.closest(".combo"), false);
1371
+ return;
1372
+ }
1373
+ if (!target.closest(".combo")) closeCombos();
1091
1374
  var triggerToggle = target.closest("[data-trigger-toggle]");
1092
1375
  if (triggerToggle) {
1093
1376
  toggleTrigger(triggerToggle);
@@ -1107,10 +1390,30 @@
1107
1390
  setSource(sourceBtn.getAttribute("data-source") || "prompt");
1108
1391
  return;
1109
1392
  }
1393
+ var flowPick = target.closest("#workflow-pick [data-workflow]");
1394
+ if (flowPick) {
1395
+ var flows = document.querySelectorAll("#workflow-pick [data-workflow]");
1396
+ for (var f = 0; f < flows.length; f++) flows[f].setAttribute("aria-pressed", flows[f] === flowPick ? "true" : "false");
1397
+ var flowHost = document.getElementById("workflow-pick");
1398
+ if (flowHost) flowHost.setAttribute("data-workflow", flowPick.getAttribute("data-workflow") || "");
1399
+ syncValidity();
1400
+ return;
1401
+ }
1110
1402
  var pick = target.closest("#repo-pick .pick");
1111
1403
  if (pick) {
1112
1404
  var picks = document.querySelectorAll("#repo-pick .pick");
1113
1405
  for (var p = 0; p < picks.length; p++) picks[p].setAttribute("aria-pressed", picks[p] === pick ? "true" : "false");
1406
+ var repoBox = document.getElementById("repo-pick");
1407
+ if (repoBox) {
1408
+ repoBox.setAttribute("data-value", pick.getAttribute("data-repo") || "");
1409
+ repoBox.setAttribute("data-path", pick.getAttribute("data-path") || "");
1410
+ }
1411
+ syncValidity();
1412
+ return;
1413
+ }
1414
+ var themeChoice = target.closest("#theme-pick [data-theme-choice]");
1415
+ if (themeChoice) {
1416
+ chooseTheme(themeChoice.getAttribute("data-theme-choice") || "");
1114
1417
  return;
1115
1418
  }
1116
1419
  var settingsTab = target.closest("#settings-form [data-settings-tab]");
@@ -1173,6 +1476,7 @@
1173
1476
  if (fsButton.classList.contains("fs-use")) {
1174
1477
  ticketsDir = next;
1175
1478
  setPicked(next);
1479
+ syncValidity();
1176
1480
  return;
1177
1481
  }
1178
1482
  loadFs(next, false);
@@ -1245,6 +1549,33 @@
1245
1549
  });
1246
1550
 
1247
1551
  document.addEventListener("keydown", function (event) {
1552
+ var keyTarget = event.target;
1553
+ if (keyTarget && keyTarget.getAttribute && keyTarget.getAttribute("role") === "combobox") {
1554
+ var combo = keyTarget.closest(".combo");
1555
+ var list = combo ? combo.querySelector("[role='listbox']") : null;
1556
+ var comboOpen = list && !list.hidden;
1557
+ if (combo && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
1558
+ event.preventDefault();
1559
+ if (!comboOpen) openCombo(combo, false);
1560
+ else setComboActive(combo, Number(combo.getAttribute("data-active") || "0") + (event.key === "ArrowDown" ? 1 : -1));
1561
+ return;
1562
+ }
1563
+ if (combo && event.key === "Enter") {
1564
+ event.preventDefault();
1565
+ if (comboOpen) {
1566
+ var choices = visibleOptions(combo);
1567
+ var index = Number(combo.getAttribute("data-active") || "0");
1568
+ if (choices[index]) chooseComboOption(combo, choices[index]);
1569
+ }
1570
+ return;
1571
+ }
1572
+ if (combo && event.key === "Escape" && comboOpen) {
1573
+ event.preventDefault();
1574
+ event.stopPropagation();
1575
+ closeCombo(combo);
1576
+ return;
1577
+ }
1578
+ }
1248
1579
  if (event.key === "Escape") closeDrawer();
1249
1580
  var el = event.target;
1250
1581
  if ((event.key === "Enter" || event.key === " ") && el && el.classList && el.classList.contains("card")) {
@@ -1320,7 +1651,48 @@
1320
1651
  var issueInput = document.getElementById("job-issue");
1321
1652
  if (issueInput) issueInput.addEventListener("input", function () { issueDirty = true; });
1322
1653
  var jiraKey = document.getElementById("jira-key");
1323
- if (jiraKey) jiraKey.addEventListener("input", syncIssueDefault);
1654
+ if (jiraKey) jiraKey.addEventListener("input", function () {
1655
+ syncIssueDefault();
1656
+ syncValidity();
1657
+ });
1658
+ document.addEventListener("input", function (event) {
1659
+ var field = event.target;
1660
+ if (!field || !field.getAttribute) return;
1661
+ if (field.getAttribute("role") === "combobox") {
1662
+ openCombo(field.closest(".combo"), true);
1663
+ return;
1664
+ }
1665
+ if (field.id === "prompt-text" || field.id === "workflow") syncValidity();
1666
+ });
1667
+ function intakeFieldName(target) {
1668
+ if (!target || !target.closest || !target.closest("#add-form")) return "";
1669
+ if (target.id === "prompt-text") return "prompt";
1670
+ if (target.id === "jira-key") return "jira";
1671
+ if (target.id === "workflow" || target.closest("#workflow-pick")) return "workflow";
1672
+ if (target.id === "repo-pick-query" || target.closest("#repo-pick")) return "repo";
1673
+ if (target.closest("#browser-field")) return "source";
1674
+ return "";
1675
+ }
1676
+
1677
+ document.addEventListener("focusout", function (event) {
1678
+ var name = intakeFieldName(event.target);
1679
+ if (!name) return;
1680
+ var next = event.relatedTarget;
1681
+ var stay = { prompt: "#prompt-text", jira: "#jira-key", workflow: "#workflow-field", repo: "#repo-field", source: "#browser-field" };
1682
+ if (next && next.closest && next.closest(stay[name])) return;
1683
+ intakeTouched[name] = true;
1684
+ syncValidity();
1685
+ });
1686
+ document.addEventListener("focusin", function (event) {
1687
+ var field = event.target;
1688
+ if (!field || !field.getAttribute || field.getAttribute("role") !== "combobox") return;
1689
+ openCombo(field.closest(".combo"), false);
1690
+ if (field.select) field.select();
1691
+ });
1692
+ document.addEventListener("change", function (event) {
1693
+ var field = event.target;
1694
+ if (field && field.id === "workflow") syncValidity();
1695
+ });
1324
1696
  var addForm = document.getElementById("add-form");
1325
1697
  if (addForm) addForm.addEventListener("submit", function (event) {
1326
1698
  event.preventDefault();
@@ -1396,6 +1768,7 @@
1396
1768
  }
1397
1769
 
1398
1770
  showFresh();
1771
+ syncValidity();
1399
1772
  renderCompact();
1400
1773
  notifyLabel();
1401
1774
  applyRepo();
package/dist/config.js CHANGED
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
3
3
  import { dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import { KilnConfigError } from "./errors.js";
5
5
  import { DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_FIX_ROUNDS, DEFAULT_MAX_SAME_FINGERPRINT, DEFAULT_STALL_TIMEOUT_MS, KilnConfigFileSchema, REPO_NAME, } from "./schemas.js";
6
+ import { resolveBoardTheme } from "./theme.js";
6
7
  const CONFIG_NAME = "kiln.config.json";
7
8
  export function loadKilnConfig(configPath, options = {}) {
8
9
  const home = options.home ?? homedir();
@@ -60,6 +61,7 @@ export function loadKilnConfig(configPath, options = {}) {
60
61
  ...(parsed.data.reviewPrompt
61
62
  ? { reviewPrompt: resolveConfiguredPath(configDir, parsed.data.reviewPrompt, home) }
62
63
  : {}),
64
+ boardTheme: resolveBoardTheme(parsed.data.board?.theme),
63
65
  };
64
66
  }
65
67
  /**
@@ -230,7 +232,7 @@ function assertNoStaleCwdConfig(cwd, homeConfig) {
230
232
  }
231
233
  function readConfigObject(configPath) {
232
234
  if (!existsSync(configPath))
233
- return { repos: {} };
235
+ return { repos: {}, board: { theme: "kiln" } };
234
236
  let raw;
235
237
  try {
236
238
  raw = JSON.parse(readFileSync(configPath, "utf8"));