Brand tile colors as generated stylesheet (CSP blocks inline styles), footer credit link

This commit is contained in:
Aurealibe
2026-07-10 04:31:15 +01:00
parent 5f3528cce1
commit 710854510b
7 changed files with 260 additions and 18 deletions
+1
View File
@@ -4,3 +4,4 @@ node_modules/
pnpm-lock.yaml
index.html
src/icons.generated.ts
src/styles/brands.generated.css
+30
View File
@@ -40,3 +40,33 @@ 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`)
+3
View File
@@ -114,5 +114,8 @@ const localeRoot = `${localePath(locale)}/`.replace('//', '/')
{t('footer.factsUpdated')}
<a href={localeRoot} class="text-dim">exitchatcontrol.org</a>
</small>
<p class="mt-3 mb-0 text-center font-mono text-xs text-dim">
by <Ext href="https://x.com/AureaLibe">Aurealibe</Ext>
</p>
</div>
</footer>
+4 -18
View File
@@ -26,23 +26,9 @@ if (!tool) {
throw new Error(`ToolCard: unknown tool id "${toolId}" (not in src/data/tools.json)`)
}
// Tile ink picked by WCAG contrast against the brand color — white ink fails
// on several light brands (the axe lesson from the reviewed PR).
function luminance(hex: string): number {
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!
}
const brand = tool.color ?? '#7d5a10'
const ink = (() => {
const L = luminance(brand)
const white = 1.05 / (L + 0.05)
const dark = (L + 0.05) / 0.05
return white >= dark ? '#faf7ee' : '#14161a'
})()
// Tile colors live in src/styles/brands.generated.css (generated from
// tools.json by scripts/gen-icons.mjs) — a stylesheet, because the
// production CSP (style-src 'self') blocks style= attributes.
const iconPath = tool.iconSlug ? BRAND_ICONS[tool.iconSlug] : undefined
const locale = (Astro.currentLocale ?? defaultLocale) as Locale
@@ -72,7 +58,7 @@ const labels = LABELS[locale]
<article class="tool-card" id={tool.id} data-level={tool.level ?? undefined}>
<header class="tool-head">
<span class="tool-logo" aria-hidden="true" style={`--b:${brand};--ink-on-brand:${ink}`}>
<span class="tool-logo" aria-hidden="true">
{
iconPath ? (
<svg viewBox="0 0 24 24" fill="currentColor">
+1
View File
@@ -143,6 +143,7 @@
"www.wired.com",
"www.wireguard.com",
"www.yubico.com",
"x.com",
"yunohost.org"
]
}
+220
View File
@@ -0,0 +1,220 @@
/* 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. */
#t-signal .tool-logo {
background: #3a76f0;
color: #14161a;
}
#t-simplex .tool-logo {
background: #1e63d0;
color: #faf7ee;
}
#t-session .tool-logo {
background: #178a5a;
color: #14161a;
}
#t-briar .tool-logo {
background: #c1272d;
color: #faf7ee;
}
#t-bitchat .tool-logo {
background: #e8871e;
color: #14161a;
}
#t-molly .tool-logo {
background: #6d4bb3;
color: #faf7ee;
}
#t-protonmail .tool-logo {
background: #6d4aff;
color: #faf7ee;
}
#t-tuta .tool-logo {
background: #b01e2e;
color: #faf7ee;
}
#t-mailbox .tool-logo {
background: #2470d4;
color: #faf7ee;
}
#t-mullvadbrowser .tool-logo {
background: #2b4b8f;
color: #faf7ee;
}
#t-firefox .tool-logo {
background: #e66000;
color: #14161a;
}
#t-brave .tool-logo {
background: #fb542b;
color: #14161a;
}
#t-ublock .tool-logo {
background: #7a0d0d;
color: #faf7ee;
}
#t-search .tool-logo {
background: #de5833;
color: #14161a;
}
#t-quad9 .tool-logo {
background: #1a5fb4;
color: #faf7ee;
}
#t-mullvaddns .tool-logo {
background: #2b4b8f;
color: #faf7ee;
}
#t-cloudflare .tool-logo {
background: #f38020;
color: #14161a;
}
#t-pihole .tool-logo {
background: #96060c;
color: #faf7ee;
}
#t-mullvad .tool-logo {
background: #2b4b8f;
color: #faf7ee;
}
#t-protonvpn .tool-logo {
background: #6d4aff;
color: #faf7ee;
}
#t-ivpn .tool-logo {
background: #3b5bdb;
color: #faf7ee;
}
#t-protondrive .tool-logo {
background: #6d4aff;
color: #faf7ee;
}
#t-cryptomator .tool-logo {
background: #26a69a;
color: #14161a;
}
#t-nc-storage .tool-logo {
background: #0082c9;
color: #14161a;
}
#t-bitwarden .tool-logo {
background: #175ddc;
color: #faf7ee;
}
#t-keepassxc .tool-logo {
background: #2c6b9e;
color: #faf7ee;
}
#t-protonpass .tool-logo {
background: #6d4aff;
color: #faf7ee;
}
#t-yubikey .tool-logo {
background: #84bd00;
color: #14161a;
}
#t-aegis .tool-logo {
background: #2c5c9c;
color: #faf7ee;
}
#t-mastodon .tool-logo {
background: #6364ff;
color: #14161a;
}
#t-nostr .tool-logo {
background: #8e30eb;
color: #faf7ee;
}
#t-element .tool-logo {
background: #0dbd8b;
color: #14161a;
}
#t-fediverse .tool-logo {
background: #ea580c;
color: #14161a;
}
#t-bitcoin .tool-logo {
background: #f7931a;
color: #14161a;
}
#t-monero .tool-logo {
background: #ff6600;
color: #14161a;
}
#t-localai .tool-logo {
background: #111111;
color: #faf7ee;
}
#t-cloudai .tool-logo {
background: #de5833;
color: #14161a;
}
#t-maps .tool-logo {
background: #0a7d33;
color: #faf7ee;
}
#t-notes .tool-logo {
background: #2e7d6b;
color: #faf7ee;
}
#t-visio .tool-logo {
background: #1d6fb8;
color: #faf7ee;
}
#t-photos .tool-logo {
background: #7b3fbf;
color: #faf7ee;
}
#t-alias .tool-logo {
background: #c0392b;
color: #faf7ee;
}
#t-backups .tool-logo {
background: #5a6570;
color: #faf7ee;
}
#t-databrokers .tool-logo {
background: #b8860b;
color: #14161a;
}
#t-yunohost .tool-logo {
background: #e29100;
color: #14161a;
}
#t-nextcloud .tool-logo {
background: #0082c9;
color: #14161a;
}
#t-synapse .tool-logo {
background: #0dbd8b;
color: #14161a;
}
#t-tailscale .tool-logo {
background: #242424;
color: #faf7ee;
}
#t-website .tool-logo {
background: #7a4ddb;
color: #faf7ee;
}
#t-tor .tool-logo {
background: #7d4698;
color: #faf7ee;
}
#t-grapheneos .tool-logo {
background: #1f8a70;
color: #14161a;
}
#t-linux .tool-logo {
background: #2b2b2b;
color: #faf7ee;
}
#t-tails .tool-logo {
background: #56347c;
color: #faf7ee;
}
#t-qubes .tool-logo {
background: #3874d8;
color: #14161a;
}
+1
View File
@@ -1,5 +1,6 @@
@import 'tailwindcss';
@import './print.css';
@import './brands.generated.css';
/* ═══════════════════════════════════════════════════════════════════════════
Exit Chat Control · design tokens ("stamped dossier" identity)