Messages, threads, acknowledgements, claims, bands, tasks, review items and stop flags for a team of AI agents; the chat page and board; host hooks and MCP server; autopilot; tools/watch.py. Put under version control after the weekend's changes so they have history and can be rolled back. Local only.
78 lines
5.5 KiB
JavaScript
78 lines
5.5 KiB
JavaScript
/* Harbor's theme picker, shared by the chat and the board. A theme is a palette family (themes.css; Harbor's own look in
|
|
harbor.css is the default) and a mode: system, light or dark. Both are kept in localStorage (harbor.palette,
|
|
harbor.theme), set on <html data-palette data-theme> before the first paint by each page's head script, and kept in
|
|
step across open tabs. Any element with data-theme-open opens the Appearance dialog; radios named "theme" set the
|
|
mode; elements with data-theme-name show the current theme's name. */
|
|
(function () {
|
|
var THEMES = [
|
|
{ id: "harbor", name: "Harbor", note: "Default" },
|
|
{ id: "dmg", name: "DMG" }, { id: "graphite", name: "Duke" }, { id: "atlas", name: "FLUDD" },
|
|
{ id: "rosepine", name: "Flowey" }, { id: "gruvbox", name: "Gerudo" }, { id: "contrast", name: "Kill Screen" },
|
|
{ id: "snes", name: "Mode 7" }, { id: "terra", name: "Shovel Knight" }, { id: "nord", name: "Snowdin" },
|
|
{ id: "solarized", name: "Solaire" }, { id: "tokyo", name: "Tokyo-to" }, { id: "dracula", name: "Vampire Killer" }
|
|
];
|
|
var MODES = [["auto", "System", "monitor"], ["light", "Light", "sun"], ["dark", "Dark", "moon"]];
|
|
var root = document.documentElement, dark = matchMedia("(prefers-color-scheme: dark)"), dialog = null;
|
|
function get(k, d) { try { var v = localStorage.getItem("harbor." + k); return v === null ? d : JSON.parse(v); } catch (e) { return d; } }
|
|
function put(k, v) { try { localStorage.setItem("harbor." + k, JSON.stringify(v)); } catch (e) {} }
|
|
function mode() { var m = get("theme", "auto"); return m === "light" || m === "dark" ? m : "auto"; }
|
|
function palette() { var p = get("palette", "harbor"); return THEMES.some(function (t) { return t.id === p; }) ? p : "harbor"; }
|
|
function side() { var m = mode(); return m === "auto" ? (dark.matches ? "dark" : "light") : m; }
|
|
function icon(n) { return '<svg class="i sm" aria-hidden="true"><use href="static/icons.svg#' + n + '"></use></svg>'; }
|
|
function name() { var p = palette(); return THEMES.filter(function (t) { return t.id === p; })[0].name; }
|
|
|
|
function apply() {
|
|
var m = mode(), p = palette();
|
|
if (m === "auto") delete root.dataset.theme; else root.dataset.theme = m;
|
|
if (p === "harbor") delete root.dataset.palette; else root.dataset.palette = p;
|
|
paint();
|
|
}
|
|
function paint() {
|
|
var m = mode(), p = palette(), s = side();
|
|
document.querySelectorAll('input[name="theme"], input[name="themeMode"]').forEach(function (r) { r.checked = r.value === m; });
|
|
document.querySelectorAll("[data-theme-name]").forEach(function (e) { e.textContent = name(); });
|
|
if (!dialog) return;
|
|
dialog.querySelectorAll("[data-pick]").forEach(function (b) {
|
|
b.setAttribute("aria-pressed", String(b.dataset.pick === p));
|
|
b.querySelector(".tprev").dataset.p = b.dataset.pick + "-" + s;
|
|
});
|
|
}
|
|
function set(key, value) { put(key, value); apply(); }
|
|
|
|
function build() {
|
|
dialog = document.createElement("dialog");
|
|
dialog.className = "theme-dialog";
|
|
dialog.setAttribute("aria-labelledby", "themeDialogTitle");
|
|
dialog.innerHTML =
|
|
'<header><h2 id="themeDialogTitle">Appearance</h2><button type="button" class="btn icon sm ghost" data-theme-close aria-label="Close">' +
|
|
'<svg class="i" aria-hidden="true"><use href="static/icons.svg#x"></use></svg></button></header>' +
|
|
'<div class="body"><div class="theme-row"><span class="field-label">Mode</span><div class="segmented" role="radiogroup" aria-label="Mode">' +
|
|
MODES.map(function (x) { return '<label><input type="radio" name="themeMode" value="' + x[0] + '"><span>' + icon(x[2]) + x[1] + "</span></label>"; }).join("") +
|
|
'</div></div><div class="theme-grid" role="list">' +
|
|
THEMES.map(function (t) {
|
|
return '<button type="button" class="tcard" role="listitem" data-pick="' + t.id + '" aria-pressed="false"><span class="tprev" data-p="' + t.id + '-light" aria-hidden="true">' +
|
|
'<i class="ps"><b></b><b></b><b></b></i><i class="ph"></i><span class="pb"><span class="pc"><b></b><b></b><em></em></span></span></span>' +
|
|
'<span class="tname">' + t.name + (t.note ? ' <span class="note">' + t.note + "</span>" : "") + "</span></button>";
|
|
}).join("") +
|
|
'</div></div><footer><span class="grow note">Every theme has a light and a dark side; the mode picks which one you see. The choice carries over to the chat and the board.</span>' +
|
|
'<button type="button" class="btn primary" data-theme-close>Done</button></footer>';
|
|
document.body.appendChild(dialog);
|
|
dialog.addEventListener("click", function (e) {
|
|
if (e.target === dialog || e.target.closest("[data-theme-close]")) return dialog.close();
|
|
var card = e.target.closest("[data-pick]");
|
|
if (card) set("palette", card.dataset.pick);
|
|
});
|
|
}
|
|
function open() { if (!dialog) build(); paint(); dialog.showModal(); var on = dialog.querySelector('[aria-pressed="true"]'); if (on) on.focus(); }
|
|
|
|
document.addEventListener("change", function (e) {
|
|
var r = e.target;
|
|
if (r.matches && r.matches('input[name="theme"], input[name="themeMode"]')) set("theme", r.value);
|
|
});
|
|
document.addEventListener("click", function (e) { if (e.target.closest && e.target.closest("[data-theme-open]")) open(); });
|
|
window.addEventListener("storage", function (e) { if (e.key === "harbor.theme" || e.key === "harbor.palette") apply(); });
|
|
if (dark.addEventListener) dark.addEventListener("change", paint);
|
|
window.HarborTheme = { themes: THEMES, apply: apply, open: open, mode: mode, palette: palette, side: side };
|
|
apply();
|
|
})();
|