prometheus: SVG icons for the 8 categories

Replaces the emoji glyphs (💬🛡️💸🎮🧰🎧⚙️✨) in the category grid with
dedicated SVG icons under /static/icons/cat-<slug>.svg, following the
same visual family as the sibling .x marks (theseus, sirius, ariadne,
pithos, aegis): 12-radius dark square + acid-green mark, legible at
16 px tab-icon size and 40 px grid size.

Designs chosen to not blur with the Silent Mode product icons also
rendered on the site:
  cat-communication  speech bubble + three dots (not an envelope — too
                     close to the Hephaestus letter imagery elsewhere)
  cat-privacy        shield + checkmark (DISTINCT from aegis.svg's
                     shield+chevron, so category + listing don't collide)
  cat-wallets        stacked coins + B serif (hints at the BCH coin)
  cat-games          stylised controller (d-pad + two action buttons)
  cat-devtools       code brackets < / > (reads as "code" at 16 px
                     where a wrench/hammer blurs with the Utilities gear)
  cat-media          play triangle in a circle (equally "player" and
                     "publish/broadcast")
  cat-utilities      gear with hollow centre (less visual weight than
                     the Media circle; the two side-by-side don't
                     crowd each other)
  cat-other          sparkle burst + two smaller sparkles (sibling of
                     sirius.svg's 4-point mark, extras so the two don't
                     look identical)

pages-browse.js: CATEGORIES[].icon is now the path to the SVG, not an
emoji string. The three places the icon rendered — grid card, category
hero, and the inline chip on listing detail — all now use <img> at the
size they need (40, 56, 14 respectively). The CSS entry
`.cat .ico` loses the font-size + grayscale (text styling) and gains
width/height for the image plus a tiny hover rotation that matches the
existing lift transform.

Also: when a listing has no `icon` of its own, the detail page's icon
slot now falls back to the category's SVG (centered + padded) instead
of a plain 📦 emoji, so the detail hero always has a vector icon.
This commit is contained in:
Local Dev 2026-10-22 21:30:13 +03:00
parent a6606d0c49
commit 8e0830f046
10 changed files with 86 additions and 13 deletions

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<!-- Communication — speech bubble with a tail hooking down and left. -->
<rect width="64" height="64" rx="12" fill="#0b0e14"/>
<path d="M12 20 Q 12 12 20 12 L 44 12 Q 52 12 52 20 L 52 36 Q 52 44 44 44 L 26 44 L 16 52 L 18 44 Q 12 44 12 36 Z" fill="#d6ff3d"/>
<circle cx="23" cy="28" r="2.5" fill="#0b0e14"/>
<circle cx="32" cy="28" r="2.5" fill="#0b0e14"/>
<circle cx="41" cy="28" r="2.5" fill="#0b0e14"/>
</svg>

After

Width:  |  Height:  |  Size: 489 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<!-- Developer tools — code brackets < / >. Reads at 16 px as "code" rather
than hammer/wrench shapes which blur with the Utilities gear. -->
<rect width="64" height="64" rx="12" fill="#0b0e14"/>
<path d="M22 20 L10 32 L22 44" fill="none" stroke="#d6ff3d" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M42 20 L54 32 L42 44" fill="none" stroke="#d6ff3d" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M37 16 L27 48" stroke="#d6ff3d" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 618 B

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<!-- Games — a stylised game controller: rounded body + d-pad on the left,
two action buttons on the right. -->
<rect width="64" height="64" rx="12" fill="#0b0e14"/>
<path d="M14 32 Q 14 20 24 20 L 40 20 Q 50 20 50 32 L 50 42 Q 50 50 42 48 L 36 44 L 28 44 L 22 48 Q 14 50 14 42 Z" fill="#d6ff3d"/>
<path d="M20 32 L28 32 M24 28 L24 36" stroke="#0b0e14" stroke-width="3" stroke-linecap="round"/>
<circle cx="39" cy="30" r="3" fill="#0b0e14"/>
<circle cx="45" cy="34" r="3" fill="#0b0e14"/>
</svg>

After

Width:  |  Height:  |  Size: 578 B

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<!-- Media & publishing — a play triangle sitting inside a circle. Reads
equally as "player" and "publish/broadcast". -->
<rect width="64" height="64" rx="12" fill="#0b0e14"/>
<circle cx="32" cy="32" r="22" fill="#d6ff3d"/>
<path d="M27 22 L45 32 L27 42 Z" fill="#0b0e14"/>
</svg>

After

Width:  |  Height:  |  Size: 359 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<!-- Everything else — one big sparkle plus two small ones. The sibling of
sirius.svg's 4-point mark, with extras so the two don't look identical. -->
<rect width="64" height="64" rx="12" fill="#0b0e14"/>
<path d="M32 10 L36 26 L52 30 L36 34 L32 50 L28 34 L12 30 L28 26 Z" fill="#d6ff3d"/>
<path d="M48 48 L50 42 L52 48 L50 54 Z" fill="#d6ff3d" opacity="0.75"/>
<path d="M14 50 L16 44 L18 50 L16 56 Z" fill="#d6ff3d" opacity="0.75"/>
</svg>

After

Width:  |  Height:  |  Size: 521 B

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<!-- Privacy & security — shield with a checkmark inside. Distinct from the
Aegis icon (chevron) so a category card and a listing card don't blur. -->
<rect width="64" height="64" rx="12" fill="#0b0e14"/>
<path d="M32 8 L50 16 L50 32 C 50 44 42 52 32 56 C 22 52 14 44 14 32 L 14 16 Z" fill="#d6ff3d"/>
<path d="M22 32 L29 39 L42 24" fill="none" stroke="#0b0e14" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 513 B

View file

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<!-- Utilities — gear with 8 teeth. The hollow centre keeps the acid mass
below the Media circle's visual weight so the two don't crowd each
other in the grid. -->
<rect width="64" height="64" rx="12" fill="#0b0e14"/>
<path d="M32 10 L35 10 L37 16 L42 17 L46 13 L48 15 L46 21 L50 25 L56 23 L57 26 L52 30 L52 34 L57 38 L56 41 L50 39 L46 43 L48 49 L46 51 L42 47 L37 48 L35 54 L32 54 L29 54 L27 48 L22 47 L18 51 L16 49 L18 43 L14 39 L8 41 L7 38 L12 34 L12 30 L7 26 L8 23 L14 25 L18 21 L16 15 L18 13 L22 17 L27 16 L29 10 Z" fill="#d6ff3d"/>
<circle cx="32" cy="32" r="8" fill="#0b0e14"/>
</svg>

After

Width:  |  Height:  |  Size: 676 B

View file

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<!-- Wallets & payments — stacked coins with a small notch to hint at the BCH 'B'. -->
<rect width="64" height="64" rx="12" fill="#0b0e14"/>
<ellipse cx="32" cy="20" rx="18" ry="6" fill="#d6ff3d"/>
<path d="M14 20 L14 44 Q 14 50 32 50 Q 50 50 50 44 L 50 20 Q 50 26 32 26 Q 14 26 14 20 Z" fill="#d6ff3d" opacity="0.5"/>
<ellipse cx="32" cy="32" rx="18" ry="6" fill="#d6ff3d"/>
<path d="M14 32 L14 44 Q 14 50 32 50 Q 50 50 50 44 L 50 32" fill="none" stroke="#d6ff3d" stroke-width="2.5" stroke-linejoin="round"/>
<text x="32" y="36" text-anchor="middle" font-family="serif" font-weight="700" font-size="10" fill="#0b0e14">B</text>
</svg>

After

Width:  |  Height:  |  Size: 712 B

View file

@ -168,7 +168,9 @@ footer .fine{max-width:660px;margin:.8rem auto 0;line-height:1.7}
display:flex;flex-direction:column;gap:.6rem;
}
.cat:hover{transform:translateY(-3px);border-color:rgba(214,255,61,.35);background:var(--panel2)}
.cat .ico{font-size:26px;line-height:1;filter:grayscale(.2)}
.cat .ico{width:40px;height:40px;display:block;transition:transform .18s ease}
.cat:hover .ico{transform:rotate(-3deg)}
.cat.empty .ico{opacity:.75}
.cat h3{margin:.1rem 0 0;font-size:1.02rem}
.cat .count{color:var(--dim);font-size:12px;letter-spacing:.02em;margin-top:auto;padding-top:.4rem}
.cat.empty .count::before{content:"— ";color:var(--dim)}

