codetrail-cli 1.4.1__py3-none-any.whl
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.
- codetrail/__init__.py +3 -0
- codetrail/__main__.py +5 -0
- codetrail/agents/__init__.py +4 -0
- codetrail/agents/convert.py +92 -0
- codetrail/agents/export.py +284 -0
- codetrail/agents/instructions.py +42 -0
- codetrail/agents/protocol.py +111 -0
- codetrail/agents/tools.py +185 -0
- codetrail/assistant/__init__.py +161 -0
- codetrail/assistant/claude_code.py +330 -0
- codetrail/assistant/codex.py +260 -0
- codetrail/assistant/environment.py +66 -0
- codetrail/assistant/estimate.py +225 -0
- codetrail/assistant/fake.py +74 -0
- codetrail/assistant/guard.py +80 -0
- codetrail/assistant/guard_hook.py +58 -0
- codetrail/assistant/local.py +245 -0
- codetrail/assistant/local_tools.py +177 -0
- codetrail/assistant/prompts.py +404 -0
- codetrail/assistant/routing.py +86 -0
- codetrail/assistant/runner.py +120 -0
- codetrail/assistant/status.py +186 -0
- codetrail/assistant/usage.py +93 -0
- codetrail/bridge.py +292 -0
- codetrail/cli.py +407 -0
- codetrail/config.py +485 -0
- codetrail/database/__init__.py +60 -0
- codetrail/database/migrations/0001_facts.sql +34 -0
- codetrail/database/migrations/0002_settings.sql +5 -0
- codetrail/database/migrations/0003_learning.sql +34 -0
- codetrail/database/migrations/0004_assistant_usage.sql +24 -0
- codetrail/database/migrations/0005_page_failures.sql +7 -0
- codetrail/database/migrations/0006_metric_values.sql +9 -0
- codetrail/errors.py +5 -0
- codetrail/extract/__init__.py +249 -0
- codetrail/extract/adr.py +98 -0
- codetrail/extract/github_actions.py +140 -0
- codetrail/extract/openapi.py +101 -0
- codetrail/extract/python.py +221 -0
- codetrail/extract/swift.py +204 -0
- codetrail/extract/terraform.py +166 -0
- codetrail/extract/typescript.py +464 -0
- codetrail/facts/__init__.py +112 -0
- codetrail/facts/store.py +209 -0
- codetrail/generate/__init__.py +1 -0
- codetrail/generate/failures.py +31 -0
- codetrail/generate/outline.py +159 -0
- codetrail/generate/revise.py +72 -0
- codetrail/generate/run.py +530 -0
- codetrail/generate/scope.py +135 -0
- codetrail/generate/validate.py +218 -0
- codetrail/guide/__init__.py +147 -0
- codetrail/learn/__init__.py +143 -0
- codetrail/learn/routes.py +139 -0
- codetrail/locales/ar/LC_MESSAGES/codetrail.mo +0 -0
- codetrail/locales/ar/LC_MESSAGES/codetrail.po +2236 -0
- codetrail/locales/codetrail.pot +1942 -0
- codetrail/lock.py +114 -0
- codetrail/metrics/__init__.py +6 -0
- codetrail/metrics/activity.py +96 -0
- codetrail/metrics/churn.py +156 -0
- codetrail/metrics/commits.py +45 -0
- codetrail/metrics/coverage.py +114 -0
- codetrail/metrics/decisions.py +77 -0
- codetrail/metrics/history.py +47 -0
- codetrail/metrics/inventory.py +63 -0
- codetrail/metrics/rationale.py +74 -0
- codetrail/metrics/releases.py +26 -0
- codetrail/metrics/report.py +111 -0
- codetrail/metrics/repository.py +128 -0
- codetrail/metrics/size.py +95 -0
- codetrail/py.typed +0 -0
- codetrail/remove.py +50 -0
- codetrail/repo/__init__.py +4 -0
- codetrail/repo/git.py +56 -0
- codetrail/repo/gitleaks.toml +6 -0
- codetrail/repo/history.py +262 -0
- codetrail/repo/mirror.py +137 -0
- codetrail/repo/refresh.py +41 -0
- codetrail/repo/rules.py +83 -0
- codetrail/repo/secrets.py +145 -0
- codetrail/repo/signal.py +65 -0
- codetrail/repo/source.py +166 -0
- codetrail/search.py +178 -0
- codetrail/server.py +45 -0
- codetrail/system.py +484 -0
- codetrail/update.py +276 -0
- codetrail/web/__init__.py +1 -0
- codetrail/web/app.py +897 -0
- codetrail/web/diagrams.py +414 -0
- codetrail/web/documentation.py +146 -0
- codetrail/web/i18n.py +44 -0
- codetrail/web/navigation.py +127 -0
- codetrail/web/render.py +126 -0
- codetrail/web/repository_view.py +230 -0
- codetrail/web/security.py +171 -0
- codetrail/web/static/brand/favicon.svg +1 -0
- codetrail/web/static/brand/mark.svg +1 -0
- codetrail/web/static/css/base.css +192 -0
- codetrail/web/static/css/components.css +2254 -0
- codetrail/web/static/css/content.css +310 -0
- codetrail/web/static/css/layout.css +410 -0
- codetrail/web/static/css/print.css +42 -0
- codetrail/web/static/css/tokens.css +228 -0
- codetrail/web/static/js/api.js +49 -0
- codetrail/web/static/js/ask.js +182 -0
- codetrail/web/static/js/d2-worker.js +34 -0
- codetrail/web/static/js/d2.js +28 -0
- codetrail/web/static/js/diagram.js +52 -0
- codetrail/web/static/js/documentation.js +24 -0
- codetrail/web/static/js/learning.js +47 -0
- codetrail/web/static/js/main.js +38 -0
- codetrail/web/static/js/outline.js +44 -0
- codetrail/web/static/js/palette.js +211 -0
- codetrail/web/static/js/repository.js +528 -0
- codetrail/web/static/js/resize.js +126 -0
- codetrail/web/static/js/shortcuts.js +87 -0
- codetrail/web/static/js/sidebar.js +194 -0
- codetrail/web/static/js/theme-init.js +27 -0
- codetrail/web/static/js/theme.js +42 -0
- codetrail/web/static/js/update.js +107 -0
- codetrail/web/static/vendor/d2/LICENSE +375 -0
- codetrail/web/static/vendor/d2/LICENSE-elk +263 -0
- codetrail/web/static/vendor/d2/LICENSE-wasm_exec +27 -0
- codetrail/web/static/vendor/d2/VERSION +12 -0
- codetrail/web/static/vendor/d2/d2.wasm +0 -0
- codetrail/web/static/vendor/d2/elk.js +105914 -0
- codetrail/web/static/vendor/d2/wasm_exec.js +656 -0
- codetrail/web/static/vendor/inter/InterVariable-Italic.woff2 +0 -0
- codetrail/web/static/vendor/inter/InterVariable.woff2 +0 -0
- codetrail/web/static/vendor/inter/LICENSE.txt +92 -0
- codetrail/web/static/vendor/inter/VERSION +6 -0
- codetrail/web/static/vendor/noto-sans-arabic/NotoSansArabic-wght.woff2 +0 -0
- codetrail/web/static/vendor/noto-sans-arabic/OFL.txt +93 -0
- codetrail/web/static/vendor/noto-sans-arabic/VERSION +12 -0
- codetrail/web/target_view.py +63 -0
- codetrail/web/templates/answers.html +25 -0
- codetrail/web/templates/area.html +46 -0
- codetrail/web/templates/base.html +37 -0
- codetrail/web/templates/decisions.html +30 -0
- codetrail/web/templates/diagram.html +10 -0
- codetrail/web/templates/digests.html +24 -0
- codetrail/web/templates/documentation.html +247 -0
- codetrail/web/templates/error.html +9 -0
- codetrail/web/templates/estimate.html +48 -0
- codetrail/web/templates/fact.html +29 -0
- codetrail/web/templates/home.html +196 -0
- codetrail/web/templates/layout/ask_panel.html +27 -0
- codetrail/web/templates/layout/header.html +32 -0
- codetrail/web/templates/layout/icons.html +27 -0
- codetrail/web/templates/layout/outline.html +4 -0
- codetrail/web/templates/layout/palette.html +27 -0
- codetrail/web/templates/layout/shortcuts.html +27 -0
- codetrail/web/templates/layout/sidebar.html +79 -0
- codetrail/web/templates/layout/update_dialog.html +18 -0
- codetrail/web/templates/macros.html +13 -0
- codetrail/web/templates/page.html +93 -0
- codetrail/web/templates/progress.html +46 -0
- codetrail/web/templates/repository.html +369 -0
- codetrail/web/templates/search.html +31 -0
- codetrail/web/templates/source.html +15 -0
- codetrail/web/templates/system.html +43 -0
- codetrail/web/templates/system_arrows.html +17 -0
- codetrail/web/templates/system_legend.html +9 -0
- codetrail/web/templates/update_log.html +30 -0
- codetrail/web/trend.py +22 -0
- codetrail_cli-1.4.1.dist-info/METADATA +372 -0
- codetrail_cli-1.4.1.dist-info/RECORD +171 -0
- codetrail_cli-1.4.1.dist-info/WHEEL +4 -0
- codetrail_cli-1.4.1.dist-info/entry_points.txt +3 -0
- codetrail_cli-1.4.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Keyboard shortcuts (design section 16.4). None of them spends anything: Ask and Update only open the panel or the
|
|
2
|
+
// estimate dialog. Single keys are ignored while typing and whenever Command, Control or Alt is held.
|
|
3
|
+
import { closeAsk, isAskOpen, openAsk } from "./ask.js";
|
|
4
|
+
import { toggleRead } from "./learning.js";
|
|
5
|
+
import { openPalette } from "./palette.js";
|
|
6
|
+
import { dismissPeek, isPeeking, toggleSidebar } from "./sidebar.js";
|
|
7
|
+
import { hideUpdatePanel, isUpdatePanelOpen, startUpdate } from "./update.js";
|
|
8
|
+
|
|
9
|
+
const PLACES = { h: "/", p: "/progress", y: "/system", s: "/answers", d: "/digests", r: "/decisions", o: "/documentation", t: "/repository" };
|
|
10
|
+
let awaitingPlace = null;
|
|
11
|
+
|
|
12
|
+
function typing(element) {
|
|
13
|
+
return element instanceof Element && Boolean(element.closest("input, textarea, select, [contenteditable]"));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function follow(selector) {
|
|
17
|
+
const link = document.querySelector(selector);
|
|
18
|
+
if (link instanceof HTMLAnchorElement) window.location.href = link.href;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function closeMenu() {
|
|
22
|
+
document.body.classList.remove("menu-open");
|
|
23
|
+
document.querySelector("[data-menu]")?.setAttribute("aria-expanded", "false");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function setUpShortcuts() {
|
|
27
|
+
const help = document.querySelector("[data-shortcuts]");
|
|
28
|
+
for (const button of document.querySelectorAll("[data-shortcuts-open]")) {
|
|
29
|
+
button.addEventListener("click", () => help?.showModal());
|
|
30
|
+
}
|
|
31
|
+
help?.querySelector("[data-shortcuts-close]")?.addEventListener("click", () => help.close());
|
|
32
|
+
const menu = document.querySelector("[data-menu]");
|
|
33
|
+
menu?.addEventListener("click", () => {
|
|
34
|
+
const open = document.body.classList.toggle("menu-open");
|
|
35
|
+
menu.setAttribute("aria-expanded", String(open));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
document.addEventListener("keydown", (event) => {
|
|
39
|
+
if (event.isComposing || event.defaultPrevented) return;
|
|
40
|
+
const key = event.key;
|
|
41
|
+
if ((event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && key.toLowerCase() === "k") {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
openPalette();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
47
|
+
if (document.querySelector("dialog[open]")) return; // an open dialog handles its own keys
|
|
48
|
+
if (key === "Escape") {
|
|
49
|
+
if (isPeeking()) dismissPeek();
|
|
50
|
+
else if (document.body.classList.contains("menu-open")) closeMenu();
|
|
51
|
+
else if (isUpdatePanelOpen()) hideUpdatePanel(); // it sits over the Ask panel
|
|
52
|
+
else if (isAskOpen()) closeAsk();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (typing(event.target)) return;
|
|
56
|
+
if (awaitingPlace) {
|
|
57
|
+
clearTimeout(awaitingPlace);
|
|
58
|
+
awaitingPlace = null;
|
|
59
|
+
const place = PLACES[key.toLowerCase()];
|
|
60
|
+
if (place) {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
window.location.href = place;
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const actions = {
|
|
67
|
+
"/": () => openPalette(),
|
|
68
|
+
a: () => openAsk(),
|
|
69
|
+
g: () => {
|
|
70
|
+
awaitingPlace = setTimeout(() => {
|
|
71
|
+
awaitingPlace = null;
|
|
72
|
+
}, 1000);
|
|
73
|
+
},
|
|
74
|
+
"[": () => follow("[data-pager-previous]"),
|
|
75
|
+
"]": () => follow("[data-pager-next]"),
|
|
76
|
+
"\\": () => toggleSidebar(),
|
|
77
|
+
m: () => toggleRead(),
|
|
78
|
+
u: () => startUpdate(),
|
|
79
|
+
"?": () => help?.showModal(),
|
|
80
|
+
};
|
|
81
|
+
const action = actions[key.length === 1 && key !== "?" ? key.toLowerCase() : key];
|
|
82
|
+
if (action) {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
action();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// The sidebar (design section 22): hiding and showing it, peeking at it while hidden, folding its sections and
|
|
2
|
+
// pinning pages. Hidden and folded are remembered in this browser; pins are kept by Codetrail. Titles are only ever
|
|
3
|
+
// moved with textContent and attributes.
|
|
4
|
+
import { post, remember } from "./api.js";
|
|
5
|
+
|
|
6
|
+
const root = document.documentElement;
|
|
7
|
+
const SECTIONS = ["paths", "areas", "concepts", "answers", "more"];
|
|
8
|
+
const PEEK_DELAY = 150; // ms the pointer rests on the edge before the sidebar slides out
|
|
9
|
+
const LEAVE_DELAY = 300; // ms after the pointer leaves before it slides away
|
|
10
|
+
const narrow = window.matchMedia("(max-width: 900px)");
|
|
11
|
+
let peekTimer = 0;
|
|
12
|
+
|
|
13
|
+
const panel = () => document.querySelector("#sidebar");
|
|
14
|
+
const edge = () => document.querySelector("[data-sidebar-edge]");
|
|
15
|
+
const isHidden = () => root.dataset.sidebar === "hidden";
|
|
16
|
+
export const isPeeking = () => document.body.classList.contains("sidebar-peek");
|
|
17
|
+
|
|
18
|
+
function syncToggle() {
|
|
19
|
+
const button = document.querySelector("[data-sidebar-toggle]");
|
|
20
|
+
if (!button) return;
|
|
21
|
+
button.setAttribute("aria-expanded", String(!isHidden()));
|
|
22
|
+
button.setAttribute("aria-label", isHidden() ? button.dataset.labelShow : button.dataset.labelHide);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function closePeek(returnFocus = false) {
|
|
26
|
+
clearTimeout(peekTimer);
|
|
27
|
+
if (!isPeeking()) return;
|
|
28
|
+
document.body.classList.remove("sidebar-peek");
|
|
29
|
+
edge()?.setAttribute("aria-expanded", "false");
|
|
30
|
+
if (returnFocus) edge()?.focus();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function openPeek(focusFirst) {
|
|
34
|
+
clearTimeout(peekTimer);
|
|
35
|
+
if (!isHidden() || narrow.matches) return;
|
|
36
|
+
document.body.classList.add("sidebar-peek");
|
|
37
|
+
edge()?.setAttribute("aria-expanded", "true");
|
|
38
|
+
if (focusFirst) panel()?.querySelector("a[href]")?.focus();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function closeSoon() {
|
|
42
|
+
clearTimeout(peekTimer);
|
|
43
|
+
peekTimer = setTimeout(() => closePeek(), LEAVE_DELAY);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Esc: the focus goes back to the edge only when it was in the sidebar; elsewhere it stays where the reader is.
|
|
47
|
+
export function dismissPeek() {
|
|
48
|
+
closePeek(Boolean(panel()?.contains(document.activeElement)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function toggleSidebar() {
|
|
52
|
+
if (narrow.matches) return;
|
|
53
|
+
const hide = !isHidden(); // while peeking it is hidden, so this docks it
|
|
54
|
+
const focusWasInside = Boolean(panel()?.contains(document.activeElement));
|
|
55
|
+
if (hide) root.dataset.sidebar = "hidden";
|
|
56
|
+
else delete root.dataset.sidebar;
|
|
57
|
+
remember("localStorage", "codetrail.sidebar", hide ? "hidden" : null);
|
|
58
|
+
closePeek();
|
|
59
|
+
syncToggle();
|
|
60
|
+
if (hide && focusWasInside) document.querySelector("[data-sidebar-toggle]")?.focus();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function setUpPeek() {
|
|
64
|
+
const strip = edge();
|
|
65
|
+
const sidebar = panel();
|
|
66
|
+
if (!strip || !sidebar) return;
|
|
67
|
+
// The card slides in under the pointer resting on the edge: moving between the two isn't leaving.
|
|
68
|
+
const inside = (node) => node instanceof Node && (node === strip || sidebar.contains(node));
|
|
69
|
+
strip.addEventListener("pointerenter", (event) => {
|
|
70
|
+
clearTimeout(peekTimer);
|
|
71
|
+
// Only a pointer arriving from the page peeks: the card sliding away under it after Esc mustn't reopen it.
|
|
72
|
+
if (!isPeeking() && !inside(event.relatedTarget)) peekTimer = setTimeout(() => openPeek(false), PEEK_DELAY);
|
|
73
|
+
});
|
|
74
|
+
strip.addEventListener("pointerleave", (event) => {
|
|
75
|
+
if (!isPeeking()) clearTimeout(peekTimer);
|
|
76
|
+
else if (!inside(event.relatedTarget)) closeSoon();
|
|
77
|
+
});
|
|
78
|
+
strip.addEventListener("click", () => openPeek(true)); // open, or already open: either way, into the sidebar
|
|
79
|
+
strip.addEventListener("focusout", (event) => {
|
|
80
|
+
const next = event.relatedTarget;
|
|
81
|
+
if (isPeeking() && next instanceof Node && !sidebar.contains(next)) closePeek(); // tabbed on into the page
|
|
82
|
+
});
|
|
83
|
+
sidebar.addEventListener("pointerenter", () => clearTimeout(peekTimer));
|
|
84
|
+
sidebar.addEventListener("pointerleave", (event) => isPeeking() && !inside(event.relatedTarget) && closeSoon());
|
|
85
|
+
narrow.addEventListener("change", () => closePeek()); // the drawer takes over below 900 px
|
|
86
|
+
sidebar.addEventListener("focusout", (event) => {
|
|
87
|
+
const next = event.relatedTarget;
|
|
88
|
+
if (isPeeking() && next instanceof Node && !sidebar.contains(next) && next !== strip) closePeek();
|
|
89
|
+
});
|
|
90
|
+
sidebar.addEventListener("click", (event) => {
|
|
91
|
+
if (isPeeking() && event.target instanceof Element && event.target.closest("a[href]")) closePeek();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function folded() {
|
|
96
|
+
return (root.dataset.folded ?? "").split(" ").filter((name) => SECTIONS.includes(name));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function setUpFolding() {
|
|
100
|
+
for (const button of document.querySelectorAll("[data-fold]")) {
|
|
101
|
+
const name = button.dataset.fold;
|
|
102
|
+
button.setAttribute("aria-expanded", String(!folded().includes(name)));
|
|
103
|
+
button.addEventListener("click", () => {
|
|
104
|
+
const now = folded();
|
|
105
|
+
const next = now.includes(name) ? now.filter((other) => other !== name) : [...now, name];
|
|
106
|
+
if (next.length) root.dataset.folded = next.join(" ");
|
|
107
|
+
else delete root.dataset.folded;
|
|
108
|
+
remember("localStorage", "codetrail.sidebar-folded", next.length ? next.join(" ") : null);
|
|
109
|
+
button.setAttribute("aria-expanded", String(!next.includes(name)));
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const pageHref = (id) => `/pages/${id.split("/").map(encodeURIComponent).join("/")}`;
|
|
115
|
+
|
|
116
|
+
function setUpPins() {
|
|
117
|
+
const sidebar = panel();
|
|
118
|
+
const list = sidebar?.querySelector("[data-pins]");
|
|
119
|
+
const template = sidebar?.querySelector("[data-pin-template]");
|
|
120
|
+
if (!sidebar || !list || !template) return;
|
|
121
|
+
const limit = Number(sidebar.dataset.maxPins) || 1;
|
|
122
|
+
// The drawn tiles are the list: a pinned page the guide dropped isn't drawn, so it isn't sent back.
|
|
123
|
+
const pinned = () => [...list.querySelectorAll("[data-unpin]")].map((button) => button.dataset.unpin);
|
|
124
|
+
|
|
125
|
+
function sync(ids) {
|
|
126
|
+
list.hidden = ids.length === 0;
|
|
127
|
+
for (const button of sidebar.querySelectorAll("[data-pin]")) {
|
|
128
|
+
const on = ids.includes(button.dataset.pin);
|
|
129
|
+
button.setAttribute("aria-pressed", String(on));
|
|
130
|
+
button.disabled = ids.length >= limit && !on;
|
|
131
|
+
if (button.disabled) button.title = sidebar.dataset.labelFull;
|
|
132
|
+
else button.removeAttribute("title");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function tile(button) {
|
|
137
|
+
const id = button.dataset.pin;
|
|
138
|
+
const title = button.closest("li")?.querySelector(".label")?.textContent.trim() || id;
|
|
139
|
+
const item = template.content.firstElementChild.cloneNode(true);
|
|
140
|
+
const link = item.querySelector("a");
|
|
141
|
+
const href = pageHref(id);
|
|
142
|
+
link.href = href;
|
|
143
|
+
link.title = title;
|
|
144
|
+
link.setAttribute("aria-label", title);
|
|
145
|
+
link.textContent = button.dataset.initials;
|
|
146
|
+
// The guide's pages are English; a saved answer is in whatever language it was asked in.
|
|
147
|
+
if (button.dataset.kind === "answer") link.dir = "auto";
|
|
148
|
+
else Object.assign(link, { lang: "en", dir: "ltr" });
|
|
149
|
+
// Compared as written: the template's link has no base to resolve against until it joins the page.
|
|
150
|
+
if (window.location.pathname === href) link.setAttribute("aria-current", "page");
|
|
151
|
+
const remove = item.querySelector("button");
|
|
152
|
+
remove.dataset.unpin = id;
|
|
153
|
+
remove.setAttribute("aria-label", sidebar.dataset.labelUnpin.replace("{title}", () => title)); // no $ patterns
|
|
154
|
+
return item;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function save(ids) {
|
|
158
|
+
try {
|
|
159
|
+
return (await post("/settings/pins", { pages: ids })).ok;
|
|
160
|
+
} catch {
|
|
161
|
+
return false; // the sidebar stays as it was
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let saving = false; // one change at a time, so a quick second click can't work from a stale list
|
|
166
|
+
sidebar.addEventListener("click", async (event) => {
|
|
167
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
168
|
+
const pin = target?.closest("[data-pin]");
|
|
169
|
+
const unpin = target?.closest("[data-unpin]");
|
|
170
|
+
if ((!pin && !unpin) || saving) return;
|
|
171
|
+
saving = true;
|
|
172
|
+
try {
|
|
173
|
+
const id = pin ? pin.dataset.pin : unpin.dataset.unpin;
|
|
174
|
+
const ids = pinned();
|
|
175
|
+
const next = ids.includes(id) ? ids.filter((other) => other !== id) : [...ids, id];
|
|
176
|
+
if (!(await save(next))) return;
|
|
177
|
+
if (next.includes(id)) list.append(tile(pin));
|
|
178
|
+
else list.querySelector(`[data-unpin="${CSS.escape(id)}"]`)?.closest("li")?.remove();
|
|
179
|
+
sync(next);
|
|
180
|
+
if (unpin) sidebar.querySelector("a[href]")?.focus();
|
|
181
|
+
} finally {
|
|
182
|
+
saving = false;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
for (const button of sidebar.querySelectorAll("[data-pin], [data-unpin]")) button.hidden = false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function setUpSidebar() {
|
|
189
|
+
document.querySelector("[data-sidebar-toggle]")?.addEventListener("click", toggleSidebar);
|
|
190
|
+
syncToggle();
|
|
191
|
+
setUpPeek();
|
|
192
|
+
setUpFolding();
|
|
193
|
+
setUpPins();
|
|
194
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Applies the reader's theme, panel widths and sidebar state before the page is drawn, so nothing flashes or jumps
|
|
2
|
+
// (design 16.1, 16.2, 22). A classic script, loaded in <head> from Codetrail itself: the security policy allows no
|
|
3
|
+
// inline script. resize.js fits the widths to the window once the page has loaded; sidebar.js runs the sidebar.
|
|
4
|
+
(function () {
|
|
5
|
+
"use strict";
|
|
6
|
+
var root = document.documentElement;
|
|
7
|
+
var widths = [
|
|
8
|
+
["codetrail.sidebar-width", "--sidebar-width", 200, 420],
|
|
9
|
+
["codetrail.panel-width", "--panel-width", 320, 720],
|
|
10
|
+
];
|
|
11
|
+
var sections = ["paths", "areas", "concepts", "answers", "more"];
|
|
12
|
+
try {
|
|
13
|
+
var theme = window.localStorage.getItem("codetrail.theme");
|
|
14
|
+
if (theme === "light" || theme === "dark") root.setAttribute("data-theme", theme);
|
|
15
|
+
for (var index = 0; index < widths.length; index += 1) {
|
|
16
|
+
var width = Number(window.localStorage.getItem(widths[index][0]));
|
|
17
|
+
if (width >= widths[index][2] && width <= widths[index][3]) root.style.setProperty(widths[index][1], width + "px");
|
|
18
|
+
}
|
|
19
|
+
if (window.localStorage.getItem("codetrail.sidebar") === "hidden") root.setAttribute("data-sidebar", "hidden");
|
|
20
|
+
var folded = (window.localStorage.getItem("codetrail.sidebar-folded") || "").split(" ").filter(function (name) {
|
|
21
|
+
return sections.indexOf(name) >= 0;
|
|
22
|
+
});
|
|
23
|
+
if (folded.length) root.setAttribute("data-folded", folded.join(" "));
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// storage refused: the system's theme, the default widths and the docked, open sidebar apply
|
|
26
|
+
}
|
|
27
|
+
})();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The theme switch: system, light or dark, remembered in localStorage when the browser allows it (design 16.2).
|
|
2
|
+
import { recall, remember } from "./api.js";
|
|
3
|
+
|
|
4
|
+
const THEMES = ["system", "light", "dark"];
|
|
5
|
+
const button = document.querySelector("[data-theme-toggle]");
|
|
6
|
+
|
|
7
|
+
function current() {
|
|
8
|
+
const stored = recall("localStorage", "codetrail.theme");
|
|
9
|
+
return THEMES.includes(stored) ? stored : "system";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function apply(theme) {
|
|
13
|
+
if (theme === "system") document.documentElement.removeAttribute("data-theme");
|
|
14
|
+
else document.documentElement.setAttribute("data-theme", theme);
|
|
15
|
+
if (button) {
|
|
16
|
+
const label = button.dataset[`label${theme[0].toUpperCase()}${theme.slice(1)}`] ?? theme;
|
|
17
|
+
button.setAttribute("aria-label", label);
|
|
18
|
+
button.title = label;
|
|
19
|
+
button.dataset.theme = theme;
|
|
20
|
+
}
|
|
21
|
+
document.dispatchEvent(new CustomEvent("codetrail:theme"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isDark() {
|
|
25
|
+
const chosen = document.documentElement.getAttribute("data-theme");
|
|
26
|
+
if (chosen) return chosen === "dark";
|
|
27
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function cycleTheme() {
|
|
31
|
+
const next = THEMES[(THEMES.indexOf(current()) + 1) % THEMES.length];
|
|
32
|
+
remember("localStorage", "codetrail.theme", next === "system" ? null : next);
|
|
33
|
+
apply(next);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function setUpTheme() {
|
|
37
|
+
apply(current());
|
|
38
|
+
button?.addEventListener("click", cycleTheme);
|
|
39
|
+
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
|
|
40
|
+
if (current() === "system") apply("system");
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Updating the guide from the page (design section 15.4): the facts are refreshed for free, then the estimate waits
|
|
2
|
+
// in a dialog; only Go ahead, with the estimate's single-use id, lets paid work start. The update panel shows each
|
|
3
|
+
// step as the server reports it, closes once the update is done, and stays open with the reason when it fails.
|
|
4
|
+
import { getJSON, post } from "./api.js";
|
|
5
|
+
|
|
6
|
+
const dialog = document.querySelector("[data-estimate-dialog]");
|
|
7
|
+
const panel = document.querySelector("[data-update-panel]");
|
|
8
|
+
const log = panel?.querySelector("[data-update-log]");
|
|
9
|
+
const stateLine = panel?.querySelector("[data-update-state]");
|
|
10
|
+
const ACTIVE = ["preparing", "waiting", "running"];
|
|
11
|
+
const DONE_SHOWN_MS = 1500; // how long "Updated." stays before the panel closes
|
|
12
|
+
let estimateId = null;
|
|
13
|
+
let polling = false;
|
|
14
|
+
let active = false;
|
|
15
|
+
let after = 0; // the number of the last step shown
|
|
16
|
+
|
|
17
|
+
function say(text, failed = false) {
|
|
18
|
+
stateLine.textContent = text;
|
|
19
|
+
stateLine.classList.toggle("failed", failed);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function showUpdatePanel() {
|
|
23
|
+
if (panel) panel.hidden = false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function hideUpdatePanel() {
|
|
27
|
+
if (panel) panel.hidden = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isUpdatePanelOpen() {
|
|
31
|
+
return Boolean(panel && !panel.hidden);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function decide(url) {
|
|
35
|
+
if (!estimateId) return;
|
|
36
|
+
const id = estimateId;
|
|
37
|
+
estimateId = null;
|
|
38
|
+
dialog.close();
|
|
39
|
+
await post(url, { estimate_id: id });
|
|
40
|
+
poll();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function poll(reloadWhenDone = true) {
|
|
44
|
+
if (polling) return;
|
|
45
|
+
polling = true;
|
|
46
|
+
try {
|
|
47
|
+
const result = await getJSON(`/update/status?after=${after}`);
|
|
48
|
+
const labels = panel.dataset;
|
|
49
|
+
const { state, message } = result;
|
|
50
|
+
active = ACTIVE.includes(state);
|
|
51
|
+
if (result.log_html) log.insertAdjacentHTML("beforeend", result.log_html); // rendered, escaped on the server
|
|
52
|
+
after = result.next ?? after;
|
|
53
|
+
if (active) say(labels.labelRunning);
|
|
54
|
+
else if (state === "done") say(labels.labelDone);
|
|
55
|
+
else if (state === "failed") say(`${labels.labelFailed} ${message ?? ""}`, true);
|
|
56
|
+
else if (state === "declined") say(message ?? "", true);
|
|
57
|
+
if (state === "waiting" && result.estimate_id && result.estimate_id !== estimateId) {
|
|
58
|
+
estimateId = result.estimate_id;
|
|
59
|
+
dialog.querySelector("[data-estimate-body]").innerHTML = result.estimate_html; // rendered, escaped on the server
|
|
60
|
+
dialog.showModal();
|
|
61
|
+
}
|
|
62
|
+
polling = false;
|
|
63
|
+
if (active) setTimeout(() => poll(reloadWhenDone), 1500);
|
|
64
|
+
else if (state === "done") setTimeout(reloadWhenDone ? () => window.location.reload() : hideUpdatePanel, DONE_SHOWN_MS);
|
|
65
|
+
} catch {
|
|
66
|
+
polling = false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function startUpdate() {
|
|
71
|
+
if (!dialog) return;
|
|
72
|
+
showUpdatePanel();
|
|
73
|
+
if (active) return; // one runs already: the panel shows it
|
|
74
|
+
log.replaceChildren();
|
|
75
|
+
say(panel.dataset.labelRunning);
|
|
76
|
+
const response = await post("/update", {});
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const result = await response.json().catch(() => ({}));
|
|
79
|
+
if (response.status !== 409 && result.message) {
|
|
80
|
+
say(result.message, true);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
poll();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function setUpUpdate() {
|
|
88
|
+
if (!dialog) return;
|
|
89
|
+
dialog.querySelector("[data-estimate-go]").addEventListener("click", () => decide("/update/confirm"));
|
|
90
|
+
dialog.querySelector("[data-estimate-cancel]").addEventListener("click", () => decide("/update/cancel"));
|
|
91
|
+
dialog.addEventListener("cancel", () => decide("/update/cancel")); // Escape cancels too
|
|
92
|
+
for (const button of document.querySelectorAll("[data-update-button]")) {
|
|
93
|
+
button.addEventListener("click", startUpdate);
|
|
94
|
+
}
|
|
95
|
+
panel.querySelector("[data-update-close]").addEventListener("click", hideUpdatePanel);
|
|
96
|
+
// An update already running or waiting when the page opens shows in the panel, with its steps so far.
|
|
97
|
+
getJSON("/update/status")
|
|
98
|
+
.then((result) => {
|
|
99
|
+
if (ACTIVE.includes(result.state)) {
|
|
100
|
+
showUpdatePanel();
|
|
101
|
+
poll(false);
|
|
102
|
+
} else {
|
|
103
|
+
after = result.next ?? 0; // the steps of an update that has ended aren't shown again
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
.catch(() => {});
|
|
107
|
+
}
|