1/* ============================================================ 2 EXPLAINLY â app shell 3 Shared across all app pages: tweaks, starfield, theme, toast 4 ============================================================ */ 5(function () { 6 'use strict'; 7 8 var TWEAK_STORE = 'explainly-app-tweaks'; 9 var tweaks = loadTweaks(); 10 11 function loadTweaks() { 12 var def = { theme: 'dark', accent: 'indigo', font: 'humanist', card: 'elevated', radius: 'rounded' }; 13 var saved = {}; 14 try { saved = JSON.parse(localStorage.getItem(TWEAK_STORE) || '{}'); } catch (e) {} 15 return Object.assign({}, def, saved); 16 } 17 18 function applyTweaks() { 19 var root = document.documentElement; 20 root.setAttribute('data-theme', tweaks.theme); 21 root.setAttribute('data-accent', tweaks.accent); 22 root.setAttribute('data-font', tweaks.font); 23 root.setAttribute('data-card', tweaks.card); 24 root.setAttribute('data-radius', tweaks.radius); 25 swapLogos(tweaks.accent); 26 } 27 28 function setTweak(key, val) { 29 tweaks[key] = val; 30 try { localStorage.setItem(TWEAK_STORE, JSON.stringify(tweaks)); } catch (e) {} 31 applyTweaks(); 32 syncTweakUI(); 33 } 34 35 function swapLogos(accent) { 36 document.querySelectorAll('.brand-logo').forEach(function (img) { 37 img.src = '/icons/logo-' + accent + '.png'; 38 }); 39 } 40 41 // Apply immediately to avoid theme flash 42 applyTweaks(); 43 44 /* ---- Toast (global) ---- */ 45 var _toastTimer; 46 window.showToast = function (msg) { 47 var el = document.getElementById('toast'); 48 if (!el) return; 49 el.textContent = msg; 50 el.classList.add('show'); 51 clearTimeout(_toastTimer); 52 _toastTimer = setTimeout(function () { el.classList.remove('show'); }, 2200); 53 }; 54 55 /* ---- DOM ready ---- */ 56 document.addEventListener('DOMContentLoaded', function () { 57 58 // Theme toggles 59 function toggleTheme() { setTweak('theme', tweaks.theme === 'light' ? 'dark' : 'light'); } 60 var thSide = document.getElementById('theme-side'); 61 var thTop = document.getElementById('theme-top'); 62 if (thSide) thSide.addEventListener('click', toggleTheme); 63 if (thTop) thTop.addEventListener('click', toggleTheme); 64 65 // Populate sidebar user info from auth helpers 66 var email = (typeof getEmail === 'function') ? getEmail() : null; 67 if (email) { 68 var initial = email[0].toUpperCase(); 69 var el; 70 el = document.getElementById('side-email'); if (el) el.textContent = email; 71 el = document.getElementById('side-name'); if (el) el.textContent = email.split('@')[0]; 72 el = document.getElementById('side-avatar'); if (el) el.textContent = initial; 73 el = document.getElementById('top-avatar'); if (el) el.textContent = initial; 74 } 75 76 // Open tweaks button 77 var openBtn = document.getElementById('open-tweaks-btn'); 78 if (openBtn) openBtn.addEventListener('click', openPanel); 79 80 // Tweaks panel (only if container exists) 81 if (document.getElementById('tweaks-root')) buildPanel(); 82 83 // Starfield 84 startStarfield(); 85 86 // Scroll reveal 87 if (typeof IntersectionObserver !== 'undefined') { 88 var obs = new IntersectionObserver(function (entries) { 89 entries.forEach(function (e) { if (e.isIntersecting) e.target.classList.add('in'); }); 90 }, { threshold: 0.1 }); 91 document.querySelectorAll('.reveal').forEach(function (el) { obs.observe(el); }); 92 } 93 }); 94 95 /* ---- Starfield ---- */ 96 function startStarfield() { 97 var canvas = document.getElementById('starfield'); 98 if (!canvas) return; 99 var ctx = canvas.getContext('2d'); 100 var root = document.documentElement; 101 var reduce = matchMedia('(prefers-reduced-motion:reduce)').matches; 102 var w, h, dpr, stars = [], shooters = []; 103 104 function getAccent() { 105 return getComputedStyle(root).getPropertyValue('--a-glow').trim() || '#818cf8'; 106 } 107 function hexToRgb() { 108 var hex = getAccent().replace('#', ''); 109 if (hex.length === 3) hex = hex.split('').map(function (x) { return x + x; }).join(''); 110 var n = parseInt(hex, 16); 111 return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; 112 } 113 var col = hexToRgb(); 114 115 function resize() { 116 dpr = Math.min(devicePixelRatio || 1, 2); 117 w = canvas.width = innerWidth * dpr;
118 h = canvas.height = innerHeight * dpr; 119 canvas.style.width = innerWidth + 'px'; 120 canvas.style.height = innerHeight + 'px'; 121 var count = Math.min(720, Math.round(innerWidth * innerHeight / 2800)); 122 stars = Array.from({ length: count }, function () { 123 return { 124 x: Math.random() * w, y: Math.random() * h, 125 z: Math.random() * 0.8 + 0.2, 126 r: (Math.random() * 1.2 + 0.3) * dpr, 127 tw: Math.random() * Math.PI * 2, ts: Math.random() * 0.04 + 0.01, 128 }; 129 }); 130 } 131 132 function spawn() { 133 if (reduce) return; 134 var fl = Math.random() > 0.5; 135 shooters.push({ 136 x: fl ? -50 : w + 50, y: Math.random() * h * 0.5, 137 vx: (fl ? 1 : -1) * (5 + Math.random() * 4) * dpr, 138 vy: (1.6 + Math.random() * 1.2) * dpr, life: 1, 139 }); 140 } 141 142 var last = 0; 143 function frame(t) { 144 ctx.clearRect(0, 0, w, h); 145 var r = col[0], g = col[1], b = col[2]; 146 var light = root.getAttribute('data-theme') === 'light'; 147 for (var i = 0; i < stars.length; i++) { 148 var s = stars[i]; 149 s.tw += s.ts; 150 var a = (light ? 0.55 + Math.sin(s.tw) * 0.25 : 0.35 + Math.sin(s.tw) * 0.3) * s.z; 151 ctx.beginPath(); 152 ctx.fillStyle = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; 153 ctx.arc(s.x, s.y, s.r, 0, 7); 154 ctx.fill(); 155 s.y += s.z * 0.1 * dpr; 156 if (s.y > h) { s.y = 0; s.x = Math.random() * w; } 157 } 158 for (var j = shooters.length - 1; j >= 0; j--) { 159 var sh = shooters[j]; 160 sh.x += sh.vx; sh.y += sh.vy; sh.life -= 0.012; 161 var grad = ctx.createLinearGradient(sh.x, sh.y, sh.x - sh.vx * 6, sh.y - sh.vy * 6); 162 grad.addColorStop(0, 'rgba(' + r + ',' + g + ',' + b + ',' + (0.9 * sh.life) + ')'); 163 grad.addColorStop(1, 'rgba(0,0,0,0)'); 164 ctx.strokeStyle = grad; ctx.lineWidth = 2 * dpr; ctx.lineCap = 'round'; 165 ctx.beginPath(); ctx.moveTo(sh.x, sh.y); ctx.lineTo(sh.x - sh.vx * 6, sh.y - sh.vy * 6); ctx.stroke(); 166 if (sh.life <= 0 || sh.x < -80 || sh.x > w + 80) shooters.splice(j, 1); 167 } 168 if (t - last > 4600 && Math.random() > 0.6) { spawn(); last = t; } 169 requestAnimationFrame(frame); 170 } 171 172 resize(); 173 addEventListener('resize', resize); 174 new MutationObserver(function () { col = hexToRgb(); }) 175 .observe(root, { attributes: true, attributeFilter: ['data-accent'] }); 176 requestAnimationFrame(frame); 177 } 178 179 /* ---- Tweaks panel ---- */ 180 var ACCENT_SW = { indigo: '#6366f1', navy: '#3b82f6', slate: '#5b9bd5', abyss: '#22d3ee', forest: '#22c55e' }; 181 182 function buildPanel() { 183 var panelCSS = [ 184 '#tw-panel{position:fixed;right:18px;bottom:18px;z-index:90;width:300px;max-width:calc(100vw - 36px);', 185 'border-radius:18px;background:var(--glass);border:1px solid var(--line-2);', 186 'backdrop-filter:blur(18px) saturate(150%);-webkit-backdrop-filter:blur(18px) saturate(150%);', 187 'box-shadow:var(--shadow);font-family:var(--body);color:var(--ink);display:none;overflow:hidden}', 188 '#tw-panel.show{display:block}', 189 '.tw-head{display:flex;align-items:center;gap:9px;padding:14px 16px;border-bottom:1px solid var(--line);cursor:grab}', 190 '.tw-head b{font-family:var(--display);font-size:14px;font-weight:600}', 191 '.tw-head .tw-dot{width:8px;height:8px;border-radius:50%;background:linear-gradient(var(--a1),var(--a2))}', 192 '.tw-head .tw-x{margin-left:auto;display:grid;place-items:center;width:26px;height:26px;border-radius:8px;color:var(--ink-3)}',
193 '.tw-head .tw-x:hover{background:var(--surface-2);color:var(--ink)}', 194 '.tw-head .tw-x svg{width:15px;height:15px}', 195 '.tw-body{padding:6px 16px 16px;max-height:min(70vh,520px);overflow-y:auto}', 196 '.tw-sec{font-family:var(--mono);font-size:10px;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-3);margin:16px 0 9px}', 197 '.tw-seg{display:flex;gap:5px;flex-wrap:wrap}', 198 '.tw-seg button{flex:1;min-width:fit-content;padding:8px 10px;border-radius:10px;border:1px solid var(--line);', 199 'background:var(--surface);color:var(--ink-2);font-size:12.5px;font-weight:600;transition:.18s;white-space:nowrap}', 200 '.tw-seg button:hover{border-color:var(--line-2);color:var(--ink)}', 201 '.tw-seg button.on{color:#fff;background:linear-gradient(120deg,var(--a1),var(--a2));border-color:transparent}', 202 '.tw-swatches{display:flex;gap:9px;flex-wrap:wrap}', 203 '.tw-sw{width:38px;height:38px;border-radius:11px;border:2px solid transparent;cursor:pointer;position:relative;transition:.18s}', 204 '.tw-sw:hover{transform:translateY(-2px)}', 205 '.tw-sw.on{border-color:var(--ink)}', 206 '.tw-sw.on::after{content:"";position:absolute;inset:0;border-radius:9px;box-shadow:0 0 0 2px var(--bg) inset}', 207 ].join(''); 208 var st = document.createElement('style'); 209 st.textContent = panelCSS; 210 document.head.appendChild(st); 211 212 function seg(label, key, opts) { 213 return '<div class="tw-sec">' + label + '</div>' + 214 '<div class="tw-seg" data-key="' + key + '">' + 215 opts.map(function (o) { return '<button data-v="' + o.v + '">' + o.l + '</button>'; }).join('') + 216 '</div>'; 217 } 218 219 var swatches = Object.keys(ACCENT_SW).map(function (a) { 220 return '<button class="tw-sw" data-v="' + a + '" style="background:' + ACCENT_SW[a] + '" aria-label="' + a + '"></button>'; 221 }).join(''); 222 223 document.getElementById('tweaks-root').innerHTML = 224 '<div id="tw-panel">' + 225 '<div class="tw-head" id="tw-head"><span class="tw-dot"></span><b>Tweaks</b>' + 226 '<button class="tw-x" id="tw-x" aria-label="Close">' + 227 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"><path d="M18 6 6 18M6 6l12 12"/></svg>' + 228 '</button>' + 229 '</div>' + 230 '<div class="tw-body">' + 231 seg('Theme', 'theme', [{ v: 'dark', l: 'Dark' }, { v: 'light', l: 'Light' }]) + 232 '<div class="tw-sec">Accent</div>' + 233 '<div class="tw-swatches" data-key="accent">' + swatches + '</div>' + 234 seg('Display font', 'font', [{ v: 'techy', l: 'Techy' }, { v: 'humanist', l: 'Humanist' }, { v: 'rounded', l: 'Rounded' }]) + 235 seg('Card style', 'card', [{ v: 'elevated', l: 'Elevated' }, { v: 'outline', l: 'Outline' }, { v: 'glass', l: 'Glass' }]) + 236 seg('Corner radius', 'radius', [{ v: 'sharp', l: 'Sharp' }, { v: 'rounded', l: 'Rounded' }, { v: 'soft', l: 'Soft' }]) + 237 '</div>' + 238 '</div>'; 239 240 document.querySelectorAll('#tw-panel .tw-seg').forEach(function (g) { 241 g.addEventListener('click', function (e) { 242 var b = e.target.closest('button'); if (!b) return; 243 setTweak(g.dataset.key, b.dataset.v); 244 }); 245 }); 246 document.querySelector('#tw-panel .tw-swatches').addEventListener('click', function (e) { 247 var b = e.target.closest('button'); if (!b) return; 248 setTweak('accent', b.dataset.v); 249 }); 250 document.getElementById('tw-x').addEventListener('click', dismissPanel); 251 makeDraggable(document.getElementById('tw-panel'), document.getElementById('tw-head')); 252 syncTweakUI(); 253 } 254 255 function syncTweakUI() { 256 document.querySelectorAll('#tw-panel .tw-seg').forEach(function (g) { 257 var k = g.dataset.key; 258 g.querySelectorAll('button').forEach(function (b) { 259 b.classList.toggle('on', b.dataset.v === tweaks[k]); 260 }); 261 }); 262 document.querySelectorAll('#tw-panel .tw-sw').forEach(function (b) { 263 b.classList.toggle('on', b.dataset.v === tweaks.accent); 264 }); 265 } 266 267 function openPanel() { var p = document.getElementById('tw-panel'); if (p) p.classList.add('show'); } 268 function dismissPanel() { var p = document.getElementById('tw-panel'); if (p) p.classList.remove('show'); } 269 270 function makeDraggable(panel, handle) { 271 var sx, sy, ox, oy, drag = false;
272 handle.addEventListener('pointerdown', function (e) { 273 if (e.target.closest('.tw-x')) return; 274 drag = true; handle.setPointerCapture(e.pointerId); 275 var r = panel.getBoundingClientRect(); 276 sx = e.clientX; sy = e.clientY; ox = r.left; oy = r.top; 277 panel.style.transition = 'none'; 278 }); 279 handle.addEventListener('pointermove', function (e) { 280 if (!drag) return; 281 var nx = Math.max(8, Math.min(innerWidth - panel.offsetWidth - 8, ox + e.clientX - sx)); 282 var ny = Math.max(8, Math.min(innerHeight - 60, oy + e.clientY - sy)); 283 panel.style.left = nx + 'px'; panel.style.top = ny + 'px'; 284 panel.style.right = 'auto'; panel.style.bottom = 'auto'; 285 }); 286 handle.addEventListener('pointerup', function () { drag = false; }); 287 } 288 289 // Host protocol 290 window.addEventListener('message', function (e) { 291 var t = e && e.data && e.data.type; 292 if (t === '__activate_edit_mode') openPanel(); 293 if (t === '__deactivate_edit_mode') dismissPanel(); 294 }); 295 try { window.parent.postMessage({ type: '__edit_mode_available' }, '*'); } catch (e) {} 296})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.