// app.js — config-driven dash with strong icon fallback, basePath, // robust grid spanning, and full set of knobs const elApp = document.getElementById('app'); const elSearch = document.getElementById('search'); let DEFAULT_ICON_SCALE = 1; let ICON_BOX_PCT = '85%'; let FALLBACK_ICON = ''; let ICON_BASE = ''; // NEW: optional basePath for icons let BUILTIN_FALLBACK = // SVG data-URI fallback if everything fails 'data:image/svg+xml;utf8,' + encodeURIComponent(` `); /* Focus search on "/" */ window.addEventListener('keydown', (e) => { if (e.key === '/' && document.activeElement !== elSearch) { e.preventDefault(); elSearch?.focus(); elSearch?.select(); } }); /* Load config */ fetch('config.json', { cache: 'no-store' }) .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }) .then(cfg => { // knobs DEFAULT_ICON_SCALE = cfg?.icons?.defaultScale ?? 1; ICON_BOX_PCT = cfg?.icons?.boxPercent ?? '85%'; FALLBACK_ICON = cfg?.icons?.fallbackIcon || ''; ICON_BASE = cfg?.icons?.basePath || ''; applyHeader(cfg); applyTheme(cfg); applyTypography(cfg); applyLayout(cfg); applyIcons(cfg); preloadIcons(cfg); document.title = (cfg.title || 'Home') + ' • Home'; (cfg.sections || []).forEach((sec, idx) => { const node = renderSection(cfg, sec, idx); elApp.appendChild(node); }); attachIconFallback(); // after DOM is built attachSearch(); // Re-apply spanning on resize (Firefox sometimes needs this) window.addEventListener('resize', () => { document.querySelectorAll('.grid').forEach(g => applyStretching(g)); }); (window.requestIdleCallback ? requestIdleCallback(checkAll) : setTimeout(checkAll, 200)); }) .catch(err => { elApp.innerHTML = `
Failed to load config.json: ${escapeHtml(err.message)}
`; }); /* --------------------- Header / Meta / Favicons --------------------- */ function applyHeader(cfg){ const h = cfg.header || {}; const titleEl = document.querySelector('.titles h1'); const subEl = document.querySelector('.titles p'); const logoEl = document.querySelector('.logo img'); const searchWrap = document.querySelector('.search'); const searchInput = document.getElementById('search'); if (titleEl) titleEl.textContent = cfg.title || 'Home'; if (subEl) subEl.textContent = cfg.subtitle || ''; if (logoEl && h.logo) logoEl.src = h.logo; document.documentElement.style.setProperty('--topbar-bg', h.background || '#101010'); if (searchWrap) searchWrap.style.display = h.showSearch === false ? 'none' : ''; if (searchInput && h.searchPlaceholder) searchInput.placeholder = h.searchPlaceholder; // theme-color meta setOrCreateMeta('meta-theme-color', 'theme-color', h.background || '#101010'); // Open Graph (title/image) setOrCreateMeta('og-title', 'og:title', cfg.title || 'MakeArmy', true); setOrCreateMeta('og-site_name', 'og:site_name', cfg.title || 'MakeArmy', true); if (cfg.icons?.ogImage) setOrCreateMeta('og-image', 'og:image', cfg.icons.ogImage, true); } function applyIcons(cfg){ const ico = cfg.icons || {}; if (ico.faviconPng) setOrCreateFavicon('favicon-png', 'image/png', ico.faviconPng); if (ico.faviconIco) setOrCreateFavicon('favicon-ico', '', ico.faviconIco); } function setOrCreateFavicon(id, type, href){ let link = document.getElementById(id); if (!link) { link = document.createElement('link'); link.id = id; link.rel = 'icon'; if (type) link.type = type; document.head.appendChild(link); } link.href = href; } function setOrCreateMeta(id, name, content, isOg=false){ let meta = document.getElementById(id); if (!meta) { meta = document.createElement('meta'); meta.id = id; if (isOg) meta.setAttribute('property', name); else meta.setAttribute('name', name); document.head.appendChild(meta); } meta.setAttribute('content', content); } /* --------------------- Theme / Layout / Typography --------------------- */ function applyTheme(cfg){ const t = cfg.theme || {}; setVar('--bg0', t.background || '#202020'); setVar('--panel', t.panel || '#121212'); setVar('--tile', t.tile || '#202020'); setVar('--ink', t.ink || '#f2f2f2'); setVar('--ink-dim',t.inkDim || '#a9a9a9'); const acc = t.accents || {}; const A = { blue: acc.blue || '#3a85ff', green: acc.green || '#1fc66e', red: acc.red || '#ff3f5d', yellow: acc.yellow || '#ffdb33' }; setVar('--blue', A.blue); setVar('--green', A.green); setVar('--red', A.red); setVar('--yellow', A.yellow); // RGB components for glow rgba() const [br,bg,bb] = hexToRgb(A.blue); const [gr,gg,gb] = hexToRgb(A.green); const [rr,rg,rb] = hexToRgb(A.red); const [yr,yg,yb] = hexToRgb(A.yellow); setVar('--blue-rgb', `${br}, ${bg}, ${bb}`); setVar('--green-rgb', `${gr}, ${gg}, ${gb}`); setVar('--red-rgb', `${rr}, ${rg}, ${rb}`); setVar('--yellow-rgb', `${yr}, ${yg}, ${yb}`); const g = t.glow || {}; setVar('--glow-core', `${num(g.corePx,3)}px`); setVar('--glow-blur', `${num(g.blurPx,7)}px`); setVar('--glow-core-a', num(g.coreAlpha,0.95)); setVar('--glow-outer-a', num(g.outerAlpha,0.64)); } function applyTypography(cfg){ const T = cfg.typography || {}; setVar('--section-title-px', num(T.sectionTitlePx, 22)); setVar('--item-label-px', num(T.itemLabelPx, 12)); } // helper for px values function px(v, d){ return `${num(v, d)}px`; } function applyLayout(cfg){ const L = cfg.layout || {}; // keep unitless; CSS uses calc(var(--page-max) * 1px) setVar('--page-max', num(L.pageMax, 2400)); // spacing + dimensions in px setVar('--gap-sec', px(L.gapSections, 14)); setVar('--gap-item', px(L.gapItems, 10)); setVar('--section-min', px(L.sectionMin, 300)); setVar('--section-basis', px(L.sectionBasis, 320)); setVar('--section-max', px(L.sectionMax, 340)); setVar('--tile-h', px(L.tileHeight, 62)); // grid knobs (integers) setVar('--grid-cols', String(num(L.gridCols, 6))); setVar('--tile-colspan', String(num(L.tileColspan, 2))); // icon box percent setVar('--icon-box-pct', ICON_BOX_PCT); } /* --------------------- Render Sections / Items --------------------- */ function renderSection(cfg, section, idx){ const sec = document.createElement('section'); sec.className = 'section'; // Accent: number 1..4 uses global vars; hex sets custom per-section vars let accentIndex = ((idx % 4) + 1); if (typeof section.accent === 'number') { accentIndex = Math.max(1, Math.min(4, section.accent)); sec.dataset.accent = String(accentIndex); } else if (typeof section.accent === 'string' && section.accent.trim().startsWith('#')) { const hex = section.accent.trim(); sec.classList.add('custom-accent'); sec.style.setProperty('--accent', hex); const [r,g,b] = hexToRgb(hex); sec.style.setProperty('--accent-rgb', `${r}, ${g}, ${b}`); } else { sec.dataset.accent = String(accentIndex); } sec.innerHTML = `