import { JSXElement } from "solid-js"; import { FA_ICONS } from "@appgen/faIcons"; interface CustomIconDef { viewBox: [number, number]; content: string; } interface IconProps { icon: string; size?: number | [number, number]; class?: string; prefix?: string; // Per-icon style override: `true` forces solid (fas), `false` forces // regular (far); when omitted, follows the app-wide FA_DEFAULT_SOLID switch. // Solid falls back to regular when a solid variant isn't in the bundle. solid?: boolean; style?: Partial; } // Vite-style HMR API the dev bundler injects into every module (see // internal/bundler/hmr_server.go + docs/hmr.md); undefined in production. // `declare global` is required here — a bare `interface ImportMeta` in a // module file wouldn't merge with the ambient one `import.meta`'s type // actually resolves against. declare global { interface ImportMeta { hot?: { readonly data: Record; accept(cb?: (mod: any) => void): void; accept(deps: string | string[], cb?: (mod: any) => void): void; dispose(cb: (data: Record) => void): void; prune(cb: (data: Record) => void): void; invalidate(): void; decline(): void; }; } } // The FontAwesome family + default weight are theme-driven so this component // stays identical across projects. Each project's CSS theme sets `--fa-style` // (classic → far/fas, sharp → fasr/fass) and `--fa-default-solid` (0/1). Read // once at load; falls back to classic/regular under SSR (no getComputedStyle). function readIconTheme(): { regular: string; solid: string; defaultSolid: boolean } { let sharp = false, defaultSolid = false; if (typeof document !== "undefined" && typeof getComputedStyle === "function" && document.documentElement) { const cs = getComputedStyle(document.documentElement); sharp = cs.getPropertyValue("--fa-style").trim() === "sharp"; defaultSolid = cs.getPropertyValue("--fa-default-solid").trim() === "1"; } return { regular: sharp ? "fasr" : "far", solid: sharp ? "fass" : "fas", defaultSolid }; } const _iconTheme = readIconTheme(); const FA_PREFIX_REGULAR = _iconTheme.regular; const FA_PREFIX_SOLID = _iconTheme.solid; // Set `--fa-default-solid: 1` in the theme to make the app default to solid icons. // Individual icons still override per-call with `solid` (true/false) or `prefix`. const FA_DEFAULT_SOLID: boolean = _iconTheme.defaultSolid; const FA_PREFIX_DEFAULT = FA_DEFAULT_SOLID ? FA_PREFIX_SOLID : FA_PREFIX_REGULAR; const ICON_BASE = "shrink-0"; const ICON_INLINE = "inline-block align-middle"; // Custom (non-FontAwesome) icon registry — a registered name overrides any FA // lookup for the same name. This shared component ships with it EMPTY: each // project registers its own SVGs from its app entry (see frontend/src/appIcons.ts) // via registerIcon, so Icons.tsx stays identical across projects. // Persisted across this module's own HMR reloads (see docs/hmr.md). Icons.tsx // is an HMR boundary, and its FA icon subset import churns on every SPA source // edit (generateFAIcons regenerates it each time), so this module reloads far // more often than one would expect from editing Icons.tsx itself. A fresh // module instance would otherwise start with an empty registry, since // registerIcon() only runs once — from appIcons.ts's side-effect import at // initial page load — blanking custom icons (playground, e-cd, ...) until a // full page refresh. import.meta.hot.data survives across reloads of this // module, so stash the registry there instead of a plain module-scoped const. const customIcons: Record = (import.meta.hot?.data.customIcons as Record | undefined) ?? {}; if (import.meta.hot) import.meta.hot.data.customIcons = customIcons; // Register a custom icon under `name`, overriding FontAwesome for that name. // Call from the app's own icon module (e.g. appIcons.ts), imported for side // effect at startup so every icon is registered before the first Icon renders. export function registerIcon(name: string, def: CustomIconDef) { customIcons[name] = def; } // [minX, minY, width, height, svgPath] — viewBox is cropped to the glyph. type FAEntry = readonly [number, number, number, number, string]; function resolveFAIcon(name: string, prefix: string): FAEntry | undefined { return FA_ICONS[prefix + ":" + name]; } export function Icon(props: IconProps): JSXElement { const size = props.size ?? 16; const custom = customIcons[props.icon]; // Style resolution: an explicit `prefix` wins; then a per-icon `solid` // override (true→solid, false→regular); otherwise the app-wide default set // by FA_DEFAULT_SOLID. The fallback chain lets any icon resolve regardless // of which style it actually ships in, so opting into solid never blanks. const faDef = (): FAEntry | undefined => { if (custom) return undefined; let want: string; if (props.prefix) want = props.prefix; else if (props.solid === true) want = FA_PREFIX_SOLID; else if (props.solid === false) want = FA_PREFIX_REGULAR; else want = FA_PREFIX_DEFAULT; return resolveFAIcon(props.icon, want) || resolveFAIcon(props.icon, FA_PREFIX_REGULAR) || resolveFAIcon(props.icon, FA_PREFIX_SOLID); }; // viewBox as [minX, minY, width, height]: custom icons are 0-origin; FA icons // carry a viewBox cropped to the glyph so they render at their intended size. const box = (): [number, number, number, number] => { const c = custom; if (c) return [0, 0, c.viewBox[0], c.viewBox[1]]; const fa = faDef(); return fa ? [fa[0], fa[1], fa[2], fa[3]] : [0, 0, 512, 512]; }; const vw = () => box()[2]; const vh = () => box()[3]; const svgContent = () => { const c = custom; if (c) return c.content; const fa = faDef(); return fa ? '' : ""; }; const h = (): number => { const s = size; return Array.isArray(s) ? (s[1] ?? s[0]) : s; }; const w = (): number => { const s = size; return Array.isArray(s) ? s[0] : Math.round(h() * (vw() / vh())); }; return ( ); } export function IconInline(props: IconProps): JSXElement { return ; } export function IconSuccess(props: IconProps): JSXElement { return ; } export function IconError(props: IconProps): JSXElement { return ; } export function IconContainer(props: {children?: JSXElement}): JSXElement { return {props.children}; }