move tsx kit -> uikit
This commit is contained in:
137
web/uikit/Icons.tsx
Normal file
137
web/uikit/Icons.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
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<CSSStyleProperties>;
|
||||
}
|
||||
|
||||
// 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.
|
||||
const customIcons: Record<string, CustomIconDef> = {};
|
||||
|
||||
// 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) {
|
||||
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 ? '<path d="' + fa[4] + '"/>' : "";
|
||||
};
|
||||
|
||||
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 (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox={box().join(" ")} fill="currentColor" width={w()} height={h()} class={ICON_BASE + " " + (props.class || "")} innerHTML={svgContent()}></svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function IconInline(props: IconProps) {
|
||||
return <Icon icon={props.icon} size={props.size} prefix={props.prefix} solid={props.solid} class={ICON_INLINE + " " + (props.class || "")}/>;
|
||||
}
|
||||
|
||||
export function IconSuccess(props: IconProps) {
|
||||
return <Icon icon={props.icon} size={props.size} prefix={props.prefix} solid={props.solid} class={ICON_INLINE + " text-green-600 " + (props.class || "")}/>;
|
||||
}
|
||||
|
||||
export function IconError(props: IconProps) {
|
||||
return <Icon icon={props.icon} size={props.size} prefix={props.prefix} solid={props.solid} class={ICON_INLINE + " text-red-600 " + (props.class || "")}/>;
|
||||
}
|
||||
|
||||
interface IconContainerProps {
|
||||
children?: JSXElement;
|
||||
}
|
||||
|
||||
export function IconContainer(props: IconContainerProps) {
|
||||
return <span class="flex flex-row items-center gap-2">{props.children}</span>;
|
||||
}
|
||||
Reference in New Issue
Block a user