Complete MIM4U A+ evidence: potrace SVG, SiteHeader, and full-site a11y.
Consolidate header via SiteHeader, regenerate potrace nav lockups, add screenshot matrix and Lighthouse site audit scripts, fix PWA dismiss aria-label, and bump institutional grade to A+ (99/100). Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Full-site Lighthouse accessibility audit (live URL).
|
||||
* Usage: node scripts/a11y/audit-site-lighthouse.mjs [--url=https://mim4u.org]
|
||||
*/
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = join(__dirname, '../..')
|
||||
const outDir = join(repoRoot, 'reports/status')
|
||||
|
||||
const urlArg = process.argv.find((a) => a.startsWith('--url='))
|
||||
const url = urlArg ? urlArg.split('=')[1] : 'https://mim4u.org'
|
||||
const lhOut = join(outDir, 'lighthouse-site-a11y-latest.json')
|
||||
|
||||
mkdirSync(outDir, { recursive: true })
|
||||
const chromePath = process.env.CHROME_PATH || '/usr/bin/google-chrome'
|
||||
|
||||
const result = spawnSync(
|
||||
'npx',
|
||||
[
|
||||
'--yes',
|
||||
'lighthouse',
|
||||
url,
|
||||
'--only-categories=accessibility,performance,best-practices,seo',
|
||||
'--skip-audits=full-page-screenshot,screenshot-thumbnails',
|
||||
'--output=json',
|
||||
`--output-path=${lhOut}`,
|
||||
'--chrome-flags=--headless --no-sandbox',
|
||||
],
|
||||
{
|
||||
cwd: repoRoot,
|
||||
env: { ...process.env, CHROME_PATH: chromePath },
|
||||
encoding: 'utf8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
}
|
||||
)
|
||||
|
||||
if (result.status !== 0 && !readFileSync(lhOut, 'utf8').includes('"categories"')) {
|
||||
console.error((result.stderr || result.stdout || 'Lighthouse failed').trim())
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const raw = JSON.parse(readFileSync(lhOut, 'utf8'))
|
||||
const scores = Object.fromEntries(
|
||||
Object.entries(raw.categories).map(([k, v]) => [k, Math.round(v.score * 100)])
|
||||
)
|
||||
const failing = raw.categories.accessibility.auditRefs
|
||||
.filter((ref) => {
|
||||
const audit = raw.audits[ref.id]
|
||||
return audit && audit.score !== null && audit.score < 1
|
||||
})
|
||||
.map((ref) => ({ id: ref.id, title: raw.audits[ref.id].title }))
|
||||
|
||||
const report = {
|
||||
schemaVersion: '1.0.0',
|
||||
assessmentDate: new Date().toISOString().slice(0, 10),
|
||||
url,
|
||||
scores,
|
||||
accessibilityFailingAudits: failing,
|
||||
reportPath: lhOut,
|
||||
}
|
||||
|
||||
const outPath = join(outDir, 'mim4u-site-lighthouse-latest.json')
|
||||
writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`)
|
||||
|
||||
console.log('Lighthouse scores:', scores)
|
||||
if (failing.length) {
|
||||
failing.forEach((f) => console.log(` a11y FAIL: ${f.id} — ${f.title}`))
|
||||
}
|
||||
console.log(`Report: ${outPath}`)
|
||||
process.exit(scores.accessibility >= 95 ? 0 : 1)
|
||||
@@ -50,6 +50,9 @@ convert "$OUT/logo-horizontal-nav.png" -quality 88 -define webp:method=6 "$OUT/l
|
||||
|
||||
convert "$symbol" -trim +repage -background none -resize 512x512\> PNG32:"$OUT/logo-symbol-nav.png"
|
||||
|
||||
echo "Tracing nav lockups to SVG (potrace)..."
|
||||
node "$MIM_ROOT/scripts/brand/trace-nav-svg.mjs"
|
||||
|
||||
echo "Exporting square PNG/WebP..."
|
||||
convert "$square" -strip "$OUT/logo-square.png"
|
||||
convert "$square" -quality 88 -define webp:method=6 "$OUT/logo-square.webp"
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Trace nav PNG lockups to SVG via potrace (high-fidelity paths from approved raster).
|
||||
* Usage: node scripts/brand/trace-nav-svg.mjs
|
||||
*/
|
||||
import { promises as fs } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import potrace from 'potrace'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const brandDir = path.join(__dirname, '../../public/brand')
|
||||
|
||||
const traces = [
|
||||
{
|
||||
input: 'logo-horizontal-nav.png',
|
||||
output: 'logo-horizontal-nav.svg',
|
||||
title: 'Miracles in Motion Foundation',
|
||||
potrace: {
|
||||
color: 'auto',
|
||||
background: 'transparent',
|
||||
turdSize: 2,
|
||||
optTolerance: 0.2,
|
||||
threshold: 180,
|
||||
},
|
||||
},
|
||||
{
|
||||
input: 'logo-symbol-nav.png',
|
||||
output: 'logo-symbol-nav.svg',
|
||||
title: 'Miracles in Motion Foundation symbol',
|
||||
potrace: {
|
||||
color: 'auto',
|
||||
background: 'transparent',
|
||||
turdSize: 2,
|
||||
optTolerance: 0.15,
|
||||
threshold: 200,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
function tracePng(inputPath, options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
potrace.trace(inputPath, options, (err, svg) => {
|
||||
if (err) reject(err)
|
||||
else resolve(svg)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeSvg(svg, title) {
|
||||
let out = svg.replace(/<\?xml[^>]*>\s*/i, '')
|
||||
if (!out.includes('aria-hidden')) {
|
||||
out = out.replace(/<svg\b/, '<svg role="img" aria-hidden="true"')
|
||||
}
|
||||
if (!out.includes('<title>')) {
|
||||
out = out.replace(/<svg([^>]*)>/, `<svg$1>\n <title>${title}</title>`)
|
||||
}
|
||||
return out.trim() + '\n'
|
||||
}
|
||||
|
||||
async function main() {
|
||||
for (const job of traces) {
|
||||
const inputPath = path.join(brandDir, job.input)
|
||||
const outputPath = path.join(brandDir, job.output)
|
||||
await fs.access(inputPath)
|
||||
const raw = await tracePng(inputPath, job.potrace)
|
||||
const svg = normalizeSvg(raw, job.title)
|
||||
await fs.writeFile(outputPath, svg)
|
||||
console.log(`Wrote ${job.output} (${svg.length} bytes)`)
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Capture header logo screenshot matrix: viewport × color scheme.
|
||||
* Usage: node scripts/evidence/capture-header-screenshots.mjs [--url URL] [--out DIR]
|
||||
*/
|
||||
import { mkdirSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
const repoRoot = join(__dirname, '../..')
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const urlArg = args.find((a) => a.startsWith('--url='))
|
||||
const outArg = args.find((a) => a.startsWith('--out='))
|
||||
const url = urlArg ? urlArg.split('=')[1] : process.env.MIM4U_URL || 'https://mim4u.org'
|
||||
const outDir =
|
||||
outArg?.split('=')[1] ||
|
||||
join(repoRoot, 'reports/status/evidence/header-logo-screenshots')
|
||||
|
||||
const viewports = [
|
||||
{ id: 'mobile', width: 390, height: 844 },
|
||||
{ id: 'tablet', width: 768, height: 1024 },
|
||||
{ id: 'desktop', width: 1280, height: 800 },
|
||||
]
|
||||
|
||||
mkdirSync(outDir, { recursive: true })
|
||||
|
||||
const browser = await chromium.launch({ headless: true })
|
||||
const manifest = {
|
||||
capturedAt: new Date().toISOString(),
|
||||
url,
|
||||
shots: [],
|
||||
}
|
||||
|
||||
try {
|
||||
for (const scheme of ['light', 'dark']) {
|
||||
for (const vp of viewports) {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: vp.width, height: vp.height },
|
||||
colorScheme: scheme,
|
||||
})
|
||||
const page = await context.newPage()
|
||||
await page.goto(url, { waitUntil: 'networkidle', timeout: 60000 })
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('cookie-consent', 'accepted')
|
||||
})
|
||||
await page.reload({ waitUntil: 'networkidle' })
|
||||
const nav = page.locator('nav.mim-site-nav').first()
|
||||
await nav.waitFor({ state: 'visible', timeout: 15000 })
|
||||
|
||||
const file = `${vp.id}-${scheme}.png`
|
||||
const filePath = join(outDir, file)
|
||||
await nav.screenshot({ path: filePath })
|
||||
manifest.shots.push({ file, viewport: vp.id, colorScheme: scheme, width: vp.width })
|
||||
await context.close()
|
||||
console.log(`Captured ${file}`)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await browser.close()
|
||||
}
|
||||
|
||||
writeFileSync(join(outDir, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`)
|
||||
console.log(`Manifest: ${join(outDir, 'manifest.json')}`)
|
||||
Reference in New Issue
Block a user