73 lines
3.1 KiB
JavaScript
73 lines
3.1 KiB
JavaScript
/* Generates src/icons.generated.ts from the simple-icons npm package —
|
|
build-time only, so the site never phones a CDN (v1 loaded brand logos
|
|
from cdn.simpleicons.org at runtime: a third-party request on a privacy
|
|
guide). The slug list is DERIVED from the data files (tools.json +
|
|
directory.json iconSlug fields) so it can never drift from the content.
|
|
Slugs missing from the installed set fall back to the letter tile at
|
|
render time. Run: pnpm icons */
|
|
import { readFileSync, writeFileSync } from 'node:fs'
|
|
import * as icons from 'simple-icons'
|
|
|
|
const dataFiles = ['../src/data/tools.json', '../src/data/directory.json']
|
|
const slugs = new Set(['github']) // footer link icon
|
|
for (const file of dataFiles) {
|
|
const entries = JSON.parse(readFileSync(new URL(file, import.meta.url), 'utf8'))
|
|
for (const entry of entries) {
|
|
if (entry.iconSlug) slugs.add(entry.iconSlug)
|
|
}
|
|
}
|
|
|
|
const bySlug = new Map(Object.values(icons).map((i) => [i.slug, i]))
|
|
const found = []
|
|
const missing = []
|
|
for (const slug of [...slugs].sort()) {
|
|
const icon = bySlug.get(slug)
|
|
if (icon) found.push(icon)
|
|
else missing.push(slug)
|
|
}
|
|
|
|
const lines = found.map((i) => ` ${JSON.stringify(i.slug)}: ${JSON.stringify(i.path)},`)
|
|
const out = `/* AUTO-GENERATED by scripts/gen-icons.mjs — do not edit.
|
|
SVG path data from the simple-icons npm package (CC0), inlined at build
|
|
time. Zero runtime CDN request. Missing slugs at generation time: ${
|
|
missing.length ? missing.join(', ') : 'none'
|
|
}. */
|
|
export const BRAND_ICONS: Record<string, string> = {
|
|
${lines.join('\n')}
|
|
}
|
|
`
|
|
writeFileSync(new URL('../src/icons.generated.ts', import.meta.url), out)
|
|
console.log(
|
|
`icons: ${found.length} embedded, ${missing.length} missing${missing.length ? ` (${missing.join(', ')})` : ''}`,
|
|
)
|
|
|
|
// Brand tile colors as a GENERATED STYLESHEET (not inline style attributes:
|
|
// the production CSP is style-src 'self', which blocks style= attributes).
|
|
// Tile ink is picked by WCAG contrast against the brand color — white ink
|
|
// fails on several light brands.
|
|
function luminance(hex) {
|
|
const n = hex.replace('#', '')
|
|
const [r, g, b] = [0, 2, 4].map((i) => {
|
|
const c = parseInt(n.slice(i, i + 2), 16) / 255
|
|
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
|
|
})
|
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
}
|
|
function inkFor(hex) {
|
|
const L = luminance(hex)
|
|
return 1.05 / (L + 0.05) >= (L + 0.05) / 0.05 ? '#faf7ee' : '#14161a'
|
|
}
|
|
|
|
const tools = JSON.parse(readFileSync(new URL('../src/data/tools.json', import.meta.url), 'utf8'))
|
|
const rules = tools
|
|
.filter((t) => t.color)
|
|
.map((t) => `#${t.id} .tool-logo {\n background: ${t.color};\n color: ${inkFor(t.color)};\n}`)
|
|
const css = `/* AUTO-GENERATED by scripts/gen-icons.mjs — do not edit.
|
|
Brand tile background + WCAG-picked ink per tool (from tools.json).
|
|
A stylesheet, not style= attributes: the CSP (style-src 'self')
|
|
blocks inline styles in production. */
|
|
${rules.join('\n')}
|
|
`
|
|
writeFileSync(new URL('../src/styles/brands.generated.css', import.meta.url), css)
|
|
console.log(`brands: ${rules.length} tile rules → src/styles/brands.generated.css`)
|