commit dd314435bdbfcae43af94a90148df61ba97cf359 Author: makearmy Date: Sun Aug 24 09:39:05 2025 -0400 Initial import diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e29d83 --- /dev/null +++ b/README.md @@ -0,0 +1,244 @@ +# MakeArmy Static Dash + +Ultra-fast, no-framework homepage for curating links/tools with a **strict 3-column grid**, **config-driven theming**, and **crisp icon tiles**. Works on any static host. + +--- + +## Quick start + +```bash +# put these files in a folder +index.html +styles.css +app.js +config.json +images/ma-icon.png # header logo +images/favicon.png|ico # favicon(s) +icons/*.svg|png # item icons + +# serve locally (choose one) +python3 -m http.server 8080 +# or +npx live-server --port=8080 + + Open http://localhost:8080 + + Press / to focus Search. Use Tab to keyboard-navigate tiles (visible focus rings). + + All content/layout is driven by config.json. + +File structure + +/ +├─ index.html # markup shell; content is injected at runtime +├─ styles.css # styles (theme via CSS variables) +├─ app.js # reads config.json, builds UI, handles grid logic +├─ config.json # your data, colors, layout, icons +├─ images/ +│ ├─ ma-icon.png +│ └─ favicon.png (or .ico) +└─ icons/ + ├─ db-f.png + ├─ privatebin.svg + └─ ... (all tile icons) + +Config guide (config.json) + + JSON must be valid: double-quoted keys/strings, no comments, no trailing commas. + +Top level +Key Type Description Default +title string Big monospace header and "Home" +subtitle string Small italic tagline under the title "" +header +Key Type Description Default +logo string (URL/path) Header square logo "/images/ma-icon.png" +background color Full-bleed header color + <meta name="theme-color"> "#101010" +searchPlaceholder string Search input placeholder "Start typing to filter" +showSearch boolean Hide/show search true +icons + +Centralizes icon handling; item icons use filenames only and are prefixed by basePath. +Key Type Description Default +basePath string Prefix for all item icons (e.g., "/icons/") "" (none) +fallbackIcon string If an icon 404s, swap to this file; if empty, a built-in SVG fallback is used "" +defaultScale number Per-item default icon_scale multiplier 1 +boxPercent string Icon box size inside tile (e.g., "85%") "85%" +preloadTopN number Preload up to N icons for faster first paint 16 +faviconPng string PNG favicon "/images/favicon.png" +faviconIco string ICO favicon "/images/favicon.ico" +ogImage string Social preview image header.logo +typography +Key Type Description Default +sectionTitlePx number Section header font size (monospace) 22 +itemLabelPx number Tile label font size (bold sans) 12 +theme + +Base greys, pastel accents, and hover glow tuning. +Key Type Description Default +background color Page background #202020 +panel color Section surface #121212 +tile color Tile surface #202020 +ink color Primary text #f2f2f2 +inkDim color Muted text #a9a9a9 + +theme.accents (used when a section’s accent is 1..4): + + blue: "#3a85ff", green: "#1fc66e", red: "#ff3f5d", yellow: "#ffdb33" + +theme.glow (tile hover underglow): + + corePx (default 3) – thin inner highlight radius + + blurPx (default 7) – small outer halo (spread) + + coreAlpha (default 0.95) – inner brightness (0–1) + + outerAlpha (default 0.64) – outer brightness (0–1) + +layout + +Strict, compact 3-across layout using a 6-track grid under the hood. +Key Type Description Default +pageMax number Content wrap width (header is full-bleed) 2400 +gapSections number Space between sections (px) 14 +gapItems number Space between tiles (px) 10 +gridCols number Internal grid tracks per section 6 +tileColspan number Default tile span 2 +sectionMin number Min section width (px) 300 +sectionBasis number Preferred section width (px) 320 +sectionMax number Max section width (px) 340 +tileHeight number Fixed tile row height (px) 62 + +Last-row behavior + + 1 leftover → tile spans all tracks (full width) + + 2 leftovers → each spans half width + + 3 or multiples of 3 → normal rows + +a11y + + reducedMotion (bool) – advisory; UI also respects OS prefs via prefers-reduced-motion. + + focusOutline (bool) – advisory; visible focus rings are enabled. + +sections + +Array of section cards with a title, accent, and items. + +Section fields +Key Type Description +title string Text in the colored bar +accent number | string 1..4 uses theme.accents; hex string (e.g., "#8a6cff") for custom + +Item fields +Key Type Description +label string Tile title (white, not underlined) +href string Destination URL +note string Optional subtitle (currently hidden in CSS; enable if desired) +icon string Filename (e.g., "db-f.png") – combined with icons.basePath +icon_scale number Per-icon scale to offset canvas padding (e.g., 1.15) +icon_class string Extra CSS class for targeting +icon_svg string Inline raw SVG string (advanced) +target string _self or _blank +check string URL to ping with <img> for “up/down” dot (should return any image; 4.5s timeout) +Minimal example + +{ + "title": "MakeArmy", + "header": { "logo": "/images/ma-icon.png", "background": "#101010", "showSearch": true }, + "icons": { "basePath": "/icons/", "preloadTopN": 16 }, + "theme": { "background": "#202020", "panel": "#121212", "tile": "#202020" }, + "layout": { "pageMax": 2400, "gridCols": 6, "tileColspan": 2, "tileHeight": 62 }, + "sections": [ + { + "title": "Utilities", + "accent": 1, + "items": [ + { "label": "PrivateBin", "href": "https://paste.example.com", "icon": "privatebin.svg", "target": "_blank" }, + { "label": "Images", "href": "https://img.example.com", "icon": "picsur.png", "target": "_blank" }, + { "label": "Docs", "href": "https://docs.example.com", "icon": "docs.svg" } + ] + } + ] +} + +Theming quickies + + Pastels: nudge theme.accents.* lighter/desaturated. + + Glow: adjust theme.glow.blurPx (spread) and coreAlpha/outerAlpha (intensity). + + Density: tweak layout.tileHeight, gapItems, gapSections. + + Text: bump typography.itemLabelPx or sectionTitlePx. + +Performance tips + + Prefer SVG icons; for PNG, export at 2× with transparent background. + + Keep icons.preloadTopN around 8–24. + + Cache /icons/ aggressively: Cache-Control: public, max-age=31536000, immutable. + + Keep app.js as defer (already set in index.html). + +Deploying + +Any static host: NGINX, Caddy, Apache, GitHub Pages, Cloudflare Pages, Netlify, S3 + CloudFront. + +Headers to consider + + Correct Content-Type for SVG: image/svg+xml. + + Cache icons long-term; keep config.json lightly cached while iterating. + +Keyboard & A11y + + / focuses search. + + Tab to navigate; tiles show visible outlines in section accent color. + + Reduced motion respected via prefers-reduced-motion. + +Troubleshooting + +“Failed to load config.json: JSON.parse …” + + JSON is strict: no comments, no trailing commas, double-quoted keys/strings only. + +Icons missing + + Confirm icons.basePath (e.g., "/icons/"). + + Hard reload (Ctrl/Cmd-Shift-R). + + Try opening https://your.site/icons/foo.png directly. + + Set icons.fallbackIcon (e.g., "fallback.svg"). + +Rows/columns odd or last row wraps + + Keep layout.gridCols: 6, tileColspan: 2. + + Ensure layout numbers are plain numbers (app adds px). + +Tiles too tall/short + + Adjust layout.tileHeight and/or typography.itemLabelPx. + +Status dot always ‘down’ + + check must return an image (some endpoints block image hotlinking). Use a dedicated “status pixel”. + +Contributing / Maintaining + + Add/remove items by editing sections[*].items in config.json. Order matters. + + Move objects inside the sections array to change section order. + + Set section accent to 1..4 (uses theme palette) or a custom hex (e.g., "#8a6cff"). + + To centralize icons, keep filenames in items and set "icons.basePath": "/icons/". diff --git a/app.js b/app.js new file mode 100644 index 0000000..5649c4c --- /dev/null +++ b/app.js @@ -0,0 +1,373 @@ +// 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(` +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> +<rect width="64" height="64" rx="8" fill="#2a2a2a"/> +<path d="M10 46l10-12 7 8 9-12 18 24H10z" fill="#bdbdbd"/> +<circle cx="24" cy="22" r="4" fill="#e0e0e0"/> +</svg> +`); + +/* 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 = `<p style="color:#ff6b6b">Failed to load config.json: ${escapeHtml(err.message)}</p>`; +}); + +/* --------------------- 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 = ` + <div class="head"> + <span>▸ ${escapeHtml(section.title || 'Section')}</span> + <span class="gear" aria-hidden="true">⋮</span> + </div> + <div class="grid"></div> + `; + const grid = sec.querySelector('.grid'); + + (section.items || []).forEach(item => { + const a = document.createElement('a'); + a.className = 'tile'; + a.href = item.href || '#'; + a.rel = 'noopener noreferrer'; + a.target = item.target || '_self'; + a.dataset.search = (item.label + ' ' + (item.note || '') + ' ' + (section.title || '')).toLowerCase(); + a.setAttribute('aria-label', item.label || 'Link'); + + a.innerHTML = ` + <div class="icon">${renderIcon(item)}</div> + <div> + <div class="label">${escapeHtml(item.label || 'Link')}</div> + ${item.note ? `<div class="sub">${escapeHtml(item.note)}</div>` : ''} + </div> + <div class="dot${item.check ? '' : ' ok'}" title="${item.check ? 'checking…' : 'static'}"></div> + `; + + if (item.check) a.dataset.check = item.check; // status ping URL + grid.appendChild(a); + }); + + // Apply last-row stretching now that tiles exist + applyStretching(grid); + + return sec; +} + +function renderIcon(item){ + const ic = item.icon; + const scale = (typeof item.icon_scale === 'number' && isFinite(item.icon_scale)) + ? item.icon_scale + : DEFAULT_ICON_SCALE; + + if (typeof ic === 'string' && /\.(svg|png|webp|jpe?g|gif)$/i.test(ic)) { + const cls = item.icon_class ? ` class="${escapeAttr(item.icon_class)}"` : ''; + const src = resolveIconSrc(ic); + return `<img src="${escapeAttr(src)}"${cls} alt="" loading="lazy" decoding="async" style="--icon-scale:${scale}">`; + } + if (item.icon_svg) return item.icon_svg; + return '🔗'; +} + +function resolveIconSrc(path){ + // If absolute URL or root path, use as-is. Else prefix with ICON_BASE (if set). + if (/^https?:\/\//i.test(path) || path.startsWith('/')) return path; + if (ICON_BASE) return ICON_BASE.replace(/\/+$/,'/') + path.replace(/^\/+/, ''); + return path; +} + +/* --------------------- Search + Filter --------------------- */ +function attachSearch(){ + const tiles = Array.from(document.querySelectorAll('.tile')); + const sections = Array.from(document.querySelectorAll('.section')); + const run = () => { + const q = (document.getElementById('search')?.value || '').trim().toLowerCase(); + tiles.forEach(t => t.classList.toggle('hidden', q && !t.dataset.search.includes(q))); + sections.forEach(s => { + const grid = s.querySelector('.grid'); + const anyVisible = grid && Array.from(grid.querySelectorAll('.tile')).some(t => !t.classList.contains('hidden')); + s.classList.toggle('hidden', !anyVisible); + if (grid) applyStretching(grid); + }); + }; + document.getElementById('search')?.addEventListener('input', run); + run(); +} + +/* --------------------- Status "up" checks via <img> --------------------- */ +function checkAll(){ + document.querySelectorAll('.tile[data-check]').forEach(tile => { + const dot = tile.querySelector('.dot'); + const url = tile.dataset.check + (tile.dataset.check.includes('?') ? '&' : '?') + 't=' + Date.now(); + const img = new Image(); + const fail = () => { dot.classList.add('down'); dot.title = 'down'; }; + const ok = () => { dot.classList.remove('down'); dot.title = 'up'; }; + const timeout = setTimeout(fail, 4500); + img.onload = () => { clearTimeout(timeout); ok(); }; + img.onerror = () => { clearTimeout(timeout); fail(); }; + img.src = url; + }); +} + +/* --------------------- Last-row stretching (robust) --------------------- */ +function applyStretching(grid){ + const tiles = Array.from(grid.querySelectorAll('.tile')) + .filter(t => !t.classList.contains('hidden')); + + // Reset any previous manual spans + tiles.forEach(t => { t.style.gridColumn = ''; }); + + const gridCols = readCssInt(grid, '--grid-cols', 6); + const tileSpan = readCssInt(grid, '--tile-colspan', 2); + const perRow = Math.max(1, Math.floor(gridCols / tileSpan)); + const remainder = tiles.length % perRow; + + if (remainder === 1) { + // 1 leftover → full width + tiles[tiles.length - 1].style.gridColumn = `span ${gridCols}`; + } else if (remainder === 2) { + // 2 leftovers → 50/50 + const half = Math.max(tileSpan, Math.floor(gridCols / 2)); + tiles[tiles.length - 2].style.gridColumn = `span ${half}`; + tiles[tiles.length - 1].style.gridColumn = `span ${half}`; + } +} + +function readCssInt(el, varName, fallback){ + let v = parseInt(getComputedStyle(el).getPropertyValue(varName), 10); + if (!v) v = parseInt(getComputedStyle(document.documentElement).getPropertyValue(varName), 10); + return Number.isFinite(v) && v > 0 ? v : fallback; +} + +/* --------------------- Preload top icons (perf) --------------------- */ +function preloadIcons(cfg){ + const limit = Math.max(0, Math.min(64, cfg?.icons?.preloadTopN ?? 16)); + if (!limit) return; + const seen = new Set(); + const paths = []; + (cfg.sections || []).forEach(s => (s.items || []).forEach(i => { + if (typeof i.icon === 'string' && /\.(svg|png|webp|jpe?g|gif)$/i.test(i.icon)) { + const src = resolveIconSrc(i.icon); + if (!seen.has(src)) { seen.add(src); paths.push(src); } + } + })); + paths.slice(0, limit).forEach(src => { + const link = document.createElement('link'); + link.rel = 'preload'; link.as = 'image'; link.href = src; + document.head.appendChild(link); + }); +} + +/* --------------------- Icon fallback --------------------- */ +function attachIconFallback(){ + document.querySelectorAll('.icon img').forEach(img => { + img.addEventListener('error', () => { + if (img.dataset.fallbackApplied) return; + img.dataset.fallbackApplied = '1'; + img.src = FALLBACK_ICON || BUILTIN_FALLBACK; + }); + }); +} + +/* --------------------- Utils --------------------- */ +function hexToRgb(hex){ + let h = hex.replace('#','').trim(); + if (h.length === 3) h = h.split('').map(c => c + c).join(''); + const numHex = parseInt(h,16); + const r = (numHex >> 16) & 255, g = (numHex >> 8) & 255, b = numHex & 255; + return [r,g,b]; +} +function setVar(name, val){ document.documentElement.style.setProperty(name, String(val)); } +function num(v, d){ return (typeof v === 'number' && isFinite(v)) ? v : d; } +function escapeHtml(s){ return String(s).replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m])); } +function escapeAttr(s){ return String(s).replace(/"/g, '"'); } diff --git a/config.json b/config.json new file mode 100644 index 0000000..b3204d6 --- /dev/null +++ b/config.json @@ -0,0 +1,288 @@ +{ + "title": "MakeArmy", + "subtitle": "Welcome to the Maker Dash", + + "header": { + "logo": "/images/ma-icon.png", + "background": "#101010", + "searchPlaceholder": "Start typing to filter", + "showSearch": true + }, + + "icons": { + "basePath": "/icons/", + "fallbackIcon": "fallback.svg", + "defaultScale": 1, + "boxPercent": "85%", + "preloadTopN": 16, + "faviconPng": "/images/favicon.png", + "faviconIco": "/images/favicon.ico", + "ogImage": "/images/ma-icon.png" + }, + + "typography": { + "sectionTitlePx": 22, + "itemLabelPx": 12 + }, + + "theme": { + "background": "#202020", + "panel": "#121212", + "tile": "#202020", + "ink": "#f2f2f2", + "inkDim": "#a9a9a9", + "accents": { + "blue": "#3a85ff", + "green": "#1fc66e", + "red": "#ff3f5d", + "yellow": "#ffdb33" + }, + "glow": { + "corePx": 3, + "blurPx": 7, + "coreAlpha": 0.95, + "outerAlpha": 0.64 + } + }, + + "layout": { + "pageMax": 2400, + "gapSections": 14, + "gapItems": 10, + "gridCols": 6, + "tileColspan": 2, + "sectionMin": 300, + "sectionBasis": 320, + "sectionMax": 340, + "tileHeight": 62 + }, + + "a11y": { + "reducedMotion": true, + "focusOutline": true + }, + + "sections": [ + { + "title": "Laser Everything", + "accent": "#8a6cff", + "items": [ + { + "label": "Fiber Settings", + "note": "community laser settings for fiber", + "icon": "db-f.png", + "href": "https://makearmy.io/fiber-settings", + "target": "_blank", + "check": "https://makearmy.io/fiber-settings" + }, + { + "label": "UV Settings", + "note": "community laser settings for uv", + "icon": "db-u.png", + "href": "https://makearmy.io/uv-settings", + "target": "_blank", + "check": "https://makearmy.io/uv-settings" + }, + { + "label": "CO2g Settings", + "note": "community laser settings for co2 galvo", + "icon": "db-c.png", + "href": "https://makearmy.io/co2-galvo-settings", + "target": "_blank", + "check": "https://makearmy.io/co2-galvo-settings" + }, + { + "label": "CO2G Settings", + "note": "community laser settings for co2 gantry", + "icon": "db-cg.png", + "href": "https://makearmy.io/co2-gantry-settings", + "target": "_blank", + "check": "https://makearmy.io/co2-gantry-settings" + }, + { + "label": "Material Safety", + "note": "material safety lookup table", + "icon": "db-m.png", + "href": "https://makearmy.io/materials", + "target": "_blank", + "check": "https://makearmy.io/materials" + }, + { + "label": "Coating Safety", + "note": "material coating safety lookup table", + "icon": "db-mc.png", + "href": "https://makearmy.io/materials-coatings", + "target": "_blank", + "check": "https://makearmy.io/materials-coatings" + }, + { + "label": "Project DB", + "note": "community submitted project files", + "icon": "db-p.png", + "href": "https://makearmy.io/projects", + "target": "_blank", + "check": "https://makearmy.io/projects" + }, + { + "label": "Laser Source DB", + "note": "database of all known laser sources", + "icon": "db-l.png", + "href": "https://makearmy.io/lasers", + "target": "_blank", + "check": "https://makearmy.io/lasers" + } + ] + }, + { + "title": "Communities", + "accent": 2, + "items": [ + { + "label": "Misskey", + "note": "the federated maker hub", + "icon": "deck.png", + "href": "https://deck.makearmy.io", + "target": "_blank", + "check": "https://deck.makearmy.io" + }, + { + "label": "Mastodon", + "note": "status update platform", + "icon": "mastodon.png", + "href": "https://mastodon.makearmy.io", + "target": "_blank", + "check": "https://mastodon.makearmy.io" + }, + { + "label": "Lemmy", + "note": "link aggregator", + "icon": "lemmy.png", + "href": "https://lemmy.makearmy.io", + "target": "_blank", + "check": "https://lemmy.makearmy.io" + }, + { + "label": "Pixelfed", + "note": "social image sharing platform", + "icon": "pixelfed.png", + "href": "https://pixels.makearmy.io", + "target": "_blank", + "check": "https://pixels.makearmy.io" + }, + { + "label": "PeerTube", + "note": "social video sharing platform", + "icon": "peertube.png", + "href": "https://watch.makearmy.io/", + "target": "_blank", + "check": "https://watch.makearmy.io/home" + }, + { + "label": "News", + "note": "updates on the maker world from makearmy", + "icon": "news.png", + "href": "https://news.makearmy.io/read", + "target": "_blank", + "check": "https://news.makearmy.io/read" + }, + { + "label": "Podcasts", + "note": "follow our podcasts! fediverse compatible!", + "icon": "castopod.png", + "href": "https://podcast.makearmy.io/", + "target": "_blank", + "check": "https://podcast.makearmy.io/@LaserSource" + } + ] + }, + { + "title": "Utilities", + "accent": 1, + "items": [ + { + "label": "Picsur", + "note": "Simple Image Host", + "icon": "picsur.png", + "href": "https://images.makearmy.io", + "target": "_blank", + "check": "https://images.makearmy.io" + }, + { + "label": "PrivateBin", + "note": "Your encrypted internet clipboard.", + "icon": "privatebin.png", + "href": "https://paste.makearmy.io/", + "target": "_blank", + "check": "https://paste.makearmy.io/" + }, + { + "label": "Laser Toolkit", + "note": "convert laser settings, interval and more", + "icon": "toolkit.png", + "href": "https://makearmy.io/laser-toolkit", + "target": "_blank", + "check": "https://makearmy.io/laser-toolkit" + }, + { + "label": "File Server", + "note": "download from our file explorer", + "icon": "fs.png", + "href": "https://makearmy.io/files", + "target": "_blank", + "check": "https://makearmy.io/files" + }, + { + "label": "Buying Guide", + "note": "reviews and listings for relevant products", + "icon": "bg.png", + "href": "https://makearmy.io/buying-guide", + "target": "_blank", + "check": "https://makearmy.io/buying-guide" + }, + { + "label": "BG Remover", + "note": "advanced open source background remover featuring 10 AI models", + "icon": "bgrm.png", + "href": "https://makearmy.io/background-remover", + "target": "_blank", + "check": "https://makearmy.io/background-remover" + }, + { + "label": "Forgejo", + "note": "git for our community members", + "icon": "forgejo.png", + "href": "https://forge.makearmy.io", + "target": "_blank", + "check": "https://forge.makearmy.io" + } + ] + }, + { + "title": "Support Us", + "accent": 4, + "items": [ + { + "label": "The LMA", + "note": "the laser master academy is the best way to show your support", + "icon": "lma.png", + "href": "https://www.mightynetworks.com/", + "target": "_blank" + }, + { + "label": "Patreon", + "note": "patreon is a great way to support the work we're doing", + "icon": "patreon.png", + "href": "https://www.patreon.com/LaserEverything", + "target": "_blank" + }, + { + "label": "Liberapay", + "note": "more discreet ways to pay", + "icon": "liberapay.png", + "href": "https://liberapay.com/LaserEverything", + "target": "_blank" + } + ] + } + ] +} diff --git a/icons/bg.png b/icons/bg.png new file mode 100644 index 0000000..13c1ef2 Binary files /dev/null and b/icons/bg.png differ diff --git a/icons/bgrm.png b/icons/bgrm.png new file mode 100644 index 0000000..dcfa141 Binary files /dev/null and b/icons/bgrm.png differ diff --git a/icons/castopod.png b/icons/castopod.png new file mode 100644 index 0000000..123dbd9 Binary files /dev/null and b/icons/castopod.png differ diff --git a/icons/db-c.png b/icons/db-c.png new file mode 100644 index 0000000..ea4c646 Binary files /dev/null and b/icons/db-c.png differ diff --git a/icons/db-cg.png b/icons/db-cg.png new file mode 100644 index 0000000..d0ec6fe Binary files /dev/null and b/icons/db-cg.png differ diff --git a/icons/db-f.png b/icons/db-f.png new file mode 100644 index 0000000..37c655a Binary files /dev/null and b/icons/db-f.png differ diff --git a/icons/db-l.png b/icons/db-l.png new file mode 100644 index 0000000..02c5b8f Binary files /dev/null and b/icons/db-l.png differ diff --git a/icons/db-m.png b/icons/db-m.png new file mode 100644 index 0000000..909918c Binary files /dev/null and b/icons/db-m.png differ diff --git a/icons/db-mc.png b/icons/db-mc.png new file mode 100644 index 0000000..c46893f Binary files /dev/null and b/icons/db-mc.png differ diff --git a/icons/db-p.png b/icons/db-p.png new file mode 100644 index 0000000..3c89468 Binary files /dev/null and b/icons/db-p.png differ diff --git a/icons/db-u.png b/icons/db-u.png new file mode 100644 index 0000000..825a402 Binary files /dev/null and b/icons/db-u.png differ diff --git a/icons/db.png b/icons/db.png new file mode 100644 index 0000000..9bb3d5b Binary files /dev/null and b/icons/db.png differ diff --git a/icons/deck.png b/icons/deck.png new file mode 100644 index 0000000..6816286 Binary files /dev/null and b/icons/deck.png differ diff --git a/icons/ergo.png b/icons/ergo.png new file mode 100644 index 0000000..66f693b Binary files /dev/null and b/icons/ergo.png differ diff --git a/icons/forgejo.png b/icons/forgejo.png new file mode 100644 index 0000000..846a1e7 Binary files /dev/null and b/icons/forgejo.png differ diff --git a/icons/fs.png b/icons/fs.png new file mode 100644 index 0000000..e11cf52 Binary files /dev/null and b/icons/fs.png differ diff --git a/icons/icon3.png b/icons/icon3.png new file mode 100644 index 0000000..5259471 Binary files /dev/null and b/icons/icon3.png differ diff --git a/icons/icon4.png b/icons/icon4.png new file mode 100644 index 0000000..eb8ed29 Binary files /dev/null and b/icons/icon4.png differ diff --git a/icons/lemmy.png b/icons/lemmy.png new file mode 100644 index 0000000..be51070 Binary files /dev/null and b/icons/lemmy.png differ diff --git a/icons/liberapay.png b/icons/liberapay.png new file mode 100644 index 0000000..7363b87 Binary files /dev/null and b/icons/liberapay.png differ diff --git a/icons/lma.png b/icons/lma.png new file mode 100644 index 0000000..68123a6 Binary files /dev/null and b/icons/lma.png differ diff --git a/icons/lpclpi.png b/icons/lpclpi.png new file mode 100644 index 0000000..043b183 Binary files /dev/null and b/icons/lpclpi.png differ diff --git a/icons/mastodon.png b/icons/mastodon.png new file mode 100644 index 0000000..d5ebee4 Binary files /dev/null and b/icons/mastodon.png differ diff --git a/icons/matrix.png b/icons/matrix.png new file mode 100644 index 0000000..c405058 Binary files /dev/null and b/icons/matrix.png differ diff --git a/icons/news.png b/icons/news.png new file mode 100644 index 0000000..ce99b21 Binary files /dev/null and b/icons/news.png differ diff --git a/icons/nextcloud.png b/icons/nextcloud.png new file mode 100644 index 0000000..d6e1ab3 Binary files /dev/null and b/icons/nextcloud.png differ diff --git a/icons/patreon.png b/icons/patreon.png new file mode 100644 index 0000000..27a1b0e Binary files /dev/null and b/icons/patreon.png differ diff --git a/icons/peertube.png b/icons/peertube.png new file mode 100644 index 0000000..113e6dc Binary files /dev/null and b/icons/peertube.png differ diff --git a/icons/picsur.png b/icons/picsur.png new file mode 100644 index 0000000..0c857ca Binary files /dev/null and b/icons/picsur.png differ diff --git a/icons/pixelfed.png b/icons/pixelfed.png new file mode 100644 index 0000000..01d2d5a Binary files /dev/null and b/icons/pixelfed.png differ diff --git a/icons/privatebin.png b/icons/privatebin.png new file mode 100644 index 0000000..f20cfc5 Binary files /dev/null and b/icons/privatebin.png differ diff --git a/icons/toolkit.png b/icons/toolkit.png new file mode 100644 index 0000000..da372b8 Binary files /dev/null and b/icons/toolkit.png differ diff --git a/images/favicon.ico b/images/favicon.ico new file mode 100644 index 0000000..be75c60 Binary files /dev/null and b/images/favicon.ico differ diff --git a/images/favicon.png b/images/favicon.png new file mode 100644 index 0000000..813f435 Binary files /dev/null and b/images/favicon.png differ diff --git a/images/ma-icon.png b/images/ma-icon.png new file mode 100644 index 0000000..813f435 Binary files /dev/null and b/images/ma-icon.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..7f82f98 --- /dev/null +++ b/index.html @@ -0,0 +1,36 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + <title>MakeArmy • Home + + + + + + + + + + +
+
+ +
+

