kaizen-agent 1.31.3 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/cli/board.test.ts +15 -21
- package/cli/browser.test.ts +3 -0
- package/cli/browser.ts +4 -2
- package/cli/build-web.ts +19 -9
- package/cli/notes.test.ts +4 -2
- package/cli/sources.test.ts +2 -2
- package/cli/state.ts +1 -1
- package/cli/web.ts +16 -7
- package/package.json +5 -2
- package/skills/kaizen/spec.md +5 -0
- package/web/board/agent-term.ts +51 -0
- package/web/board/api.ts +102 -0
- package/web/board/approvals.ts +100 -0
- package/web/board/cards.ts +600 -0
- package/web/board/chat.ts +108 -0
- package/web/board/dictation.ts +300 -0
- package/web/board/dom.ts +5 -0
- package/web/board/drafts.ts +21 -0
- package/web/board/fixes.ts +90 -0
- package/web/board/git.ts +76 -0
- package/web/board/icebox.ts +869 -0
- package/web/board/keys.ts +126 -0
- package/web/board/labels.ts +40 -0
- package/web/board/live.ts +69 -0
- package/web/board/main.ts +19 -0
- package/web/board/markdown.ts +197 -0
- package/web/board/panel.ts +331 -0
- package/web/board/projects.ts +220 -0
- package/web/board/settings.ts +191 -0
- package/web/board/store.ts +33 -0
- package/web/board/toast.ts +32 -0
- package/web/board/update.ts +75 -0
- package/web/board/views.ts +65 -0
- package/web/board.html +6 -978
- package/web/board.js +30 -0
- package/web/notebook.js +1 -1
- package/web/notebook.ts +726 -180
- package/web/notes-lib.ts +51 -15
- package/web/notes.html +1 -1
- package/web/term.js +1 -1
- package/web/term.ts +17 -5
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { store } from "./store.ts";
|
|
2
|
+
import { termStop } from "./agent-term.ts";
|
|
3
|
+
import { load, post } from "./api.ts";
|
|
4
|
+
import { stopDictation } from "./dictation.ts";
|
|
5
|
+
import { $, esc } from "./dom.ts";
|
|
6
|
+
import { close } from "./panel.ts";
|
|
7
|
+
import { askNotify, toast } from "./toast.ts";
|
|
8
|
+
import { updBar } from "./update.ts";
|
|
9
|
+
import { view } from "./views.ts";
|
|
10
|
+
|
|
11
|
+
// ---- settings
|
|
12
|
+
// Rows in config order, grouped as the TUI groups them.
|
|
13
|
+
const groups = (rows) => {
|
|
14
|
+
const m = new Map();
|
|
15
|
+
for (const r of rows) {
|
|
16
|
+
if (!m.has(r.group)) m.set(r.group, []);
|
|
17
|
+
m.get(r.group).push(r);
|
|
18
|
+
}
|
|
19
|
+
return [...m];
|
|
20
|
+
};
|
|
21
|
+
export async function openSettings() {
|
|
22
|
+
if (view !== "settings") return;
|
|
23
|
+
const gen = ++store.pageGen;
|
|
24
|
+
stopDictation(true);
|
|
25
|
+
// Coming from another page or a card, its body goes now, not when the fetch answers.
|
|
26
|
+
if (!$("panel").classList.contains("open") || $("ptitle").textContent !== "Settings") {
|
|
27
|
+
$("pbody").innerHTML = `<p class="snone">Loading…</p>`;
|
|
28
|
+
$("psub").textContent = "";
|
|
29
|
+
}
|
|
30
|
+
termStop();
|
|
31
|
+
store.openKey = null;
|
|
32
|
+
store.runDoc = null;
|
|
33
|
+
$("pterm").hidden = true;
|
|
34
|
+
$("panel").classList.add("open");
|
|
35
|
+
$("scrim").classList.add("on");
|
|
36
|
+
$("ptitle").textContent = "Settings";
|
|
37
|
+
$("ptabs").innerHTML = "";
|
|
38
|
+
$("pfoot").innerHTML = "";
|
|
39
|
+
const cur = document.documentElement.dataset.theme || "system";
|
|
40
|
+
const cfg = await fetch("/settings")
|
|
41
|
+
.then((r) => r.json())
|
|
42
|
+
.catch(() => null);
|
|
43
|
+
if (gen !== store.pageGen) return;
|
|
44
|
+
$("psub").textContent = "Theme applies to this browser. Everything else is saved to your kaizen settings.";
|
|
45
|
+
$("psub").title = cfg ? cfg.file : "";
|
|
46
|
+
// One control per row: a switch for on/off, one segment per value for a small number,
|
|
47
|
+
// a select for a choice. The row's label names the control; its help sits under it.
|
|
48
|
+
const ctl = (id, vals, on) => {
|
|
49
|
+
// The preset steps like a game's quick preset: ◁ ▷ go to the neighbouring tier,
|
|
50
|
+
// wrapping, and never to custom, which only an edit to another row makes.
|
|
51
|
+
if (id === "s-preset") {
|
|
52
|
+
const t = vals.filter((v) => v !== "custom"),
|
|
53
|
+
at = t.indexOf(on),
|
|
54
|
+
go = (d) => t[at < 0 ? (d > 0 ? 0 : t.length - 1) : (at + d + t.length) % t.length];
|
|
55
|
+
return `<span class="car" id="${id}" role="group" aria-label="Preset"><button id="${id}-prev" data-v="${go(-1)}" aria-label="Previous preset: ${go(-1)}" title="${go(-1)}">◁</button><span aria-live="polite">${esc(on)}<i aria-hidden="true">${t.map((v) => `<u class="${v === on ? "on" : ""}"></u>`).join("")}</i></span><button id="${id}-next" data-v="${go(1)}" aria-label="Next preset: ${go(1)}" title="${go(1)}">▷</button></span>`;
|
|
56
|
+
}
|
|
57
|
+
if (vals.length === 2 && vals.includes("true") && vals.includes("false")) {
|
|
58
|
+
const yes = on === "true";
|
|
59
|
+
return `<button class="chk${yes ? " on" : ""}" id="${id}" data-v="${yes ? "false" : "true"}" role="switch" aria-checked="${yes}" title="${yes ? "On" : "Off"}"></button>`;
|
|
60
|
+
}
|
|
61
|
+
if (vals.every((v) => /^\d+$/.test(v)))
|
|
62
|
+
return `<span class="bar" id="${id}" role="group">${vals.map((v) => `<button data-v="${v}" class="${v === on ? "on" : ""}" aria-pressed="${v === on}">${v}</button>`).join("")}</span>`;
|
|
63
|
+
return `<select class="sel" id="${id}">${vals.map((v) => `<option value="${esc(v)}"${v === on ? " selected" : ""}>${v === "" ? "—" : esc(v)}</option>`).join("")}</select>`;
|
|
64
|
+
};
|
|
65
|
+
const row = (label, control, hint, id, q) =>
|
|
66
|
+
`<div class="srow" data-q="${esc(q.toLowerCase())}"><label for="${id}">${esc(label)}</label>${control}<p class="hint">${hint}</p></div>`;
|
|
67
|
+
const hadFocus = document.activeElement?.id === "sq";
|
|
68
|
+
const np = typeof Notification === "undefined" ? null : Notification.permission;
|
|
69
|
+
const ntHelp =
|
|
70
|
+
"A desktop notification when a run starts waiting on you." +
|
|
71
|
+
(np ? (np === "denied" ? " Blocked in this browser's site settings." : "") : " Not supported in this browser.");
|
|
72
|
+
const names = [...(cfg ? groups(cfg.rows).map(([g]) => g) : []), "Browser"];
|
|
73
|
+
$("pbody").innerHTML =
|
|
74
|
+
`<div class="sq"><input class="sel" id="sq" type="search" placeholder="Search settings" aria-label="Search settings" spellcheck="false" autocomplete="off" value="${esc(store.setQ)}"><div class="sjump" role="group" aria-label="Jump to section">${names.map((g) => `<button data-g="${esc(g)}">${esc(g)}</button>`).join("")}</div></div>
|
|
75
|
+
${
|
|
76
|
+
cfg
|
|
77
|
+
? groups(cfg.rows)
|
|
78
|
+
.map(
|
|
79
|
+
([g, rows]) =>
|
|
80
|
+
`<h3 class="sg">${esc(g)}</h3>${rows
|
|
81
|
+
.map((r) => {
|
|
82
|
+
const id = "s-" + r.key.replace(/\./g, "-");
|
|
83
|
+
return row(
|
|
84
|
+
r.label,
|
|
85
|
+
r.text
|
|
86
|
+
? r.folder
|
|
87
|
+
? `<span class="fpick"><input class="sel" id="${id}" value="${esc(r.value)}" placeholder="not set" spellcheck="false"><button class="btn" id="${id}-pick" type="button">Browse…</button></span>`
|
|
88
|
+
: `<input class="sel" id="${id}" value="${esc(r.value)}" placeholder="not set" spellcheck="false">`
|
|
89
|
+
: ctl(id, r.values, r.value),
|
|
90
|
+
esc(r.help),
|
|
91
|
+
id,
|
|
92
|
+
[r.label, r.help, r.key, g, ...r.values].join(" "),
|
|
93
|
+
);
|
|
94
|
+
})
|
|
95
|
+
.join("")}`,
|
|
96
|
+
)
|
|
97
|
+
.join("")
|
|
98
|
+
: `<p class="snone">Settings could not be read. Run <code>kaizen settings</code>.</p>`
|
|
99
|
+
}
|
|
100
|
+
<h3 class="sg">Browser</h3>${row("Theme", ctl("th", ["system", "light", "dark"], cur), "Light or dark for this browser only; system follows your device.", "th", "theme light dark system browser")}
|
|
101
|
+
${row("Notifications", `<button class="chk${np === "granted" ? " on" : ""}" id="nt" role="switch" aria-checked="${np === "granted"}"${np && np !== "denied" ? "" : " disabled"}></button>`, ntHelp, "nt", "notifications " + ntHelp + " browser")}
|
|
102
|
+
<p class="snone" id="snone" hidden></p>`;
|
|
103
|
+
// Hides rows that do not match, then any group (and its chip) left with none.
|
|
104
|
+
updBar();
|
|
105
|
+
const heads = [...$("pbody").querySelectorAll(".sg")],
|
|
106
|
+
chips = [...$("pbody").querySelectorAll(".sjump button")];
|
|
107
|
+
const filter = () => {
|
|
108
|
+
const q = store.setQ.trim().toLowerCase();
|
|
109
|
+
let any = false;
|
|
110
|
+
heads.forEach((h, i) => {
|
|
111
|
+
let n = h.nextElementSibling,
|
|
112
|
+
vis = false;
|
|
113
|
+
for (; n && n.matches(".srow"); n = n.nextElementSibling) {
|
|
114
|
+
n.hidden = !!q && !n.dataset.q.includes(q);
|
|
115
|
+
vis ||= !n.hidden;
|
|
116
|
+
}
|
|
117
|
+
h.hidden = !vis;
|
|
118
|
+
if (chips[i]) chips[i].hidden = !vis;
|
|
119
|
+
any ||= vis;
|
|
120
|
+
});
|
|
121
|
+
$("snone").hidden = any;
|
|
122
|
+
$("snone").textContent = `No setting matches “${store.setQ.trim()}”.`;
|
|
123
|
+
};
|
|
124
|
+
// Scrolled by measure: the pinned bar's height changes as the chips wrap.
|
|
125
|
+
chips.forEach(
|
|
126
|
+
(b, i) =>
|
|
127
|
+
(b.onclick = () => {
|
|
128
|
+
const pb = $("pbody");
|
|
129
|
+
pb.scrollTo({
|
|
130
|
+
top:
|
|
131
|
+
pb.scrollTop +
|
|
132
|
+
heads[i].getBoundingClientRect().top -
|
|
133
|
+
pb.querySelector(".sq").getBoundingClientRect().bottom -
|
|
134
|
+
8,
|
|
135
|
+
behavior: "smooth",
|
|
136
|
+
});
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
$("sq").oninput = (e) => {
|
|
140
|
+
store.setQ = e.target.value;
|
|
141
|
+
filter();
|
|
142
|
+
};
|
|
143
|
+
// Esc clears a search; with nothing to clear it closes the panel, as it does outside the field.
|
|
144
|
+
$("sq").onkeydown = (e) => {
|
|
145
|
+
if (e.key !== "Escape") return;
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
if (store.setQ) {
|
|
148
|
+
store.setQ = "";
|
|
149
|
+
e.target.value = "";
|
|
150
|
+
filter();
|
|
151
|
+
} else close();
|
|
152
|
+
};
|
|
153
|
+
filter();
|
|
154
|
+
if (hadFocus) {
|
|
155
|
+
const f = $("sq");
|
|
156
|
+
f.focus();
|
|
157
|
+
f.setSelectionRange(f.value.length, f.value.length);
|
|
158
|
+
}
|
|
159
|
+
$("th").onchange = (e) => setTheme(e.target.value);
|
|
160
|
+
$("nt").onclick = () => {
|
|
161
|
+
askNotify();
|
|
162
|
+
setTimeout(openSettings, 300);
|
|
163
|
+
};
|
|
164
|
+
if (cfg)
|
|
165
|
+
cfg.rows.forEach((r) => {
|
|
166
|
+
const el = $("s-" + r.key.replace(/\./g, "-"));
|
|
167
|
+
const save = async (v) => {
|
|
168
|
+
const f = document.activeElement?.id;
|
|
169
|
+
const j = await post("/settings", { key: r.key, value: v });
|
|
170
|
+
if (j.ok) toast("Saved");
|
|
171
|
+
await openSettings();
|
|
172
|
+
if (f?.startsWith("s-preset-")) $(f)?.focus();
|
|
173
|
+
};
|
|
174
|
+
if (el.tagName === "SELECT" || el.tagName === "INPUT")
|
|
175
|
+
el.onchange = async () => {
|
|
176
|
+
await save(el.value.trim());
|
|
177
|
+
if (el.tagName === "INPUT") load();
|
|
178
|
+
};
|
|
179
|
+
else el.querySelectorAll("[data-v]").forEach((b) => (b.onclick = () => save(b.dataset.v)));
|
|
180
|
+
if (el.matches(".chk")) el.onclick = () => save(el.dataset.v);
|
|
181
|
+
// The system's own folder dialog, opened by the board: a page cannot read a real path.
|
|
182
|
+
if (r.folder)
|
|
183
|
+
$(el.id + "-pick").onclick = async () => {
|
|
184
|
+
const j = await post("/pick-folder", {});
|
|
185
|
+
if (j.path) {
|
|
186
|
+
await save(j.path);
|
|
187
|
+
load();
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// State more than one module writes: the board's data, the open card and form, the
|
|
2
|
+
// panel's tab and doc, and the page filters. Everything else lives in the module that
|
|
3
|
+
// owns it.
|
|
4
|
+
export const store = {
|
|
5
|
+
S: { project: null, projects: [], cards: [], now: Date.now(), all: false },
|
|
6
|
+
all: new URLSearchParams(location.search).get("all") !== "0",
|
|
7
|
+
openKey: null,
|
|
8
|
+
mode: null,
|
|
9
|
+
modeKey: null,
|
|
10
|
+
menuKey: null,
|
|
11
|
+
tab: "request",
|
|
12
|
+
lastTab: "request",
|
|
13
|
+
runDoc: null,
|
|
14
|
+
clearMenu: false,
|
|
15
|
+
wantTerm: null,
|
|
16
|
+
// The settings search survives the re-render each save does; the gear button starts it empty.
|
|
17
|
+
setQ: "",
|
|
18
|
+
// Settings and the Icebox fill the same panel after a fetch: a fill only lands if no other view
|
|
19
|
+
// or fill started since, or a slow answer paints one page under the other's title.
|
|
20
|
+
pageGen: 0,
|
|
21
|
+
// The project picker's list.
|
|
22
|
+
PL: [],
|
|
23
|
+
// The all-projects filter: tick the projects to show; none ticked shows every one. Kept per browser.
|
|
24
|
+
shown: new Set(
|
|
25
|
+
(() => {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(localStorage.getItem("kz-show")) || [];
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
})(),
|
|
32
|
+
),
|
|
33
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { $ } from "./dom.ts";
|
|
2
|
+
|
|
3
|
+
// With `report`, the toast carries a link to a prefilled GitHub bug form (/issue) and
|
|
4
|
+
// stays long enough to click it.
|
|
5
|
+
let tt;
|
|
6
|
+
export function toast(s, report) {
|
|
7
|
+
const t = $("toast");
|
|
8
|
+
t.textContent = s;
|
|
9
|
+
t.classList.toggle("rep", !!report);
|
|
10
|
+
if (report) {
|
|
11
|
+
const a = document.createElement("a");
|
|
12
|
+
a.href = "/issue?" + new URLSearchParams(report);
|
|
13
|
+
a.target = "_blank";
|
|
14
|
+
a.rel = "noopener";
|
|
15
|
+
a.textContent = "Report";
|
|
16
|
+
t.append(" ", a);
|
|
17
|
+
}
|
|
18
|
+
t.classList.add("on");
|
|
19
|
+
clearTimeout(tt);
|
|
20
|
+
tt = setTimeout(() => t.classList.remove("on"), report ? 8000 : 4000);
|
|
21
|
+
}
|
|
22
|
+
export function askNotify() {
|
|
23
|
+
try {
|
|
24
|
+
if (typeof Notification !== "undefined" && Notification.permission === "default") Notification.requestPermission();
|
|
25
|
+
} catch (e) {}
|
|
26
|
+
}
|
|
27
|
+
export function notifyUser(body) {
|
|
28
|
+
try {
|
|
29
|
+
if (typeof Notification !== "undefined" && Notification.permission === "granted")
|
|
30
|
+
new Notification("kaizen", { body, icon: "/logo.png" });
|
|
31
|
+
} catch (e) {}
|
|
32
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { post } from "./api.ts";
|
|
2
|
+
import { $, esc } from "./dom.ts";
|
|
3
|
+
import { openSettings } from "./settings.ts";
|
|
4
|
+
import { view } from "./views.ts";
|
|
5
|
+
|
|
6
|
+
// A newer kaizen on npm: a dot on Settings, and an Update bar at the top of that page.
|
|
7
|
+
// The server upgrades and starts a board on the new version, which replaces this one;
|
|
8
|
+
// the hello with its new boot id reloads the page (listen()). With none known, the bar
|
|
9
|
+
// is a quiet row whose Check for updates asks npm now instead of the server's 6 h cache.
|
|
10
|
+
let upd = null,
|
|
11
|
+
ver = null;
|
|
12
|
+
function gotVer(v) {
|
|
13
|
+
ver = v;
|
|
14
|
+
if (!v.update) return false;
|
|
15
|
+
upd = v;
|
|
16
|
+
const a = document.querySelector('.rail a[data-view="settings"]');
|
|
17
|
+
a.classList.add("upd");
|
|
18
|
+
a.title = `Settings: kaizen ${v.latest} is available`;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
export function updBar() {
|
|
22
|
+
if (!upd) {
|
|
23
|
+
const d = document.createElement("div");
|
|
24
|
+
d.className = "updbar quiet";
|
|
25
|
+
d.innerHTML = `<span id="upd-msg" aria-live="polite">${ver ? `You are running kaizen ${esc(ver.version)}.` : "kaizen"}</span><button class="btn" id="upd-chk">Check for updates</button>`;
|
|
26
|
+
$("pbody").prepend(d);
|
|
27
|
+
const b = $("upd-chk");
|
|
28
|
+
b.onclick = async () => {
|
|
29
|
+
b.disabled = true;
|
|
30
|
+
b.textContent = "Checking…";
|
|
31
|
+
const v = await fetch("/version?fresh=1")
|
|
32
|
+
.then((r) => r.json())
|
|
33
|
+
.catch(() => null);
|
|
34
|
+
if (!d.isConnected) return;
|
|
35
|
+
b.disabled = false;
|
|
36
|
+
b.textContent = "Check for updates";
|
|
37
|
+
if (v && gotVer(v)) {
|
|
38
|
+
d.remove();
|
|
39
|
+
updBar();
|
|
40
|
+
$("upd-go").focus();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
$("upd-msg").textContent =
|
|
44
|
+
!v || !v.latest ? "Could not reach npm. Try again later." : `kaizen ${v.version} is up to date.`;
|
|
45
|
+
};
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const d = document.createElement("div");
|
|
49
|
+
d.className = "updbar";
|
|
50
|
+
d.innerHTML = `<span>kaizen <b>${esc(upd.latest)}</b> is available. You are running ${esc(upd.version)}.</span><button class="btn pri" id="upd-go">Update kaizen</button>`;
|
|
51
|
+
$("pbody").prepend(d);
|
|
52
|
+
const b = $("upd-go");
|
|
53
|
+
b.onclick = async () => {
|
|
54
|
+
if (!confirm(`Update kaizen to ${upd.latest}? The board restarts, and agents running inside cards stop.`)) return;
|
|
55
|
+
b.disabled = true;
|
|
56
|
+
b.textContent = "Updating…";
|
|
57
|
+
const r = await post("/upgrade", {});
|
|
58
|
+
if (!r.ok) {
|
|
59
|
+
b.disabled = false;
|
|
60
|
+
b.textContent = "Update kaizen";
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
b.textContent = "Restarting…";
|
|
64
|
+
setTimeout(() => location.reload(), 20000);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function initUpdate() {
|
|
69
|
+
fetch("/version")
|
|
70
|
+
.then((r) => r.json())
|
|
71
|
+
.then((v) => {
|
|
72
|
+
if (gotVer(v) && view === "settings") openSettings();
|
|
73
|
+
})
|
|
74
|
+
.catch(() => {});
|
|
75
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { store } from "./store.ts";
|
|
2
|
+
import { $ } from "./dom.ts";
|
|
3
|
+
import { openSources } from "./icebox.ts";
|
|
4
|
+
import { close } from "./panel.ts";
|
|
5
|
+
import { openSettings } from "./settings.ts";
|
|
6
|
+
|
|
7
|
+
// ---- views: the board, or Settings and the Icebox as pages in the docked panel, each with its own address
|
|
8
|
+
export let view = "board";
|
|
9
|
+
export const markView = (v) => {
|
|
10
|
+
document
|
|
11
|
+
.querySelectorAll(".rail a.it")
|
|
12
|
+
.forEach((a) =>
|
|
13
|
+
a.dataset.view === v ? a.setAttribute("aria-current", "page") : a.removeAttribute("aria-current"),
|
|
14
|
+
);
|
|
15
|
+
// The Icebox is titled in the header, top left, like the board.
|
|
16
|
+
document.querySelector(".hdr h1").textContent = v === "sources" ? "Icebox" : "Board";
|
|
17
|
+
};
|
|
18
|
+
export function go(v, push) {
|
|
19
|
+
store.pageGen++;
|
|
20
|
+
if (v === "icebox") v = "sources";
|
|
21
|
+
if (!["settings", "sources"].includes(v)) v = "board";
|
|
22
|
+
const was = view;
|
|
23
|
+
view = v;
|
|
24
|
+
markView(v);
|
|
25
|
+
document.body.classList.toggle("paged", v !== "board");
|
|
26
|
+
document.body.classList.toggle("ice", v === "sources");
|
|
27
|
+
if (push && v !== was)
|
|
28
|
+
history.pushState(null, "", v === "board" ? location.pathname : "?view=" + (v === "sources" ? "icebox" : v));
|
|
29
|
+
const p = $("panel");
|
|
30
|
+
if (v === "board") {
|
|
31
|
+
if (was === "board") return;
|
|
32
|
+
// Undocked without the slide: it was a page, not a drawer leaving.
|
|
33
|
+
p.style.transition = "none";
|
|
34
|
+
p.classList.remove("open", "docked");
|
|
35
|
+
p.offsetWidth;
|
|
36
|
+
p.style.transition = "";
|
|
37
|
+
close();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
p.classList.add("docked");
|
|
41
|
+
if (v === "settings") openSettings();
|
|
42
|
+
else openSources();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function initViews() {
|
|
46
|
+
document.querySelectorAll(".rail a[data-view]").forEach((a) => {
|
|
47
|
+
if (a.dataset.view === "notes") return;
|
|
48
|
+
a.onclick = (e) => {
|
|
49
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey || e.button) return;
|
|
50
|
+
e.preventDefault();
|
|
51
|
+
if (a.dataset.view === "settings") store.setQ = "";
|
|
52
|
+
go(a.dataset.view, true);
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
document.querySelector(".rail .home").onclick = (e) => {
|
|
56
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey || e.button) return;
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
go("board", true);
|
|
59
|
+
};
|
|
60
|
+
addEventListener("popstate", () => go(new URLSearchParams(location.search).get("view")));
|
|
61
|
+
// The menu's theme switch while Settings shows: its Theme row follows.
|
|
62
|
+
addEventListener("kz-theme", () => {
|
|
63
|
+
if (view === "settings") openSettings();
|
|
64
|
+
});
|
|
65
|
+
}
|