HTML · 16020 bytes Raw Blame History
1 {{ define "layout" -}}
2 <!DOCTYPE html>
3 <html lang="en" data-theme="auto">
4 <head>
5 <script>
6 // Theme flash avoidance: read the cookie or system preference and apply
7 // before any CSS computes. The four themes are: light, dark, auto,
8 // high-contrast (S10 wires the picker; S02 just enforces the contract).
9 (function () {
10 var match = document.cookie.match(/(?:^|; )theme=([^;]+)/);
11 var theme = match ? decodeURIComponent(match[1]) : "auto";
12 if (theme === "auto") {
13 theme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
14 }
15 document.documentElement.setAttribute("data-theme", theme);
16 })();
17 </script>
18 <meta charset="UTF-8">
19 <meta name="viewport" content="width=device-width, initial-scale=1">
20 <meta name="color-scheme" content="light dark">
21 <meta name="description" content="shithub — GitHub. Open source. Without Copilot.">
22 {{ if .OGTitle }}<meta property="og:title" content="{{ .OGTitle }}">{{ end }}
23 {{ if .OGDescription }}<meta property="og:description" content="{{ .OGDescription }}">{{ end }}
24 {{ if .OGImage }}<meta property="og:image" content="{{ .OGImage }}">{{ end }}
25 <title>{{ .Title }} · shithub</title>
26 <link rel="icon" type="image/svg+xml" href="/static/logo/favicon.svg">
27 <link rel="stylesheet" href="/static/primer/primer.css" onerror="this.remove()">
28 <link rel="stylesheet" href="/static/css/shithub.css">
29 <link rel="stylesheet" href="/static/css/chroma.css">
30 {{ if flag . "UseHTMX" }}<script src="/static/vendor/htmx/htmx.min.js" defer></script>{{ end }}
31 </head>
32 <body class="shithub-body">
33 {{ template "nav" . }}
34 {{ template "impersonation-banner" . }}
35 <main class="shithub-main">
36 {{ template "page" . }}
37 </main>
38 {{ template "footer" . }}
39 <script>
40 // Click-to-copy on clone-dropdown buttons. Walk every button with
41 // data-clone-copy="<selector>" and wire it once. Falls back to a
42 // visual flash on the input when navigator.clipboard is unavailable
43 // (older browsers, file:// dev pages, etc).
44 (function () {
45 var buttons = document.querySelectorAll("[data-clone-copy]");
46 buttons.forEach(function (btn) {
47 btn.addEventListener("click", function () {
48 var sel = btn.getAttribute("data-clone-copy");
49 var input = document.querySelector(sel);
50 if (!input) return;
51 var ok = false;
52 try {
53 if (navigator.clipboard) {
54 navigator.clipboard.writeText(input.value);
55 ok = true;
56 }
57 } catch (e) { ok = false; }
58 if (!ok) {
59 input.select();
60 try { document.execCommand("copy"); ok = true; } catch (e) {}
61 }
62 var prev = btn.getAttribute("title") || "";
63 btn.setAttribute("title", ok ? "Copied" : "Press ⌘/Ctrl-C to copy");
64 setTimeout(function () { btn.setAttribute("title", prev); }, 1200);
65 });
66 });
67 })();
68
69 (function () {
70 var drawer = document.querySelector("[data-offcanvas]");
71 var opener = document.querySelector("[data-offcanvas-open]");
72 if (!drawer || !opener) return;
73 var panel = drawer.querySelector(".shithub-offcanvas-panel");
74 var closers = drawer.querySelectorAll("[data-offcanvas-close]");
75 var notice = drawer.querySelector("[data-offcanvas-notice]");
76 var noticeClose = drawer.querySelector("[data-offcanvas-notice-close]");
77 var lastFocus = null;
78
79 function openDrawer() {
80 lastFocus = document.activeElement;
81 drawer.hidden = false;
82 document.body.classList.add("shithub-offcanvas-open");
83 opener.setAttribute("aria-expanded", "true");
84 if (panel) panel.focus();
85 }
86
87 function closeDrawer() {
88 drawer.hidden = true;
89 document.body.classList.remove("shithub-offcanvas-open");
90 opener.setAttribute("aria-expanded", "false");
91 if (lastFocus && lastFocus.focus) lastFocus.focus();
92 }
93
94 opener.addEventListener("click", openDrawer);
95 closers.forEach(function (button) {
96 button.addEventListener("click", closeDrawer);
97 });
98 document.addEventListener("keydown", function (event) {
99 if (event.key === "Escape" && !drawer.hidden) closeDrawer();
100 });
101
102 var storage = null;
103 try { storage = window.localStorage; } catch (e) {}
104
105 if (notice && storage) {
106 try {
107 if (storage.getItem("shithubTeamsMovedNoticeDismissed") === "1") {
108 notice.hidden = true;
109 }
110 } catch (e) {}
111 }
112 if (noticeClose && notice) {
113 noticeClose.addEventListener("click", function () {
114 notice.hidden = true;
115 try {
116 if (storage) {
117 storage.setItem("shithubTeamsMovedNoticeDismissed", "1");
118 }
119 } catch (e) {}
120 });
121 }
122 })();
123
124 (function () {
125 var root = document.querySelector("[data-search-root]");
126 if (!root) return;
127 var input = root.querySelector("[data-search-input]");
128 var panel = root.querySelector("[data-search-results]");
129 if (!input || !panel || !window.fetch) return;
130
131 var timer = 0;
132 var controller = null;
133
134 function closePanel() {
135 panel.hidden = true;
136 panel.innerHTML = "";
137 input.setAttribute("aria-expanded", "false");
138 if (controller) {
139 controller.abort();
140 controller = null;
141 }
142 }
143
144 function showPanel(html) {
145 panel.innerHTML = html;
146 panel.hidden = false;
147 input.setAttribute("aria-expanded", "true");
148 }
149
150 function loadSuggestions() {
151 var query = input.value.trim();
152 if (query.length < 2) {
153 closePanel();
154 return;
155 }
156 if (controller) controller.abort();
157 controller = new AbortController();
158 fetch("/search/quick?q=" + encodeURIComponent(query), {
159 headers: { "X-Requested-With": "XMLHttpRequest" },
160 signal: controller.signal
161 }).then(function (response) {
162 if (response.status === 204) return "";
163 if (!response.ok) throw new Error("search quick failed");
164 return response.text();
165 }).then(function (html) {
166 if (!html) {
167 closePanel();
168 return;
169 }
170 showPanel(html);
171 }).catch(function (error) {
172 if (error.name !== "AbortError") closePanel();
173 });
174 }
175
176 function queueSuggestions() {
177 window.clearTimeout(timer);
178 timer = window.setTimeout(loadSuggestions, 160);
179 }
180
181 input.addEventListener("input", queueSuggestions);
182 input.addEventListener("focus", function () {
183 if (input.value.trim().length >= 2) queueSuggestions();
184 });
185 input.addEventListener("keydown", function (event) {
186 if (event.key === "Escape") {
187 closePanel();
188 input.blur();
189 }
190 });
191 root.addEventListener("submit", closePanel);
192 document.addEventListener("click", function (event) {
193 if (!root.contains(event.target)) closePanel();
194 });
195 document.addEventListener("keydown", function (event) {
196 var active = document.activeElement;
197 var editable = active && (
198 active.tagName === "INPUT" ||
199 active.tagName === "TEXTAREA" ||
200 active.isContentEditable
201 );
202 if (event.key === "/" && !event.metaKey && !event.ctrlKey && !event.altKey && !editable) {
203 event.preventDefault();
204 input.focus();
205 input.select();
206 }
207 });
208 })();
209
210 (function () {
211 var modal = document.querySelector("[data-pins-modal]");
212 if (!modal) return;
213
214 var openers = document.querySelectorAll("[data-pins-open]");
215 var closeButton = modal.querySelector("[data-pins-close]");
216 var filter = modal.querySelector("[data-pins-filter]");
217 var rows = Array.prototype.slice.call(modal.querySelectorAll("[data-pins-row]"));
218 var checkboxes = Array.prototype.slice.call(modal.querySelectorAll("[data-pins-checkbox]"));
219 var remaining = modal.querySelector("[data-pins-remaining]");
220 var submit = modal.querySelector("[data-pins-submit]");
221
222 function checkedCount() {
223 return checkboxes.filter(function (box) { return box.checked; }).length;
224 }
225
226 function refreshPins() {
227 var count = checkedCount();
228 var left = Math.max(0, 6 - count);
229 if (remaining) {
230 remaining.textContent = String(left);
231 remaining.parentElement.classList.toggle("is-full", left === 0);
232 }
233 if (submit) submit.disabled = count > 6;
234 checkboxes.forEach(function (box) {
235 box.disabled = !box.checked && count >= 6;
236 });
237 }
238
239 function applyFilter() {
240 if (!filter) return;
241 var query = filter.value.trim().toLowerCase();
242 rows.forEach(function (row) {
243 var haystack = (row.getAttribute("data-pins-search") || "").toLowerCase();
244 row.hidden = query !== "" && haystack.indexOf(query) === -1;
245 });
246 }
247
248 function openModal() {
249 modal.hidden = false;
250 document.body.classList.add("shithub-modal-open");
251 applyFilter();
252 refreshPins();
253 if (filter) filter.focus();
254 }
255
256 function closeModal() {
257 modal.hidden = true;
258 document.body.classList.remove("shithub-modal-open");
259 }
260
261 openers.forEach(function (button) {
262 button.addEventListener("click", openModal);
263 });
264 if (closeButton) closeButton.addEventListener("click", closeModal);
265 modal.addEventListener("click", function (event) {
266 if (event.target === modal) closeModal();
267 });
268 document.addEventListener("keydown", function (event) {
269 if (event.key === "Escape" && !modal.hidden) closeModal();
270 });
271 if (filter) filter.addEventListener("input", applyFilter);
272 checkboxes.forEach(function (box) {
273 box.addEventListener("change", refreshPins);
274 });
275 refreshPins();
276 })();
277
278 (function () {
279 var copyIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>';
280 var checkIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></svg>';
281
282 function fallbackCopy(text) {
283 var textarea = document.createElement("textarea");
284 textarea.value = text;
285 textarea.setAttribute("readonly", "");
286 textarea.style.position = "fixed";
287 textarea.style.top = "-1000px";
288 textarea.style.left = "-1000px";
289 document.body.appendChild(textarea);
290 textarea.select();
291 var ok = false;
292 try { ok = document.execCommand("copy"); } catch (e) { ok = false; }
293 document.body.removeChild(textarea);
294 return ok;
295 }
296
297 function copyText(text) {
298 if (navigator.clipboard && navigator.clipboard.writeText) {
299 return navigator.clipboard.writeText(text).then(function () {
300 return true;
301 }).catch(function () {
302 return fallbackCopy(text);
303 });
304 }
305 return Promise.resolve(fallbackCopy(text));
306 }
307
308 document.querySelectorAll(".markdown-body pre").forEach(function (pre) {
309 if (pre.closest(".shithub-markdown-codeblock")) return;
310 var parent = pre.parentNode;
311 if (!parent) return;
312
313 var wrapper = document.createElement("div");
314 wrapper.className = "shithub-markdown-codeblock";
315 parent.insertBefore(wrapper, pre);
316 wrapper.appendChild(pre);
317
318 var button = document.createElement("button");
319 button.type = "button";
320 button.className = "shithub-markdown-code-copy";
321 button.setAttribute("aria-label", "Copy code");
322 button.setAttribute("title", "Copy");
323 button.innerHTML = copyIcon;
324 wrapper.appendChild(button);
325
326 button.addEventListener("click", function () {
327 var target = pre.querySelector("code") || pre;
328 copyText(target.textContent || "").then(function (ok) {
329 button.classList.toggle("is-copied", ok);
330 button.innerHTML = ok ? checkIcon : copyIcon;
331 button.setAttribute("title", ok ? "Copied" : "Copy failed");
332 window.setTimeout(function () {
333 button.classList.remove("is-copied");
334 button.innerHTML = copyIcon;
335 button.setAttribute("title", "Copy");
336 }, 1400);
337 });
338 });
339 });
340 })();
341
342 (function () {
343 function slugify(text) {
344 return text.toLowerCase()
345 .trim()
346 .replace(/[^a-z0-9 _-]+/g, "")
347 .replace(/\s+/g, "-")
348 .replace(/-+/g, "-")
349 .replace(/^-|-$/g, "") || "heading";
350 }
351
352 document.querySelectorAll("[data-readme-outline]").forEach(function (root) {
353 var readme = root.closest(".shithub-readme");
354 var body = readme && readme.querySelector(".shithub-readme-body");
355 var list = root.querySelector("[data-readme-outline-list]");
356 var filter = root.querySelector("[data-readme-outline-filter]");
357 var empty = root.querySelector("[data-readme-outline-empty]");
358 var summary = root.querySelector("summary");
359 if (!body || !list) return;
360
361 var used = {};
362 body.querySelectorAll("[id]").forEach(function (element) {
363 if (element.id) used[element.id] = true;
364 });
365
366 var headings = Array.prototype.slice.call(body.querySelectorAll("h1, h2, h3, h4, h5, h6")).filter(function (heading) {
367 return (heading.textContent || "").trim() !== "";
368 });
369 if (headings.length === 0) return;
370
371 headings.forEach(function (heading) {
372 var text = (heading.textContent || "").trim().replace(/\s+/g, " ");
373 if (!heading.id) {
374 var base = slugify(text);
375 var id = base;
376 var index = 1;
377 while (used[id]) {
378 id = base + "-" + index;
379 index += 1;
380 }
381 heading.id = id;
382 used[id] = true;
383 }
384
385 var level = parseInt(heading.tagName.slice(1), 10) || 1;
386 var link = document.createElement("a");
387 link.href = "#" + heading.id;
388 link.className = "shithub-readme-outline-item is-depth-" + Math.min(level, 6);
389 link.textContent = text;
390 link.setAttribute("data-readme-outline-text", text.toLowerCase());
391 list.appendChild(link);
392 });
393
394 var links = Array.prototype.slice.call(list.querySelectorAll("a"));
395 function applyFilter() {
396 var query = filter ? filter.value.trim().toLowerCase() : "";
397 var shown = 0;
398 links.forEach(function (link) {
399 var matches = query === "" || (link.getAttribute("data-readme-outline-text") || "").indexOf(query) !== -1;
400 link.hidden = !matches;
401 if (matches) shown += 1;
402 });
403 if (empty) empty.hidden = shown !== 0;
404 }
405
406 root.hidden = false;
407 if (filter) filter.addEventListener("input", applyFilter);
408 root.addEventListener("toggle", function () {
409 if (root.open && filter) {
410 window.setTimeout(function () {
411 filter.focus();
412 filter.select();
413 }, 0);
414 }
415 });
416 list.addEventListener("click", function (event) {
417 if (event.target.closest("a")) root.open = false;
418 });
419 root.addEventListener("keydown", function (event) {
420 if (event.key === "Escape" && root.open) {
421 root.open = false;
422 if (summary) summary.focus();
423 }
424 });
425 document.addEventListener("click", function (event) {
426 if (root.open && !root.contains(event.target)) root.open = false;
427 });
428 applyFilter();
429 });
430 })();
431 </script>
432 </body>
433 </html>
434 {{- end }}