the project lead: "a site now what is on par with our current miner and wallet app look and feel with the new animation ... simple and
beautiful ... apple and windows icons everywhere needed, GPU brand icons ... easy access to all of our pages without a header
that is crazy".
- site.css section 10: the miner and wallet apps' treatments (app.css miner-ui-5, wallet-0.1.5) over the package: buttons
(Plex Sans 600, radius 10), the status pill, graphite cards at 16 and rows at 12 on the hairline, the wallet lock screen's
ember glow, the mark wells; the fourteen palette tokens stay in the scene-tokens block (scene/tokens.css); light and dark.
- Navigation: the seven-item header is gone. The slim bar carries the mark, a DEVNET pill with the live dot, Mine / Network /
Learn (each opens a panel listing every route of its group with one line), the theme toggle and Download; on phones one
button opens a full-screen sheet with the same groups. Keyboard: arrows between groups, Escape closes and returns focus,
Tab walks a panel, the sheet is a dialog with its focus kept. The gate's header check is the new rule
(tools/ci/site-nav-check.mjs: the bar, the three groups with their aria, 18 routes in the panels and the sheet with their
descriptions, Download, the burger; self-test known-failed first).
- Home: one fold. The mark on the ember glow, one line, Download for Windows and Download for macOS with the OS glyphs and the
stamped version (the visitor's platform first), "Linux and HiveOS" to /download, IgneumDag 2.0.3 painting underneath from
/api/live through IgneumFeed (window 60, fps 60, poll false, no fork). Below: three lines on what a miner gets (E5 and X35
verbatim), the live network in three tiles (hash rate, blocks a second, locked checkpoint; X2 and X31 verbatim), the
community row (Discord, GitHub, Reddit), the footer (X7). The scene scripts are deferred; fonts preload with swap.
- /download: a new route. Windows, macOS, Linux and HiveOS cards with their marks, the stamped version, size and sha256, the
HiveOS flight sheet, the three steps, the card picker with the vendor badge, the wallet for macOS; Windows wallet named as
next with no file.
- Marks: brand/marks/vendor-marks.mjs (gpu-logos lane, a94dd192) copied byte for byte to site/lib/marks.mjs (the build and a
gate line refuse drift); site/lib/os-marks.mjs adds Linux and HiveOS in the same treatment. The build stamps
<span data-os> and <span data-gpu> and puts the vendor's mini badge in front of every table cell that names a card
(/miner, /miners, the bench table), writes the --mark-* tokens into site.css between markers, and hands yourcard.js the
badges as JSON for the card picker. The vendor well is scoped to [data-mark] so the ledger's and the journey's own
.badge keep their look.
- The footer carries the four OS download chips; the nav's group wrappers are .nav-group (the wallet page owns .group).
- tools/site/serve.mjs and capture.sh: the site as Vercel serves it with /api passed to the live observer, captured on
igneum-build-2 at 390 and 1440, dark and light (docs/plans/site-ui-5-shots, the home set in this commit). The overlap
lane's detector ran over every page on build-2: 0 overlaps after the two fixes here (the explorer's hash titles wrap, the
ledger's status badges wrap at 390).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
78 lines
6.4 KiB
JavaScript
78 lines
6.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Every served page carries the slim bar (site-ui-5, the owner's rule of 7 October 2026: "easy access to all of our pages
|
|
// without a header that is crazy"): the mark and wordmark, the three groups (Mine, Network, Learn) as buttons with their
|
|
// aria, the Download button, and every route of the site reachable from the group panels AND from the phone sheet, each with
|
|
// its one-line description. A page with a shorter nav, a panel missing a route, or a route the panels do not name fails the
|
|
// build and the gate. The route list below is the contract: a new page adds its line here and in site/partials/nav.html in
|
|
// the same commit.
|
|
// node tools/ci/site-nav-check.mjs [site-dir]
|
|
// node tools/ci/site-nav-check.mjs --self-test a page with a route dropped from the sheet is caught; the tree passes
|
|
import { readdirSync, readFileSync, writeFileSync, mkdtempSync, rmSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const GROUPS = {
|
|
mine: ['/miner', '/download', '/app', '/wallet', '/miners', '/dev-fee', '/faucet', '/metamask'],
|
|
network: ['/live', '/explorer', '/journey', '/bench', '/evidence'],
|
|
learn: ['/litepaper', '/ledger', '/claims', '/randomx', '/provenance'],
|
|
};
|
|
const ROUTES = Object.values(GROUPS).flat();
|
|
// pages served without their own panel entry: the explorer's two rewrites, the 404 and the unlinked scenes lab
|
|
const UNLISTED = new Set(['block.html', 'address.html', '404.html', 'scenes.html']);
|
|
|
|
export function check(site) {
|
|
const pages = readdirSync(site).filter(f => f.endsWith('.html'));
|
|
const bad = [];
|
|
for (const page of pages) {
|
|
const html = readFileSync(join(site, page), 'utf8');
|
|
const nav = html.match(/<nav class="nav"[\s\S]*?<\/nav>/); if (!nav) { bad.push(`${page}: no <nav class="nav">`); continue; }
|
|
const n = nav[0];
|
|
if (!/<span class="word">IGNEUM<\/span>/.test(n)) bad.push(`${page}: no wordmark`);
|
|
if (!/<svg class="brand-mark"/.test(n)) bad.push(`${page}: no mark`);
|
|
for (const g of Object.keys(GROUPS)) {
|
|
if (!new RegExp(`<button type="button" class="group-btn" id="nav-btn-${g}" data-group="${g}"(?: data-active)? aria-expanded="(true|false)" aria-controls="nav-panel-${g}" aria-haspopup="true"`).test(n)) bad.push(`${page}: the ${g} group button or its aria is missing`);
|
|
const panel = n.match(new RegExp(`<div class="nav-panel" id="nav-panel-${g}"[\\s\\S]*?<\\/div>\\s*<\\/div>`));
|
|
if (!panel) { bad.push(`${page}: no panel for ${g}`); continue; }
|
|
for (const r of GROUPS[g]) if (!new RegExp(`<a href="${r}"[^>]*><b>[^<]+</b><span>[^<]+</span></a>`).test(panel[0])) bad.push(`${page}: the ${g} panel lacks ${r} with its description`);
|
|
}
|
|
const sheet = n.match(/<div class="nav-sheet" id="nav-sheet"[\s\S]*?<\/nav>/);
|
|
if (!sheet) { bad.push(`${page}: no phone sheet`); continue; }
|
|
for (const r of ROUTES) if (!new RegExp(`<a href="${r}"[^>]*><b>[^<]+</b><span>[^<]+</span></a>`).test(sheet[0])) bad.push(`${page}: the sheet lacks ${r} with its description`);
|
|
if (!/<a href="\/download" class="btn primary small cta" data-nav="download">Download<\/a>/.test(n)) bad.push(`${page}: no Download button in the bar`);
|
|
if (!/<a href="\/download" class="btn primary big" data-nav="download">Download<\/a>/.test(sheet[0])) bad.push(`${page}: no Download in the sheet`);
|
|
if (!/<button class="burger" id="nav-burger" type="button" aria-expanded="(true|false)" aria-controls="nav-sheet"/.test(n)) bad.push(`${page}: the burger lacks its aria attributes`);
|
|
if (!/role="dialog" aria-modal="true" aria-label="Menu"/.test(sheet[0])) bad.push(`${page}: the sheet is not a dialog`);
|
|
if (/\.(group-btn|panel-list a|sheet-group a|nav \.cta)[^{]*\{[^}]*display:\s*none/.test(html.replace(/@media \(max-width:860px\)\{[\s\S]*?\}\}/g, ''))) bad.push(`${page}: a page style hides the bar's items`);
|
|
// the page's own route is marked in the bar (aria-current) unless it is one of the unlisted views
|
|
const route = '/' + page.replace(/\.html$/, '').replace(/^index$/, '');
|
|
if (route !== '/' && !UNLISTED.has(page) && !ROUTES.includes(route)) bad.push(`${page}: served but named in no panel (add it to a group in nav.html and here)`);
|
|
}
|
|
// every panel route is a served page
|
|
for (const r of ROUTES) if (!pages.includes(r.slice(1) + '.html')) bad.push(`${r} is in a panel but has no page`);
|
|
return { pages: pages.length, bad };
|
|
}
|
|
|
|
const args = process.argv.slice(2);
|
|
if (args[0] === '--self-test') {
|
|
const site = join(here, '..', '..', 'site');
|
|
const fx = mkdtempSync(join(tmpdir(), 'nav-check-'));
|
|
try {
|
|
for (const f of readdirSync(site).filter(f => f.endsWith('.html'))) writeFileSync(join(fx, f), readFileSync(join(site, f), 'utf8'));
|
|
const clean = check(fx); if (clean.bad.length) { console.error('self-test: the tree copy should pass\n' + clean.bad.join('\n')); process.exit(1); }
|
|
// known failed: /ledger dropped from the sheet of one page
|
|
const p = join(fx, 'miner.html'); const h = readFileSync(p, 'utf8');
|
|
const sheetAt = h.indexOf('<div class="nav-sheet"'); const broken = h.slice(0, sheetAt) + h.slice(sheetAt).replace(/<a href="\/ledger"[^>]*>[\s\S]*?<\/a>/, '');
|
|
writeFileSync(p, broken);
|
|
const r = check(fx); if (!r.bad.some(b => /miner\.html: the sheet lacks \/ledger/.test(b))) { console.error('self-test: a route dropped from the sheet was not caught'); process.exit(1); }
|
|
// known failed: a group button without its aria
|
|
writeFileSync(p, h.replace('id="nav-btn-learn" data-group="learn" aria-expanded="false"', 'id="nav-btn-learn" data-group="learn"'));
|
|
const r2 = check(fx); if (!r2.bad.some(b => /miner\.html: the learn group button/.test(b))) { console.error('self-test: a group button without aria was not caught'); process.exit(1); }
|
|
console.log('self-test passed: a route dropped from the sheet and a group button without its aria are caught; the tree passes');
|
|
} finally { rmSync(fx, { recursive: true, force: true }); }
|
|
} else {
|
|
const site = args[0] || join(here, '..', '..', 'site');
|
|
const { pages, bad } = check(site);
|
|
if (bad.length) { console.error('site nav check: ' + bad.length + ' problems\n' + bad.join('\n')); process.exit(1); }
|
|
console.log(`site nav check: ${pages} pages carry the slim bar: the mark, three groups with ${ROUTES.length} routes in the panels and the sheet, Download, the burger`);
|
|
}
|