Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | 10x 10x 137x 137x 137x 137x 137x 137x 137x 137x 4x 4x 4x 4x 4x 1x 3x 138x 135x 135x 135x 135x 135x 135x 135x 3x 61x 61x 58x 58x 3x | import fs from 'node:fs';
import path from 'node:path';
/**
* Link-tile icon registry for the `::link-card` directive's `icon` attribute.
*
* Each SVG in `src/icons/link-tiles/` registers under its file name (without
* `.svg`), plus a forgiving slug alias ("Guideline 2" → "guideline-2"). Icons
* are read once, on first lookup (server-side, build time); adding or renaming
* one needs a dev-server restart. See the folder's README.
*/
// The brand indigo the icons are exported with. Rewriting it to currentColor
// lets each icon inherit the brand color and hover state from CSS.
const BRAND_HEX = /#5867e8/gi;
// The icons are drawn on a 64-unit frame with a 1.75 stroke, which reads too
// thin at the 40px / 50px tile size. Every stroke is rewritten to this width.
const STROKE_WIDTH = 2.5;
/** Normalize an exported SVG so CSS can size and color it. */
export function normalizeIconSvg(svg: string): string {
let out = svg
.replace(/<\?xml[\s\S]*?\?>/gi, '')
.replace(/<!DOCTYPE[\s\S]*?>/gi, '')
.trim();
// Strip the root's fixed width / height so CSS controls the size.
out = out.replace(/(<svg\b[^>]*?)\swidth="[^"]*"/i, '$1').replace(/(<svg\b[^>]*?)\sheight="[^"]*"/i, '$1');
// White knockouts and `none` stay as they are.
out = out.replace(BRAND_HEX, 'currentColor');
// Fill-only icons (no stroke-width) are untouched.
out = out.replace(/stroke-width="[\d.]+"/gi, `stroke-width="${STROKE_WIDTH}"`);
if (!/\sfill=/.test(out)) out = out.replace(/<svg\b/i, '<svg fill="currentColor"');
// Decorative: the card's title carries the meaning.
Eif (!/aria-hidden=/.test(out)) out = out.replace(/<svg\b/i, '<svg aria-hidden="true" focusable="false"');
return out;
}
/** Canonical lookup key: lowercase, non-alphanumerics collapsed to hyphens. */
function slug(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
type Registry = { icons: Record<string, string>; names: string[] };
function loadIcons(): Registry {
const iconDir = path.join(process.cwd(), 'src/icons/link-tiles');
const icons: Record<string, string> = {};
const names: string[] = [];
let files: string[];
try {
files = fs.readdirSync(iconDir);
} catch (error) {
// Without the folder every icon name would fall through to a broken image URL.
throw new Error(`Link-tile icons: cannot read ${iconDir}`, { cause: error });
}
for (const file of files) {
if (!file.toLowerCase().endsWith('.svg')) continue;
const name = file.replace(/\.svg$/i, '');
try {
const svg = normalizeIconSvg(fs.readFileSync(path.join(iconDir, file), 'utf8'));
icons[name] = svg;
names.push(name);
const alias = slug(name);
if (!(alias in icons)) icons[alias] = svg;
} catch {
/* skip unreadable files */
}
}
return { icons, names: names.sort() };
}
let registry: Registry | undefined;
function getRegistry(): Registry {
registry ??= loadIcons();
return registry;
}
/** Inlined SVG markup for a registered icon name, or null. */
export function getLinkIcon(name: string): string | null {
const { icons } = getRegistry();
return icons[name] ?? icons[slug(name)] ?? null;
}
/** Canonical icon names (one per file), sorted. */
export function linkIconNames(): string[] {
return getRegistry().names;
}
|