PageSourceSearch

https://explainly-ai.netlify.app/js/app-shell.js

js explainly-ai.netlify.app collected 2026-10-03 10:43:07 UTC 13,247 bytes, 296 lines download raw bytes

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.