Initial import

This commit is contained in:
makearmy 2025-08-24 09:39:05 -04:00
commit dd314435bd
40 changed files with 1175 additions and 0 deletions

244
README.md Normal file
View file

@ -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 <title> "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 sections 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 (01)
outerAlpha (default 0.64) outer brightness (01)
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 824.
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/".

373
app.js Normal file
View file

@ -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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m])); }
function escapeAttr(s){ return String(s).replace(/"/g, '&quot;'); }

288
config.json Normal file
View file

@ -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"
}
]
}
]
}

BIN
icons/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
icons/bgrm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
icons/castopod.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
icons/db-c.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
icons/db-cg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
icons/db-f.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
icons/db-l.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
icons/db-m.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
icons/db-mc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
icons/db-p.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
icons/db-u.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
icons/db.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
icons/deck.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
icons/ergo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
icons/forgejo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

BIN
icons/fs.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
icons/icon3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

BIN
icons/icon4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
icons/lemmy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
icons/liberapay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
icons/lma.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
icons/lpclpi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
icons/mastodon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
icons/matrix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
icons/news.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
icons/nextcloud.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

BIN
icons/patreon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
icons/peertube.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
icons/picsur.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
icons/pixelfed.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
icons/privatebin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
icons/toolkit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

BIN
images/ma-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

36
index.html Normal file
View file

@ -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</title>
<!-- Favicons -->
<link rel="icon" href="/images/favicon.png" type="image/png">
<link rel="icon" href="/images/favicon.ico">
<link rel="preload" href="styles.css" as="style">
<link rel="stylesheet" href="styles.css">
<meta name="color-scheme" content="dark light">
</head>
<body>
<header class="topbar">
<div class="brand">
<div class="logo">
<img src="/images/ma-icon.png" alt="MakeArmy logo">
</div>
<div class="titles">
<h1>MakeArmy</h1>
<p>Welcome to the Maker Dash</p>
</div>
</div>
<div class="search">
<input id="search" type="search" placeholder="Start typing to filter" autocomplete="off" />
</div>
</header>
<main id="app" class="wrap" aria-live="polite"></main>
<script defer src="app.js"></script>
</body>
</html>

234
styles.css Normal file
View file

@ -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; }
}