View file

@ -30,22 +30,26 @@ import { softwareApplicationLd } from './seo.js';
* allow-list in listings.js. The browse grid picks up the new row
* automatically.
*/
// `icon` is a path to an SVG under /static/icons/. Rendered via <img> at the
// three sizes the templates use (26 px in the grid, 40 px in the category
// hero, 1em in the inline chip on listing detail pages). The icons share a
// family with the sibling .x marks: 12-radius dark square + acid-green mark.
export const CATEGORIES = [
{ slug: 'communication', icon: '💬', title: 'Communication',
{ slug: 'communication', icon: '/static/icons/cat-communication.svg', title: 'Communication',
blurb: `Messengers, voice, boards, group chats. Clients that route through peers rather than someone else's server.` },
{ slug: 'privacy', icon: '🛡️', title: 'Privacy & security',
{ slug: 'privacy', icon: '/static/icons/cat-privacy.svg', title: 'Privacy & security',
blurb: `Tor and mix-net tooling, key and vault managers, disk crypto. The things a conventional store delists first when a government writes.` },
{ slug: 'wallets', icon: '💸', title: 'Wallets & payments',
{ slug: 'wallets', icon: '/static/icons/cat-wallets.svg', title: 'Wallets & payments',
blurb: `BCH wallets, cross-chain bridges, point-of-sale terminals. The chain publishes the listing and the chain settles the payment.` },
{ slug: 'games', icon: '🎮', title: 'Games',
{ slug: 'games', icon: '/static/icons/cat-games.svg', title: 'Games',
blurb: `Standalone titles, on-chain leaderboards, in-app purchases that reach the publisher instead of a 30% toll.` },
{ slug: 'devtools', icon: '🧰', title: 'Developer tools',
{ slug: 'devtools', icon: '/static/icons/cat-devtools.svg', title: 'Developer tools',
blurb: `SDKs, libraries, indexers, faucets, protocol utilities. The pieces other apps on this list are built out of.` },
{ slug: 'media', icon: '🎧', title: 'Media & publishing',
{ slug: 'media', icon: '/static/icons/cat-media.svg', title: 'Media & publishing',
blurb: `Readers, players, one-file blogs, podcast hosts. Clients that find their audience without going through an aggregator.` },
{ slug: 'utilities', icon: '⚙️', title: 'Utilities',
{ slug: 'utilities', icon: '/static/icons/cat-utilities.svg', title: 'Utilities',
blurb: `File tools, format converters, backup, calculators. The small sharp things that solve one problem and do not ask for an account first.` },
{ slug: 'other', icon: '✨', title: 'Everything else',
{ slug: 'other', icon: '/static/icons/cat-other.svg', title: 'Everything else',
blurb: `Anything that does not fit above. New categories are proposed in-band — any listing can declare one and clients will offer to add it.` },
];
@ -77,7 +81,7 @@ export function renderCategoriesGrid({ verbose = true } = {}) {
const emptyCls = n === 0 ? ' empty' : '';
const countLabel = `${n} ${n === 1 ? 'listing' : 'listings'}`;
return `<a class="cat${emptyCls}" href="/c/${c.slug}">
<div class="ico">${c.icon}</div>
<img class="ico" src="${c.icon}" alt="" width="40" height="40">
<h3>${escapeHtml(c.title)}</h3>
${verbose ? `<p>${escapeHtml(c.blurb)}</p>` : ''}
<span class="count">${countLabel}</span>
@ -102,7 +106,7 @@ export function categoryPage(req, slug) {
here,
body: `
<header class="hero compact">
<div class="mark" style="font-size:40px">${cat.icon}</div>
<img class="mark" src="${cat.icon}" alt="" width="56" height="56" style="display:block;margin:0 auto 1rem">
<h1>${escapeHtml(title)}</h1>
<p class="tag">${escapeHtml(cat.blurb)}</p>
</header>
@ -136,7 +140,7 @@ export function appPage(req, publisher, slug) {
const cat = CATEGORY_BY_SLUG[row.category];
const catChip = cat
? `<a href="/c/${cat.slug}" class="pill p-upcoming" style="text-decoration:none">${cat.icon} ${escapeHtml(cat.title)}</a>`
? `<a href="/c/${cat.slug}" class="pill p-upcoming" style="text-decoration:none;display:inline-flex;align-items:center;gap:4px"><img src="${cat.icon}" alt="" width="14" height="14" style="vertical-align:-2px"> ${escapeHtml(cat.title)}</a>`
: '';
const scanReport = safeParse(row.scan_report);
@ -150,7 +154,10 @@ export function appPage(req, publisher, slug) {
const iconHtml = row.icon
? `<img src="${escapeAttr(row.icon)}" alt="" width="80" height="80"
style="width:80px;height:80px;border-radius:16px;border:1px solid var(--line);background:var(--panel);object-fit:cover">`
: `<div style="width:80px;height:80px;border-radius:16px;border:1px solid var(--line);background:var(--panel);display:grid;place-items:center;font-size:36px">${cat?.icon || '📦'}</div>`;
: cat?.icon
? `<img src="${cat.icon}" alt="" width="80" height="80"
style="width:80px;height:80px;border-radius:16px;border:1px solid var(--line);background:var(--panel);padding:12px">`
: `<div style="width:80px;height:80px;border-radius:16px;border:1px solid var(--line);background:var(--panel);display:grid;place-items:center;font-size:36px">📦</div>`;
return {
title: `${row.name} by ${row.publisher}`,