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