MakeArmy

+

Welcome to the Maker Dash

+
+
+ +
+ +
+ + + + diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..07c8f98 --- /dev/null +++ b/styles.css @@ -0,0 +1,234 @@ +/* === Config-driven Ultra-compact 4-up • full-bleed header, crisp icons, tight bright glow === */ + +:root{ + /* fonts */ + --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, "Liberation Mono", + "DejaVu Sans Mono", "Courier New", monospace; + --sans: Inter, "Inter var", "InterVariable", "Public Sans", + "Noto Sans", "Liberation Sans", ui-sans-serif, system-ui, + -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + + /* greys + ink (JS can override) */ + --bg0:#202020; --panel:#121212; --tile:#202020; + --ink:#f2f2f2; --ink-dim:#a9a9a9; + + /* accents (JS can override) */ + --blue:#3a85ff; --blue-rgb:58,133,255; + --green:#1fc66e; --green-rgb:31,198,110; + --red:#ff3f5d; --red-rgb:255,63,93; + --yellow:#ffdb33; --yellow-rgb:255,219,51; + + /* glow intensity/shape (JS can override) */ + --glow-core: 3px; + --glow-blur: 7px; + --glow-core-a: .95; + --glow-outer-a: .64; + + /* layout (JS can override) */ + --page-max:2400; + --gap-sec:14px; /* between sections */ + --gap-item:10px; /* between tiles */ + + /* NEW: grid controls */ + --grid-cols: 6; /* columns in section grid */ + --tile-colspan: 2; /* default span for a tile (→ 3 per row with 6 cols) */ + + --section-min:300px; + --section-basis:320px; + --section-max:340px; + --tile-h:62px; + + /* header bg (JS overrides via --topbar-bg) */ + --topbar-bg:#101010; + + /* NEW: typography knobs */ + --section-title-px: 22; + --item-label-px: 12; + + /* NEW: icon box size */ + --icon-box-pct: 85%; + + /* radii & shadows */ + --radius-sec:4px; --radius-tile:3px; + --shadow-soft:0 2px 8px rgba(0,0,0,.30); +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; color:var(--ink); background:var(--bg0); + font:11.5px/1.35 var(--sans); + -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility; +} + +/* ================= HEADER (full-bleed) ================= */ +.topbar{ + background:var(--topbar-bg); + width:100%; + margin:0 0 16px; + padding:16px 18px; + display:flex; align-items:center; gap:18px; + box-sizing:border-box; +} +.brand{display:flex; align-items:center; gap:14px} +.logo{ width:66px; height:66px; border-radius:12px; overflow:hidden; display:grid; place-items:center; } +.logo img{ width:100%; height:100%; object-fit:contain; image-rendering:auto; -ms-interpolation-mode:bicubic; } +.titles h1{ margin:0; font-weight:800; font-family:var(--mono); font-size:36px; line-height:1; letter-spacing:.2px } +.titles p{ margin:4px 0 0 0; color:#b8b8b8; font:italic 400 16px/1.25 var(--sans) } +.search{margin-left:auto} +.search input{ + width:360px; max-width:50vw; + padding:10px 14px; border-radius:10px; + background:#181818; color:var(--ink); border:none; +} + +/* ================= SECTIONS ================= */ +.wrap{ + /* FIX: use calc() so unitless --page-max is multiplied by px */ + max-width: min(95vw, calc(var(--page-max) * 1px)); + margin:0 auto 18px; padding:0 6px; + display:flex; flex-wrap:wrap; gap:var(--gap-sec); align-items:flex-start; +} +.section{ + flex:1 1 var(--section-basis); + min-width: var(--section-min); + max-width: var(--section-max); + background:var(--panel); border-radius:var(--radius-sec); overflow:hidden; +} + +/* Section headers (monospace, configurable size) */ +.section .head{ + display:flex; align-items:center; gap:8px; padding:8px 10px; + font-weight:900; font-family:var(--mono); line-height:1; + font-size: calc(var(--section-title-px) * 1px); + letter-spacing:.08px; color:#0a0a0a; +} +.section .gear{margin-left:auto; opacity:.6; font-size:12px} + +/* default accent by index */ +.section[data-accent="1"] .head{ background:var(--blue); } +.section[data-accent="2"] .head{ background:var(--green); } +.section[data-accent="3"] .head{ background:var(--red); } +.section[data-accent="4"] .head{ background:var(--yellow); } + +/* custom per-section accent */ +.section.custom-accent .head{ background:var(--accent); } + +/* === STRICT GRID INSIDE EACH SECTION === */ +.grid{ + display:grid; + gap:var(--gap-item); + padding:var(--gap-item); + background:var(--panel); + + /* NEW: 6-track grid by default */ + grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr)); + grid-auto-rows: var(--tile-h); + grid-auto-flow: row; +} + +/* Default tile span (→ 3 per row with 6 cols) */ +.tile{ grid-column: span var(--tile-colspan); } + +.tile{ + position:relative; + background:var(--tile); + border:none; border-radius:var(--radius-tile); + box-shadow:var(--shadow-soft); + transition: transform .06s ease, box-shadow .12s ease, background-color .12s ease; + + display:grid; + grid-template-areas:"title" "icon"; + grid-template-rows: 44% 56%; + padding:5px; + + color:var(--ink); + text-decoration:none; +} +.tile:link, .tile:visited, .tile:hover, .tile:active{ + color:var(--ink); text-decoration:none; +} +.tile:hover{ transform:translateY(-1px); } + +/* Hover underglow: tighter + brighter, config controlled */ +.section[data-accent="1"] .tile:hover{ + box-shadow: + 0 2px 7px rgba(0,0,0,.24), + 0 0 var(--glow-core) rgba(var(--blue-rgb), var(--glow-core-a)), + 0 0 var(--glow-blur) rgba(var(--blue-rgb), var(--glow-outer-a)); +} +.section[data-accent="2"] .tile:hover{ + box-shadow: + 0 2px 7px rgba(0,0,0,.24), + 0 0 var(--glow-core) rgba(var(--green-rgb), var(--glow-core-a)), + 0 0 var(--glow-blur) rgba(var(--green-rgb), var(--glow-outer-a)); +} +.section[data-accent="3"] .tile:hover{ + box-shadow: + 0 2px 7px rgba(0,0,0,.24), + 0 0 var(--glow-core) rgba(var(--red-rgb), var(--glow-core-a)), + 0 0 var(--glow-blur) rgba(var(--red-rgb), var(--glow-outer-a)); +} +.section[data-accent="4"] .tile:hover{ + box-shadow: + 0 2px 7px rgba(0,0,0,.24), + 0 0 var(--glow-core) rgba(var(--yellow-rgb), var(--glow-core-a)), + 0 0 var(--glow-blur) rgba(var(--yellow-rgb), var(--glow-outer-a)); +} + +/* custom-accent hover */ +.section.custom-accent .tile:hover{ + box-shadow: + 0 2px 7px rgba(0,0,0,.24), + 0 0 var(--glow-core) rgba(var(--accent-rgb), var(--glow-core-a)), + 0 0 var(--glow-blur) rgba(var(--accent-rgb), var(--glow-outer-a)); +} + +/* Item title (white, normal case, configurable size) */ +.tile > div:nth-child(2){ grid-area:title; display:grid; place-items:start center; text-align:center; } +.label{ + margin:0; + font-family:var(--sans); + font-weight:700; font-style:normal; + letter-spacing:.05px; line-height:1.1; + font-size: calc(var(--item-label-px) * 1px); + white-space:nowrap; overflow:hidden; text-overflow:ellipsis; + color:var(--ink); text-decoration:none; +} +.sub{ display:none; } + +/* Icon block — big & crisp (configurable box size) */ +.icon{ + grid-area:icon; place-self:end center; + width: var(--icon-box-pct); height: var(--icon-box-pct); + display:flex; align-items:center; justify-content:center; + font-size:clamp(24px, 9vw, 48px); + line-height:1; margin-bottom:4px; background:none; +} +.icon img, .icon svg{ + display:block; max-width:100%; max-height:100%; + object-fit:contain; image-rendering:auto; -ms-interpolation-mode:bicubic; + transform: scale(var(--icon-scale, 1)); transform-origin:center bottom; +} +.icon svg{ shape-rendering:geometricPrecision; text-rendering:optimizeLegibility } + +/* Status dot */ +.dot{ + --ok:#00e676; --down:#ff3d57; + position:absolute; left:5px; bottom:5px; + width:7px; height:7px; border-radius:50%; background:var(--ok); +} +.dot.down{ background:var(--down); } + +/* Focus states (accessibility) */ +.section[data-accent="1"] .tile:focus-visible{ outline:2px solid var(--blue); outline-offset:2px; } +.section[data-accent="2"] .tile:focus-visible{ outline:2px solid var(--green); outline-offset:2px; } +.section[data-accent="3"] .tile:focus-visible{ outline:2px solid var(--red); outline-offset:2px; } +.section[data-accent="4"] .tile:focus-visible{ outline:2px solid var(--yellow); outline-offset:2px; } +.section.custom-accent .tile:focus-visible{ outline:2px solid var(--accent); outline-offset:2px; } + +/* Reduced motion */ +@media (prefers-reduced-motion: reduce){ + * { transition: none !important; } + .tile:hover{ transform:none; } +}