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