HTML · 7783 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 root = document.querySelector("[data-search-root]");
70 if (!root) return;
71 var input = root.querySelector("[data-search-input]");
72 var panel = root.querySelector("[data-search-results]");
73 if (!input || !panel || !window.fetch) return;
74
75 var timer = 0;
76 var controller = null;
77
78 function closePanel() {
79 panel.hidden = true;
80 panel.innerHTML = "";
81 input.setAttribute("aria-expanded", "false");
82 if (controller) {
83 controller.abort();
84 controller = null;
85 }
86 }
87
88 function showPanel(html) {
89 panel.innerHTML = html;
90 panel.hidden = false;
91 input.setAttribute("aria-expanded", "true");
92 }
93
94 function loadSuggestions() {
95 var query = input.value.trim();
96 if (query.length < 2) {
97 closePanel();
98 return;
99 }
100 if (controller) controller.abort();
101 controller = new AbortController();
102 fetch("/search/quick?q=" + encodeURIComponent(query), {
103 headers: { "X-Requested-With": "XMLHttpRequest" },
104 signal: controller.signal
105 }).then(function (response) {
106 if (response.status === 204) return "";
107 if (!response.ok) throw new Error("search quick failed");
108 return response.text();
109 }).then(function (html) {
110 if (!html) {
111 closePanel();
112 return;
113 }
114 showPanel(html);
115 }).catch(function (error) {
116 if (error.name !== "AbortError") closePanel();
117 });
118 }
119
120 function queueSuggestions() {
121 window.clearTimeout(timer);
122 timer = window.setTimeout(loadSuggestions, 160);
123 }
124
125 input.addEventListener("input", queueSuggestions);
126 input.addEventListener("focus", function () {
127 if (input.value.trim().length >= 2) queueSuggestions();
128 });
129 input.addEventListener("keydown", function (event) {
130 if (event.key === "Escape") {
131 closePanel();
132 input.blur();
133 }
134 });
135 root.addEventListener("submit", closePanel);
136 document.addEventListener("click", function (event) {
137 if (!root.contains(event.target)) closePanel();
138 });
139 document.addEventListener("keydown", function (event) {
140 var active = document.activeElement;
141 var editable = active && (
142 active.tagName === "INPUT" ||
143 active.tagName === "TEXTAREA" ||
144 active.isContentEditable
145 );
146 if (event.key === "/" && !event.metaKey && !event.ctrlKey && !event.altKey && !editable) {
147 event.preventDefault();
148 input.focus();
149 input.select();
150 }
151 });
152 })();
153
154 (function () {
155 var modal = document.querySelector("[data-pins-modal]");
156 if (!modal) return;
157
158 var openers = document.querySelectorAll("[data-pins-open]");
159 var closeButton = modal.querySelector("[data-pins-close]");
160 var filter = modal.querySelector("[data-pins-filter]");
161 var rows = Array.prototype.slice.call(modal.querySelectorAll("[data-pins-row]"));
162 var checkboxes = Array.prototype.slice.call(modal.querySelectorAll("[data-pins-checkbox]"));
163 var remaining = modal.querySelector("[data-pins-remaining]");
164 var submit = modal.querySelector("[data-pins-submit]");
165
166 function checkedCount() {
167 return checkboxes.filter(function (box) { return box.checked; }).length;
168 }
169
170 function refreshPins() {
171 var count = checkedCount();
172 var left = Math.max(0, 6 - count);
173 if (remaining) {
174 remaining.textContent = String(left);
175 remaining.parentElement.classList.toggle("is-full", left === 0);
176 }
177 if (submit) submit.disabled = count > 6;
178 checkboxes.forEach(function (box) {
179 box.disabled = !box.checked && count >= 6;
180 });
181 }
182
183 function applyFilter() {
184 if (!filter) return;
185 var query = filter.value.trim().toLowerCase();
186 rows.forEach(function (row) {
187 var haystack = (row.getAttribute("data-pins-search") || "").toLowerCase();
188 row.hidden = query !== "" && haystack.indexOf(query) === -1;
189 });
190 }
191
192 function openModal() {
193 modal.hidden = false;
194 document.body.classList.add("shithub-modal-open");
195 applyFilter();
196 refreshPins();
197 if (filter) filter.focus();
198 }
199
200 function closeModal() {
201 modal.hidden = true;
202 document.body.classList.remove("shithub-modal-open");
203 }
204
205 openers.forEach(function (button) {
206 button.addEventListener("click", openModal);
207 });
208 if (closeButton) closeButton.addEventListener("click", closeModal);
209 modal.addEventListener("click", function (event) {
210 if (event.target === modal) closeModal();
211 });
212 document.addEventListener("keydown", function (event) {
213 if (event.key === "Escape" && !modal.hidden) closeModal();
214 });
215 if (filter) filter.addEventListener("input", applyFilter);
216 checkboxes.forEach(function (box) {
217 box.addEventListener("change", refreshPins);
218 });
219 refreshPins();
220 })();
221 </script>
222 </body>
223 </html>
224 {{- end }}