site-ui-5: the padding and visuals sweep (7 October 2026, 17:xx UK): the /wallet band's padding, tools/ci/padding-check.mjs in the gate, every finding fixed in site.css, the eye pass
the project lead: the ember band on /wallet had no vertical padding (the heading on the band's top edge, the button on its bottom), then "run a full site sweep for padding and visuals". - The band: .band .wrap carried the padding and the three bands use .container; both selectors now, 56 px (40 on phones), and 24 px at the sides on phones. - tools/ci/padding-check.mjs: renders every served page at 390, 768, 1024, 1440 and 1600 in both themes and flags TIGHT (text or a control under 24 px from the edge of the filled band, card or section that holds it; rows of radius 12 and under are the app's compact rows and keep their own padding; small filled things such as buttons, pills, badges, tabs, cells and the bar are never containers), SCALE (a section's vertical padding not 0 and not a multiple of 4 from 24), TOUCH (controls under 6 px apart outside segmented groups), OVERRUN (an image or canvas past its frame) and UNDER-BAR (a heading under the sticky bar on load, an anchor the bar would cover). --self-test builds the fixture from the site's own stylesheet and bar with the exact /wallet band with its padding lost, a touching pair, an image over its frame and a heading under the bar; each must fail, the clean twin must pass. A gate line runs it (self-test, then the built site); where no browser exists it says so. - Findings fixed (first run 2,740, then 1,040, then 48, now 0 at 230 renders): page heroes 64/48 (were 65/45), the observatory head and the 404 the same, the footer's bottom 24, doc sections and the litepaper's 40 (were 42), .sec 72 at tablet (was 70), the 404 card 36, /live's motion block 24 (was 25), the scenes lab 48 (was 50); every card at 24 inside (download cards, home tiles, the live tiles and stats, the evidence labels, the wallet state cards, the journey phases, the live inspector and toolbar, the download card on phones). - The eye pass over every page at 1440 and 390 dark: the journey's phases are cards now, so the old timeline rail and marker (drawn for an unboxed list, sitting on the card's top border) go and the when column keeps clear of the body; tables keep words whole (break-word, not anywhere: "regenerat es" on the litepaper) and give the label column 14ch (one word per line on /randomx); the litepaper's body paragraphs share one measure; the ledger's count-table chips stay on one line; orphan words go through text-wrap: pretty on body text (copy law 18, "measurement." alone on /claims); the dev-fee page lost its doubled gutter (a lifted section keeps its own .container) and its table clears the text above; /faucet and /metamask keep a gap between headline and lead, line their two cards up at the top and end 72 px above the footer; a heading after a card grid gets 40 px. - The overlap sweep at the same five widths and both themes: 0 overlaps at 290 renders. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
40ea28aea0
commit
0f539f8b39
3 changed files with 227 additions and 2 deletions
|
|
@ -213,7 +213,8 @@ p.notice{display:block;margin:14px 0 0;font-size:13px;line-height:1.7}
|
|||
.dl-bar{display:none}
|
||||
@media (max-width:720px){.dl-bar{display:flex;position:fixed;left:0;right:0;bottom:0;z-index:30;justify-content:space-between;align-items:center;gap:var(--s-3);padding:10px var(--gutter) calc(10px + env(safe-area-inset-bottom));background:var(--top-bg);backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);border-top:1px solid var(--line)}.dl-bar span{font:400 var(--t-sm)/1.4 var(--f-mono);color:var(--ash);min-width:0}body.has-dl-bar{padding-bottom:72px}}
|
||||
.band{background:var(--ember);color:var(--ember-ink)}
|
||||
.band .wrap{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--s-5);padding-top:56px;padding-bottom:56px}
|
||||
.band .wrap,.band .container{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--s-5);padding-top:56px;padding-bottom:56px}
|
||||
@media (max-width:560px){.band .wrap,.band .container{padding-top:40px;padding-bottom:40px}}
|
||||
.band h2{color:var(--ember-ink);font-weight:900;margin-bottom:var(--s-2)}
|
||||
.band p{margin:0;max-width:56ch}
|
||||
.btn.dark{background:var(--obsidian);color:var(--bone);border-color:var(--obsidian)}
|
||||
|
|
@ -497,9 +498,62 @@ article .status{white-space:normal;flex-wrap:wrap;overflow:visible}
|
|||
article .status .badge:not([data-mark]){white-space:normal;overflow-wrap:anywhere;line-height:1.5;max-width:100%}
|
||||
/* data tables size to their content and scroll inside their frame, never squeeze a column to a sliver (site-ui-5) */
|
||||
.tbl table,.table-wrap table{width:100%;min-width:720px;table-layout:auto}
|
||||
.tbl td,.table-wrap td{white-space:normal;overflow-wrap:anywhere;vertical-align:top;min-width:9ch}
|
||||
.tbl td,.table-wrap td{white-space:normal;overflow-wrap:break-word;vertical-align:top;min-width:9ch}
|
||||
.tbl td .cardname,.table-wrap td .cardname{white-space:nowrap}
|
||||
.phero .os span{display:inline-flex;align-items:center}
|
||||
.download-platform{display:flex;align-items:center;gap:14px}
|
||||
/* the evidence table's status pills wrap inside their cell (overlap sweep, 7 Oct 2026) */
|
||||
table#claims .st{white-space:normal;display:inline-block;line-height:1.4;max-width:100%}
|
||||
/* the padding sweep (tools/ci/padding-check.mjs, 7 Oct 2026): every section's vertical padding on the scale (a multiple of 4
|
||||
from 24), every card's inner padding 24, so no text or control sits under 24 px from a filled edge */
|
||||
.page-hero,main .obs-head,.lost{padding-top:64px;padding-bottom:48px}
|
||||
@media (max-width:560px){.page-hero{padding:24px 0}}
|
||||
.site-footer{padding-bottom:24px}
|
||||
.footer-bottom{padding-top:24px}
|
||||
.doc-section,article section{padding-bottom:40px;margin-bottom:40px}
|
||||
@media (max-width:860px){.sec{padding:72px 0}}
|
||||
.lost{padding-bottom:96px}
|
||||
.grid>section.card{padding-top:36px;padding-bottom:36px}
|
||||
.motion{padding:24px}
|
||||
.lab{padding-top:48px}
|
||||
.dl-card,.home-tile,.tiles6>div,.mstats>div,.label,.card,.feature,.tool-card,.stat{padding:24px}
|
||||
.dl-card{gap:14px 16px}
|
||||
.dl-card .body{grid-column:1/-1}
|
||||
.footer-bottom{padding-bottom:0}
|
||||
@media (max-width:560px){.footer-bottom{padding-bottom:0}}
|
||||
/* the ledger's count table keeps each status badge whole: the table scrolls, the word never breaks */
|
||||
.counts .badge:not([data-mark]){white-space:nowrap;overflow-wrap:normal}
|
||||
/* the same scale and 24 px rule where a page's own style block sits later in the cascade (the sweep's second round) */
|
||||
main .layout article section{padding-bottom:40px;margin-bottom:40px}
|
||||
main .lost{padding-top:64px;padding-bottom:96px}
|
||||
main .motion{padding:24px}
|
||||
main .lab{padding-top:48px}
|
||||
main .tiles6>div,main .mstats>div{padding:24px 25px}
|
||||
main .labels .label{padding:24px}
|
||||
main .state-card,main .milestone{padding:24px}
|
||||
main .strip{padding-top:24px;padding-bottom:24px}
|
||||
/* the sweep's third round: the live inspector and toolbar at 24, the band and the download card 24 from their edges on phones */
|
||||
main .icol{padding:24px}
|
||||
main .toolbar{padding:24px 25px}
|
||||
@media (max-width:560px){.band .wrap,.band .container{padding-left:24px;padding-right:24px}.download-card{padding:24px}}
|
||||
/* the journey's phases are cards in this system: the old timeline rail and marker (drawn for an unboxed list) go, the badge says the state; the when column keeps clear of the body */
|
||||
main .milestone:before,main .milestone:after{display:none}
|
||||
main .milestone .when{padding-right:12px;padding-top:2px}
|
||||
@media (max-width:560px){main .milestone{padding:24px}}
|
||||
/* the litepaper's body paragraphs share one measure (the eye pass: the first paragraph sat narrower than the second) */
|
||||
main .layout article section>p{max-width:76ch}
|
||||
/* the ledger's count table: a filter chip stays on one line, the table scrolls before a pill wraps */
|
||||
main table .chip{white-space:nowrap}
|
||||
/* no orphan word on its own line (copy law 18): the browser's pretty wrapping on body text; headings already balance */
|
||||
p,li,dd,figcaption,.lead,.d,.sub{text-wrap:pretty}
|
||||
/* a comparison table's label column holds a short phrase on one or two lines, never one word per line */
|
||||
main .tbl td:first-child,main .table-wrap td:first-child{min-width:14ch}
|
||||
/* a section lifted into a derived page keeps its own .container: the inner one adds no second gutter; a card's table clears the text above it */
|
||||
main .container .container,main .wrap .wrap{padding-left:0;padding-right:0}
|
||||
.feecard .tbl,.feecard .table-wrap{margin-top:16px}
|
||||
/* the eye pass on /faucet and /metamask: the headline keeps a gap above its lead, two cards in a grid line up at the top, the page ends 72 px above the footer */
|
||||
main.wrap h1+p,main.wrap h1+.lead,.page-hero h1+.lead{margin-top:16px}
|
||||
main.wrap>.grid{align-items:start}
|
||||
main.wrap{padding-bottom:72px}
|
||||
@media (max-width:560px){main.wrap{padding-bottom:48px}}
|
||||
main.wrap>.grid{margin-bottom:40px}
|
||||
|
|
|
|||
170
tools/ci/padding-check.mjs
Normal file
170
tools/ci/padding-check.mjs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
#!/usr/bin/env node
|
||||
// The padding and visuals sweep (site-ui-5, 7 October 2026; the project lead's /wallet band whose heading touched the band's top edge and
|
||||
// whose button touched its bottom). Renders every served page at five widths in both themes and flags what a reader sees as
|
||||
// cramped or broken, five classes:
|
||||
// TIGHT text or a control closer than 24 px to the edge of the filled band, card or section that holds it
|
||||
// SCALE a section whose vertical padding is off the design system's scale (0, or a multiple of 4 from 24 up)
|
||||
// TOUCH two controls (buttons, links styled as buttons, inputs, selects) that touch or nearly touch (under 6 px apart)
|
||||
// OVERRUN an image or canvas that runs out of its frame (the nearest .frame, .app-window, .scene, .panel or figure)
|
||||
// UNDER-BAR a heading that renders under the sticky bar on load, or an anchor target the bar would cover on arrival
|
||||
// The things that are small and filled by design (buttons, pills, badges, chips, tabs, table cells, the bar, tooltips) are
|
||||
// containers whose own padding is theirs, so they are never the container of a TIGHT finding; segmented controls (.tabs,
|
||||
// .theme-group, .zoom, [role=tablist]) are exempt from TOUCH.
|
||||
//
|
||||
// node tools/ci/padding-check.mjs --self-test the fixture (the exact /wallet band with its padding lost, a
|
||||
// touching pair, an image over its frame, a heading under the bar)
|
||||
// must fail with one finding of each class; the clean fixture passes
|
||||
// node tools/ci/padding-check.mjs --site [dir] every *.html of the site (default site/)
|
||||
// options: --widths 390,768,1024,1440,1600 --themes dark,light --only <substring> --json <file> --jobs 3
|
||||
//
|
||||
// Needs a browser: Playwright from $IGNEUM_PLAYWRIGHT_DIR, /srv/builds/_bin/overlap (the box) or this tree's node_modules.
|
||||
// Without one it says so and exits 0 (CI installs Playwright; the box has it; the Mac renders nothing). Pages are served by
|
||||
// tools/site/serve.mjs with /api answering 502, so every page renders its rest state; ?theme= and ?motion=off are passed.
|
||||
import { readdirSync, readFileSync, writeFileSync, mkdtempSync, rmSync, existsSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawn } from 'node:child_process';
|
||||
import http from 'node:http';
|
||||
const here = dirname(fileURLToPath(import.meta.url)), repo = join(here, '..', '..');
|
||||
const args = process.argv.slice(2);
|
||||
const opt = (k, d) => { const i = args.indexOf(k); return i >= 0 && args[i + 1] ? args[i + 1] : d; };
|
||||
const WIDTHS = opt('--widths', '390,768,1024,1440,1600').split(',').map(Number);
|
||||
const THEMES = opt('--themes', 'dark,light').split(',');
|
||||
const ONLY = opt('--only', ''), JSON_OUT = opt('--json', ''), JOBS = Number(opt('--jobs', '3'));
|
||||
function playwright() {
|
||||
for (const root of [process.env.IGNEUM_PLAYWRIGHT_DIR, '/srv/builds/_bin/overlap', repo].filter(Boolean)) {
|
||||
try { return createRequire(join(root, 'package.json'))('playwright'); } catch (e) { /* next */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const pw = playwright();
|
||||
if (!pw) { console.log('padding-check: no Playwright here (the box and CI have it); nothing checked'); process.exit(0); }
|
||||
|
||||
// what runs inside the page: returns the findings for this render
|
||||
const INSPECT = `(() => {
|
||||
const MIN = 24, GAP = 6, out = [];
|
||||
const vis = el => { const r = el.getBoundingClientRect(); const cs = getComputedStyle(el); return r.width > 0 && r.height > 0 && cs.visibility !== 'hidden' && cs.display !== 'none' && Number(cs.opacity) > 0.05; };
|
||||
const txt = el => (el.textContent || '').replace(/\\s+/g, ' ').trim().slice(0, 60);
|
||||
const path = el => { const parts = []; for (let e = el; e && e !== document.body && parts.length < 4; e = e.parentElement) parts.unshift(e.tagName.toLowerCase() + (e.id ? '#' + e.id : '') + (e.classList.length ? '.' + [...e.classList].slice(0, 2).join('.') : '')); return parts.join(' > '); };
|
||||
const alpha = c => { const m = /rgba?\\(([^)]+)\\)/.exec(c); if (!m) return c === 'transparent' ? 0 : 1; const p = m[1].split(',').map(s => parseFloat(s)); return p.length > 3 ? p[3] : 1; };
|
||||
// a filled container has a background, or a border on every side (a framed box); a rule above or below a block is not a box
|
||||
const filled = el => { const cs = getComputedStyle(el); const bordered = ['Top','Right','Bottom','Left'].every(s => parseFloat(cs['border' + s + 'Width']) > 0 && alpha(cs['border' + s + 'Color']) > 0.02); return alpha(cs.backgroundColor) > 0.02 || cs.backgroundImage !== 'none' || bordered; };
|
||||
const SMALL = el => el.matches('a,button,input,select,textarea,label,td,th,.pill,.badge,.osmark,.chip,.tag,.tab,.tb,.state,.kind,.st,.sw,.lgi,.dot,code,kbd,summary,[role=tab],.nav,nav,.tip,.chip-zoom,.gen,.step-index,.n,.number,.num,.dev-label,.devnet,.hash-btn,.linkish,.switch,.track,.record-top .badge,.mini');
|
||||
// 1. TIGHT: the nearest filled, sizeable container of each text or control, and the distance to its edges
|
||||
const containerOf = el => { for (let e = el.parentElement; e && e !== document.body; e = e.parentElement) { if (SMALL(e)) return null; if (filled(e)) { const r = e.getBoundingClientRect(); const radius = parseFloat(getComputedStyle(e).borderTopLeftRadius) || 0; /* a row (radius 12 or under) is the app's compact row, its own padding is its own; a card is 14 and up */ if (r.height >= 72 && r.width >= 200 && !(radius > 0 && radius <= 12)) return e; return null; } } return null; };
|
||||
const seen = new Set();
|
||||
for (const el of document.querySelectorAll('h1,h2,h3,h4,p,li,a.btn,button,input,select,.eyebrow,.lead,figcaption,dt,dd,.k,.v,.d,.mk,.mv,.mf,.key,strong.v')) {
|
||||
if (!vis(el) || !txt(el) && !el.matches('a,button,input,select')) continue;
|
||||
if (el.closest('.nav,.tip,.chip-zoom,table,.tabs,.theme-group,.zoom,.mode,.nav-sheet,.panels,figcaption,.frame .bar,.window-top,.hive-sheet,.footer-dl,.sheet-social,.social')) continue;
|
||||
if (el.matches('figcaption')) continue;
|
||||
const c = containerOf(el); if (!c) continue;
|
||||
const r = el.getBoundingClientRect(), cr = c.getBoundingClientRect(), cs = getComputedStyle(c);
|
||||
const inset = { top: r.top - cr.top, bottom: cr.bottom - r.bottom, left: r.left - cr.left, right: cr.right - r.right };
|
||||
// a container that scrolls or clips (a table frame, a window) is judged by what is inside its padding box only
|
||||
const bad = Object.entries(inset).filter(([k, v]) => v >= -1 && v < MIN).map(([k, v]) => k + ' ' + Math.round(v) + 'px');
|
||||
if (bad.length) { const key = path(c) + '|' + bad.join(','); if (!seen.has(key)) { seen.add(key); out.push({ kind: 'TIGHT', what: txt(el) || el.tagName.toLowerCase(), where: path(el), container: path(c), detail: bad.join(', ') }); } }
|
||||
}
|
||||
// 2. SCALE: section paddings on the scale
|
||||
for (const el of document.querySelectorAll('section,.sec,.band,.page-hero,.fold,.site-footer,.cta-band,main > .obs-head')) {
|
||||
if (!vis(el)) continue; const cs = getComputedStyle(el);
|
||||
for (const side of ['paddingTop', 'paddingBottom']) { const v = Math.round(parseFloat(cs[side])); if (v !== 0 && (v < 24 || v % 4 !== 0)) out.push({ kind: 'SCALE', what: side + ' ' + v + 'px', where: path(el), detail: 'not 0 and not a multiple of 4 from 24 up' }); }
|
||||
}
|
||||
// 3. TOUCH: controls that touch
|
||||
const ctrls = [...document.querySelectorAll('a.btn,button,input,select,.community a,.dl-list a,.dl-row a,.footer-dl a')].filter(e => vis(e) && !e.closest('.tabs,.theme-group,.zoom,.mode,[role=tablist],.nav,.nav-sheet,.panels,table'));
|
||||
for (let i = 0; i < ctrls.length; i++) for (let j = i + 1; j < ctrls.length; j++) {
|
||||
const a = ctrls[i].getBoundingClientRect(), b = ctrls[j].getBoundingClientRect(); if (ctrls[i].contains(ctrls[j]) || ctrls[j].contains(ctrls[i])) continue;
|
||||
const dx = Math.max(a.left, b.left) - Math.min(a.right, b.right), dy = Math.max(a.top, b.top) - Math.min(a.bottom, b.bottom);
|
||||
if ((dx < GAP && dy < 0) || (dy < GAP && dx < 0)) out.push({ kind: 'TOUCH', what: (txt(ctrls[i]) || ctrls[i].tagName) + ' / ' + (txt(ctrls[j]) || ctrls[j].tagName), where: path(ctrls[i]), detail: 'gap ' + Math.round(Math.max(dx, dy)) + 'px' });
|
||||
}
|
||||
// 4. OVERRUN: media out of its frame
|
||||
for (const el of document.querySelectorAll('img,canvas,video')) {
|
||||
if (!vis(el)) continue; const f = el.closest('.frame,.app-window,.scene,.panel,.fold-scene,.proof-scene,figure,.wallet-card'); if (!f) continue;
|
||||
const r = el.getBoundingClientRect(), fr = f.getBoundingClientRect(), over = Math.max(r.right - fr.right, r.bottom - fr.bottom, fr.left - r.left, fr.top - r.top);
|
||||
if (over > 2 && getComputedStyle(f).overflow === 'visible') out.push({ kind: 'OVERRUN', what: el.tagName.toLowerCase() + ' ' + (el.getAttribute('src') || el.id || '').split('/').pop(), where: path(f), detail: Math.round(over) + 'px past the frame' });
|
||||
}
|
||||
// 5. UNDER-BAR: the bar covers a heading on load, or an anchor the bar would cover on arrival
|
||||
const nav = document.querySelector('.nav'); const navH = nav ? nav.getBoundingClientRect().height : 0;
|
||||
const h1 = [...document.querySelectorAll('h1')].find(vis);
|
||||
if (h1 && navH && h1.getBoundingClientRect().top < navH - 1) out.push({ kind: 'UNDER-BAR', what: txt(h1), where: path(h1), detail: 'top ' + Math.round(h1.getBoundingClientRect().top) + 'px under a ' + Math.round(navH) + 'px bar' });
|
||||
const sp = parseFloat(getComputedStyle(document.documentElement).scrollPaddingTop) || 0;
|
||||
for (const el of document.querySelectorAll('section[id],h2[id],h3[id],article[id],div[id].doc-section')) { const sm = parseFloat(getComputedStyle(el).scrollMarginTop) || 0; if (navH && Math.max(sp, sm) < navH) { out.push({ kind: 'UNDER-BAR', what: '#' + el.id, where: path(el), detail: 'scroll padding ' + Math.round(Math.max(sp, sm)) + 'px under a ' + Math.round(navH) + 'px bar' }); break; } }
|
||||
return out;
|
||||
})()`;
|
||||
|
||||
async function serve(dir) {
|
||||
const port = 4800 + Math.floor(Math.random() * 200);
|
||||
const child = spawn(process.execPath, [join(repo, 'tools', 'site', 'serve.mjs'), String(port), dir, 'http://127.0.0.1:1'], { stdio: 'ignore' });
|
||||
await new Promise((res, rej) => { let n = 0; const t = setInterval(() => { http.get(`http://127.0.0.1:${port}/`, () => { clearInterval(t); res(); }).on('error', () => { if (++n > 80) { clearInterval(t); rej(new Error('serve.mjs did not answer')); } }); }, 100); });
|
||||
return { port, stop: () => child.kill() };
|
||||
}
|
||||
async function sweep(dir, pages, browser) {
|
||||
const { port, stop } = await serve(dir);
|
||||
const findings = []; let renders = 0;
|
||||
const jobs = []; for (const p of pages) for (const w of WIDTHS) for (const t of THEMES) jobs.push({ p, w, t });
|
||||
async function one({ p, w, t }) {
|
||||
const ctx = await browser.newContext({ viewport: { width: w, height: w < 500 ? 844 : 900 }, deviceScaleFactor: 1, reducedMotion: 'reduce' });
|
||||
const page = await ctx.newPage();
|
||||
const route = p === 'index' ? '/' : '/' + p;
|
||||
try {
|
||||
await page.goto(`http://127.0.0.1:${port}${route}?theme=${t}&motion=off`, { waitUntil: 'load', timeout: 30000 });
|
||||
await page.waitForTimeout(250);
|
||||
const found = await page.evaluate(INSPECT);
|
||||
renders++;
|
||||
for (const f of found) findings.push({ page: p, width: w, theme: t, ...f });
|
||||
} catch (e) { findings.push({ page: p, width: w, theme: t, kind: 'RENDER', what: e.message.split('\n')[0].slice(0, 120), where: route, detail: '' }); }
|
||||
await ctx.close();
|
||||
}
|
||||
let i = 0; await Promise.all(Array.from({ length: Math.max(1, JOBS) }, async () => { while (i < jobs.length) await one(jobs[i++]); }));
|
||||
stop();
|
||||
return { renders, findings };
|
||||
}
|
||||
const fmt = f => ` FAIL ${f.page} ${f.width}px ${f.theme}: ${f.kind} "${f.what}" in ${f.where}${f.container ? ' (container ' + f.container + ')' : ''}${f.detail ? ': ' + f.detail : ''}`;
|
||||
|
||||
const browser = await pw.chromium.launch({ headless: true, args: ['--no-sandbox'] });
|
||||
try {
|
||||
if (args.includes('--self-test')) {
|
||||
// the fixture: the site's own stylesheet and bar, the exact /wallet band with its padding lost, a touching pair, an image over
|
||||
// its frame, a heading under the bar; then the same page with each fixed
|
||||
const site = join(repo, 'site');
|
||||
const fx = mkdtempSync(join(tmpdir(), 'padding-check-'));
|
||||
const nav = readFileSync(join(site, 'partials', 'nav.html'), 'utf8'), head = readFileSync(join(site, 'partials', 'head.html'), 'utf8');
|
||||
const css = readFileSync(join(site, 'site.css'), 'utf8');
|
||||
writeFileSync(join(fx, 'site.css'), css);
|
||||
const page = (broken) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>fixture</title>${head.replace(/href="\/fonts\//g, 'href="/nofonts/').replace(/url\(\/fonts\//g, 'url(/nofonts/')}
|
||||
<style>${broken ? '.band .container{padding-top:0!important;padding-bottom:0!important}.pair .btn+.btn{margin-left:0!important}.pair{gap:0!important}.frame img{width:120%!important;max-width:none!important}.page-hero{margin-top:-120px}' : ''}</style></head><body>
|
||||
${nav}
|
||||
<main id="main"><section class="page-hero"><div class="container"><h1>Fixture.</h1><p class="lead">A page for the padding check.</p></div></section>
|
||||
<section class="section"><div class="container"><div class="actions pair"><a class="btn primary" href="#a">One</a><a class="btn" href="#b">Two</a></div>
|
||||
<figure class="frame" style="max-width:400px;overflow:visible"><div class="bar"><i></i><i></i><i></i></div><img src="/icon-192.png" width="192" height="192" alt=""></figure></div></section></main>
|
||||
<div class="band"><div class="container"><div><h2>The miner pays this wallet.</h2><p>Install the miner, press Start, and the address it makes is the one this wallet imports with one click.</p></div><a href="/miner" class="btn dark">The miner</a></div></div>
|
||||
</body></html>`;
|
||||
writeFileSync(join(fx, 'broken.html'), page(true)); writeFileSync(join(fx, 'clean.html'), page(false));
|
||||
writeFileSync(join(fx, '404.html'), '<!doctype html><title>404</title>'); writeFileSync(join(fx, 'icon-192.png'), readFileSync(join(site, 'icon-192.png')));
|
||||
const saveW = WIDTHS.splice(0, WIDTHS.length, 1440), saveT = THEMES.splice(0, THEMES.length, 'dark');
|
||||
const r = await sweep(fx, ['broken', 'clean'], browser);
|
||||
rmSync(fx, { recursive: true, force: true });
|
||||
const broken = r.findings.filter(f => f.page === 'broken'), clean = r.findings.filter(f => f.page === 'clean');
|
||||
const kinds = new Set(broken.map(f => f.kind));
|
||||
const band = broken.find(f => f.kind === 'TIGHT' && /band/.test(f.container || ''));
|
||||
let ok = true;
|
||||
for (const k of ['TIGHT', 'TOUCH', 'OVERRUN', 'UNDER-BAR']) if (!kinds.has(k)) { console.error('self-test: the broken fixture did not raise ' + k); ok = false; }
|
||||
if (!band) { console.error('self-test: the /wallet band with its padding lost was not flagged TIGHT'); ok = false; }
|
||||
if (clean.length) { console.error('self-test: the clean fixture should pass\n' + clean.map(fmt).join('\n')); ok = false; }
|
||||
if (!ok) process.exit(1);
|
||||
console.log(`self-test passed: the band with no padding (${band.detail}), a touching pair, an image over its frame and a heading under the bar are caught; the clean fixture passes`);
|
||||
WIDTHS.push(...saveW); THEMES.push(...saveT);
|
||||
}
|
||||
if (args.includes('--site')) {
|
||||
const dir = opt('--site', '').startsWith('--') || !opt('--site', '') ? join(repo, 'site') : opt('--site');
|
||||
const pages = readdirSync(dir).filter(f => f.endsWith('.html')).map(f => f.replace(/\.html$/, '')).filter(p => !ONLY || p.includes(ONLY));
|
||||
const t0 = Date.now();
|
||||
const r = await sweep(dir, pages, browser);
|
||||
if (JSON_OUT) writeFileSync(JSON_OUT, JSON.stringify(r.findings, null, 1));
|
||||
for (const f of r.findings) console.log(fmt(f));
|
||||
const byKind = {}; for (const f of r.findings) byKind[f.kind] = (byKind[f.kind] || 0) + 1;
|
||||
console.log(`padding-check (site): ${r.renders} renders, ${r.findings.length} findings${r.findings.length ? ' (' + Object.entries(byKind).map(([k, n]) => k + ' ' + n).join(', ') + ')' : ''} in ${((Date.now() - t0) / 1000).toFixed(1)} s`);
|
||||
if (r.findings.length) process.exitCode = 1;
|
||||
}
|
||||
} finally { await browser.close(); }
|
||||
|
|
@ -79,6 +79,7 @@ tree_checks() {
|
|||
run "every served page carries the slim bar (mark, Mine, Network, Learn, Download) with every route in its panels and the sheet (self-test, then the tree)" bash -c 'node tools/ci/site-nav-check.mjs --self-test && node tools/ci/site-nav-check.mjs'
|
||||
run "vendor marks: site/lib/marks.mjs is brand/marks/vendor-marks.mjs byte for byte (the app and the site draw one set)" cmp brand/marks/vendor-marks.mjs site/lib/marks.mjs
|
||||
run "the phone menu opens and is seen at 390 px on every page (self-test first; needs the box or CI browser, says so without one)" node tools/site/sheet-test.mjs --self-test
|
||||
run "padding and visuals: text 24 px from every band, card and section edge, section padding on the scale, no touching controls, media inside its frame, no heading under the bar, at 390 to 1600 px both themes (known-failed fixture first)" bash -c 'node tools/ci/padding-check.mjs --self-test && node tools/ci/padding-check.mjs --site "${SITE_TMP:-.}/site"'
|
||||
run "ledger sentences present verbatim on their public pages" node tools/ci/ledger-text-check.mjs
|
||||
never_push_checks
|
||||
run "shell inside .github/workflows parses (bash -n, the PowerShell 5.1 rule)" node tools/ci/check-workflow-shell.mjs
|
||||
|
|
|
|||
Loading…
Reference in a new issue