/**
* Favicon helpers.
*
* The static fallback in index.html is a neutral monogram so the tab is never
* left without an icon. Once branding is known we override it with the custom
* logo (or a brand-letter monogram) via {@link setFavicon}.
*/
/** Point the page favicon at `href`, creating the if needed. */
export function setFavicon(href: string): void {
if (!href) return;
let link = document.querySelector("link[rel*='icon']");
if (!link) {
link = document.createElement('link');
link.rel = 'icon';
document.head.appendChild(link);
}
link.href = href;
}
/**
* Generate a square monogram favicon (SVG data URI) from a brand letter.
* Used when the deployment has no custom logo, so every page still gets an
* icon that matches the brand letter instead of the browser's blank default.
*/
export function letterFaviconDataUri(letter: string): string {
const ch = (letter || 'V').trim().charAt(0).toUpperCase() || 'V';
const svg =
``;
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}
/**
* Render `src` (e.g. a square custom logo) into a rounded-corner PNG data URI
* so the favicon gets the same rounded tile as the header logo, instead of
* hard square corners.
*
* `radiusRatio` 0.3 mirrors the header tile (rounded-linear-lg = 12px on a 40px
* tile). Returns null if the image can't be loaded or the canvas is tainted —
* the caller should fall back to the raw `src`.
*/
export async function roundedFaviconDataUri(
src: string,
size = 64,
radiusRatio = 0.3,
): Promise {
if (!src) return null;
try {
const img = await loadImage(src);
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
if (!ctx) return null;
traceRoundedRect(ctx, size, size * radiusRatio);
ctx.clip();
// object-fit: cover — fill the rounded tile, center-cropping any overflow.
const scale = Math.max(size / img.width, size / img.height);
const dw = img.width * scale;
const dh = img.height * scale;
ctx.drawImage(img, (size - dw) / 2, (size - dh) / 2, dw, dh);
return canvas.toDataURL('image/png');
} catch {
return null;
}
}
function loadImage(src: string): Promise {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = src;
});
}
/** Trace a centered square rounded-rect path of side `size` and corner `r`. */
function traceRoundedRect(ctx: CanvasRenderingContext2D, size: number, r: number): void {
const radius = Math.min(r, size / 2);
ctx.beginPath();
ctx.moveTo(radius, 0);
ctx.arcTo(size, 0, size, size, radius);
ctx.arcTo(size, size, 0, size, radius);
ctx.arcTo(0, size, 0, 0, radius);
ctx.arcTo(0, 0, size, 0, radius);
ctx.closePath();
}