Skip to content

Kody is live

Watch the launch video — what Kody is, and why it exists.

← Public packages

@kentcdodds/package-app-kit

Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.

src/icons.ts

167 lines · 5.1 KB · TypeScript
/**
 * PWA icon helpers. Kit PNG bytes are embedded in `./icon-png-bytes.ts`
 * (mirrors `icons/icon-192.png` + `icons/icon-512.png` in this repo).
 *
 * Tool-only / `packageSave` agents: do **not** commit binary PNGs into the
 * consumer app. Serve kit bytes from app routes via `iconPngResponse` and point
 * the manifest at those routes with `kitServedManifestIcons` (alias of
 * `defaultManifestIcons`). Optional UTF-8 SVG helpers are below for favicons /
 * apps that only need an SVG mark (Chromium URL-bar Install still wants PNG 192+512).
 */
import { ICON_192_BYTES, ICON_512_BYTES } from './icon-png-bytes.ts'

export type PwaIconSize = 192 | 512

/** Bump when kit PWA PNGs change so SW + favicon/manifest caches refresh. */
export const ICON_ASSET_REVISION = '7'

const ICON_BYTES: Record<PwaIconSize, Uint8Array> = {
	192: ICON_192_BYTES,
	512: ICON_512_BYTES,
}

export type ManifestIcon = {
	src: string
	sizes: string
	type: string
	purpose: string
}

/**
 * Return the branded PNG bytes for a Chromium-required PWA icon size.
 * Prefer shipping `icons/icon-192.png` / `icons/icon-512.png` in the app repo
 * when using the git lane; for tool-only apps, call this (or `iconPngResponse`)
 * from routes — no local PNG files required.
 *
 * @param size - 192 or 512
 * @returns PNG file bytes
 *
 * @example
 * import { pngIconBytes } from 'kody:@kentcdodds/package-app-kit/icons'
 * const png = pngIconBytes(192)
 */
export function pngIconBytes(size: PwaIconSize): Uint8Array {
	return ICON_BYTES[size]
}

/**
 * Build a `200 image/png` Response for a PWA icon.
 * Wire to `GET /icons/icon-192.png` and `GET /icons/icon-512.png`.
 * No local PNG files needed in the consumer package.
 *
 * @param size - 192 or 512
 * @returns PNG response with long-lived cache headers
 *
 * @example
 * import { iconPngResponse } from 'kody:@kentcdodds/package-app-kit/icons'
 * // in fetch handler:
 * if (path === '/icons/icon-192.png') return iconPngResponse(192)
 * if (path === '/icons/icon-512.png') return iconPngResponse(512)
 */
export function iconPngResponse(size: PwaIconSize): Response {
	const body = pngIconBytes(size)
	return new Response(body, {
		headers: {
			'content-type': 'image/png',
			'cache-control': 'public, max-age=86400',
			'content-length': String(body.byteLength),
		},
	})
}

/**
 * Manifest `icons` entries pointing at kit-served 192 + 512 PNG routes
 * (`{appBase}/icons/icon-192.png` + `icon-512.png`). Pair with `iconPngResponse`
 * handlers — no binary PNG files in the consumer package.
 *
 * @param appBasePath - `packageContext.appBasePath`
 * @returns Manifest icon objects with correct sizes/types
 *
 * @example
 * import { defaultManifestIcons } from 'kody:@kentcdodds/package-app-kit/icons'
 * const icons = defaultManifestIcons('/packages/package-app-kit')
 */
export function defaultManifestIcons(appBasePath: string): ManifestIcon[] {
	const base = appBasePath.replace(/\/$/, '')
	return [
		{
			src: `${base}/icons/icon-192.png?v=${ICON_ASSET_REVISION}`,
			sizes: '192x192',
			type: 'image/png',
			purpose: 'any',
		},
		{
			src: `${base}/icons/icon-512.png?v=${ICON_ASSET_REVISION}`,
			sizes: '512x512',
			type: 'image/png',
			purpose: 'any',
		},
	]
}

/**
 * Same as `defaultManifestIcons` — named for tool-only / fetch-handler apps that
 * serve kit PNG bytes from routes (no committed `icons/*.png` in the app).
 *
 * @example
 * import {
 *   kitServedManifestIcons,
 *   iconPngResponse,
 * } from 'kody:@kentcdodds/package-app-kit/icons'
 * import { buildWebManifest } from 'kody:@kentcdodds/package-app-kit/sw'
 *
 * buildWebManifest({
 *   appBasePath,
 *   name: 'My App',
 *   icons: kitServedManifestIcons(appBasePath),
 * })
 * // routes: /icons/icon-192.png → iconPngResponse(192), same for 512
 */
export function kitServedManifestIcons(appBasePath: string): ManifestIcon[] {
	return defaultManifestIcons(appBasePath)
}

/** Minimal branded SVG mark (UTF-8 shippable — fine for `packageSave` / assets). */
export function kitIconSvg(options?: {
	/** Background fill (default kit teal-dark). */
	bg?: string
	/** Foreground mark fill. */
	fg?: string
}): string {
	const bg = options?.bg ?? '#0f766e'
	const fg = options?.fg ?? '#ecfeff'
	return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64"><rect width="64" height="64" rx="14" fill="${bg}"/><circle cx="32" cy="32" r="14" fill="${fg}"/></svg>`
}

/**
 * `200 image/svg+xml` Response for a UTF-8 SVG icon (favicon / apple-touch).
 * Does **not** replace PNG 192+512 for Chromium installability.
 */
export function iconSvgResponse(svg = kitIconSvg()): Response {
	const body = svg
	return new Response(body, {
		headers: {
			'content-type': 'image/svg+xml; charset=utf-8',
			'cache-control': 'public, max-age=86400',
		},
	})
}

/**
 * Manifest icon entry for an SVG at `{assetBasePath}/icon.svg` (or custom src).
 * Use as a supplement; keep PNG 192+512 for Chromium URL-bar Install.
 */
export function svgManifestIcon(
	src: string,
	options?: { purpose?: string },
): ManifestIcon {
	return {
		src,
		sizes: 'any',
		type: 'image/svg+xml',
		purpose: options?.purpose ?? 'any',
	}
}

/** Primary callable export for this subpath. */
export default iconPngResponse