Base.astro (8366B)
1 --- 2 import { ClientRouter } from 'astro:transitions'; 3 import Header from '../components/Header.astro'; 4 import Footer from '../components/Footer.astro'; 5 import Icon from '../components/Icon.astro'; 6 import SearchModal from '../components/SearchModal.astro'; 7 import { url } from '../lib/url'; 8 import '../styles/app.css'; 9 // Preload the two faces that paint above the fold — the wordmark in the 10 // masthead and the DM Mono the masthead/labels use — so the header does not flash 11 // its fallback on first paint. The rest load on demand via @font-face. 12 import notoWordmark from '../fonts/noto-sans-display-var-latin.woff2'; 13 import dmMono from '../fonts/dm-mono-400.woff2'; 14 15 interface Props { 16 title?: string; 17 description?: string; 18 railless?: boolean; 19 } 20 const { 21 title = 'DÆMON//SEC — Cyber & IT Cheatsheet Vault', 22 description = 'A curated, glassy vault of the best IT and cybersecurity cheatsheets — Active Directory, enumeration, exploitation, priv-esc, and more.', 23 } = Astro.props; 24 const canonical = new URL(Astro.url.pathname, Astro.site).href; 25 --- 26 <!doctype html> 27 <html lang="en" data-theme="light"> 28 <head> 29 <meta charset="utf-8" /> 30 <meta name="viewport" content="width=device-width, initial-scale=1" /> 31 <title>{title}</title> 32 <meta name="description" content={description} /> 33 <link rel="canonical" href={canonical} /> 34 <link rel="icon" href={url('favicon.svg')} type="image/svg+xml" /> 35 <link rel="preload" href={notoWordmark} as="font" type="font/woff2" crossorigin /> 36 <link rel="preload" href={dmMono} as="font" type="font/woff2" crossorigin /> 37 <meta name="theme-color" content="#191724" media="(prefers-color-scheme: dark)" /> 38 <meta name="theme-color" content="#faf4ed" media="(prefers-color-scheme: light)" /> 39 <meta property="og:type" content="website" /> 40 <meta property="og:title" content={title} /> 41 <meta property="og:description" content={description} /> 42 <meta property="og:image" content={url('og.svg')} /> 43 <meta name="twitter:card" content="summary_large_image" /> 44 <!-- Set theme before first paint to avoid FOUC --> 45 <script is:inline> 46 (function () { 47 function apply() { 48 try { 49 var t = localStorage.getItem('theme'); 50 // Dawn (cream) is the default, matching the main DÆMON site. 51 if (t !== 'light' && t !== 'dark') t = 'light'; 52 document.documentElement.setAttribute('data-theme', t); 53 document.documentElement.style.colorScheme = t; 54 } catch (e) { 55 document.documentElement.setAttribute('data-theme', 'light'); 56 } 57 } 58 59 apply(); 60 61 /* Every page ships `<html data-theme="light">` in its static markup, 62 because that is the default and the build has no way to know what 63 any given reader chose. On a view-transition navigation Astro 64 swaps in the new document — attributes on <html> included — so 65 that baked-in "light" overwrites the live attribute and the page 66 reverts to dawn mid-session. 67 68 This listener re-applies the stored choice on every swap. It runs 69 in `astro:after-swap`, which fires after the new document is in 70 place but before it is painted, so the correction never flashes. 71 72 The listener is registered once, from an inline head script that 73 executes on the first full load only — which is exactly why it 74 has to be `is:inline` and live here rather than in app.ts: a 75 bundled module re-imported per page would re-register it, and a 76 deferred one would run too late to beat the paint. */ 77 document.addEventListener('astro:after-swap', apply); 78 })(); 79 </script> 80 <!-- Page iris. Every internal navigation opens the next page as a 81 circle from the link that was pressed: the theme wipe's gesture, 82 at page pace. The CSS is beside the theme wipe in global.css; 83 this only supplies the origin and the end radius. --> 84 <script is:inline> 85 (function () { 86 function centre(el) { 87 var r = el.getBoundingClientRect(); 88 return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; 89 } 90 91 /* Where the last click landed. Capture phase, so it is recorded 92 before the ClientRouter's own listener takes the click. A click 93 with detail 0 came from the keyboard and has no pointer 94 position, so it opens from the middle of the control. */ 95 var press = null; 96 document.addEventListener('click', function (e) { 97 var at = e.detail === 0 && e.target instanceof Element 98 ? centre(e.target) 99 : { x: e.clientX, y: e.clientY }; 100 at.t = Date.now(); 101 press = at; 102 }, true); 103 104 var origin = null; 105 function paint() { 106 if (!origin) return; 107 var s = document.documentElement.style; 108 s.setProperty('--vt-x', origin.x + 'px'); 109 s.setProperty('--vt-y', origin.y + 'px'); 110 s.setProperty('--vt-r', origin.r + 'px'); 111 } 112 113 /* A press older than a second did not cause this navigation (back, 114 forward, a scripted one), so it falls back to the link Astro 115 reports, then to the middle of the screen. The radius reaches 116 the farthest corner exactly, so the circle finishes as it 117 covers the page rather than part-way through an overshoot. */ 118 document.addEventListener('astro:before-preparation', function (e) { 119 var at = press && Date.now() - press.t < 1000 ? press 120 : e.sourceElement instanceof Element ? centre(e.sourceElement) 121 : { x: innerWidth / 2, y: innerHeight / 2 }; 122 origin = { 123 x: at.x, 124 y: at.y, 125 r: Math.hypot(Math.max(at.x, innerWidth - at.x), Math.max(at.y, innerHeight - at.y)), 126 }; 127 paint(); 128 }); 129 130 /* The swap replaces <html>'s attributes with the new page's, and 131 the inline style carrying these three goes with them, so they 132 are put back before the new page is painted, for the same 133 reason as the theme note above. */ 134 document.addEventListener('astro:after-swap', paint); 135 })(); 136 </script> 137 <ClientRouter /> 138 </head> 139 <body> 140 <a class="skip-link" href="#main">Skip to content</a> 141 <!-- Shared arrowhead for every inline-SVG flowchart (src/styles/flow.css). 142 Defined once here so a diagram only has to reference `url(#flow-arrow)`. 143 `context-stroke` makes the head take each edge's own stroke colour, so 144 one marker serves foam forward edges and love back-edges alike. --> 145 <svg width="0" height="0" aria-hidden="true" style="position:absolute" focusable="false"> 146 <defs> 147 <marker id="flow-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" 148 markerHeight="7" orient="auto-start-reverse" markerUnits="userSpaceOnUse"> 149 <path class="flow-arrow" d="M0,0 L10,5 L0,10 z" fill="context-stroke" /> 150 </marker> 151 </defs> 152 </svg> 153 <!-- The film over the whole page. It is what keeps a flat colour 154 reading as stock rather than as a swatch, and it stands in for the 155 texture the old glass build got from blur. --> 156 <span class="grain" aria-hidden="true"></span> 157 <Header /> 158 <main id="main" tabindex="-1"> 159 <slot /> 160 </main> 161 <Footer /> 162 <!-- Sheets run to a couple of thousand lines; without this the only way 163 back to the header is a long scroll. Hidden until app.ts marks it 164 visible, so it never shows on a page short enough not to need it. --> 165 <button id="to-top" class="to-top" type="button" hidden aria-label="Back to top"> 166 <Icon name="chevron" /> 167 </button> 168 <SearchModal /> 169 <script> 170 import '../scripts/app.ts'; 171 </script> 172 <script> 173 // The effects shared with daemon-sec.xyz (src/scripts/dmn): the cursor 174 // pill and the sheet image viewer. Re-bound after each navigation. 175 import { initDmn } from '../scripts/dmn/init'; 176 document.addEventListener('astro:page-load', initDmn); 177 if (document.readyState !== 'loading') initDmn(); 178 else document.addEventListener('DOMContentLoaded', initDmn, { once: true }); 179 </script> 180 </body> 181 </html>