/** @jsxImportSource react */
/**
* Pure OG image JSX components — no DB, Hono, or metrics imports.
* Used by the OG render worker to generate images off the main thread.
*/
import type { ReactNode } from "react";
import type {
BookOgCardProps,
StatsOgCardProps,
ProfileOgCardProps,
LabeledCoverCardProps,
MarketingOgCardProps,
AppOgCardProps,
} from "./types";
// ─── Constants ───────────────────────────────────────────────────────────────
export const OG_WIDTH = 1200;
export const OG_HEIGHT = 630;
export const COLORS = {
bg: "#f9eabc", // sand background
card: "#fef9c3", // yellow-50
border: "#e5d5a0", // sand-dark
text: "#1c1917", // stone-900
textMuted: "#78716c", // stone-500
primary: "#d97706", // amber-600
primaryLight: "#fef3c7", // amber-100
white: "#ffffff",
};
export const OG_RENDER_OPTIONS = {
width: OG_WIDTH,
height: OG_HEIGHT,
format: "webp" as const,
};
// ─── Shared UI components ────────────────────────────────────────────────────
/** SVG star — avoids missing-glyph boxes since Geist has no ★ */
function StarIcon({ filled, size = 28 }: { filled: boolean; size?: number }) {
return (
);
}
function StarRating({
rating,
max = 5,
size = 28,
}: {
rating: number;
max?: number;
size?: number;
}) {
const filled = Math.round(rating);
return (
{Array.from({ length: max }, (_, i) => (
))}
);
}
/** BookHive logo mark using the sidebar book SVG */
function BrandMark({ size = 32 }: { size?: number }) {
const h = size;
const w = Math.round(size * (532 / 688));
return (
BookHive
);
}
/** 3×2 grid of book covers filling the available space */
function CoverGrid({ covers }: { covers: string[] }) {
return (
{covers.slice(0, 6).map((url, i) => (
))}
);
}
/** Left sidebar + cover grid layout shared by ProfileOgCard and LabeledCoverCard */
function SidebarLayout({
sidebarWidth,
sidebarPadding,
children,
covers,
}: {
sidebarWidth: number;
sidebarPadding: string;
children: ReactNode;
covers: string[];
}) {
return (