import type { ComponentType, ReactNode } from "react"; import { CardTitle } from "@/components/ui/card"; import { cn } from "@/lib/utils"; /** * v1.12.6 — the one canonical tile header used by every Insights tile. * * Pre-v1.12.6 each tile hand-rolled its own `icon + heading` row at a * slightly different size, weight, and colour: the "Einschätzung" card * led with a foreground-colour icon + `CardTitle`, the target panel led * with a small muted `Target` glyph + a range string, and the stat strip * led with a tiny uppercase muted caption. Side-by-side on the same * subpage they read as three different card languages. * * `TileHeader` pins the single header contract to the "Einschätzung" * reference (the calmest, most legible of the three): a `flex * items-center gap-2` row with the icon and the `CardTitle` BOTH in the * foreground colour (white in dark mode) — never `text-muted-foreground` * — at `text-base`, icon `h-5 w-5 shrink-0`. An optional `right` slot * carries a trailing affordance (a provenance / tooltip glyph, a status * pill) without breaking the row's alignment. * * RSC-safe: no hooks, no browser API, so it renders on the server inside * any client or server tile. */ interface TileHeaderProps { /** * Leading glyph. Accepts a Lucide icon component (or any component that * takes a `className`) so the caller passes the component itself * (`icon={Target}`), not a pre-sized node — `TileHeader` owns the size * and colour so every tile header matches. Optional: a text-only tile * header (chart cards whose title needs no glyph) omits it and still * gets the canonical row + `CardTitle` treatment. */ icon?: ComponentType<{ className?: string }>; /** Heading text (or node). Rendered inside a `CardTitle` at `text-base`. */ title: ReactNode; /** Optional trailing affordance pinned to the right edge of the row. */ right?: ReactNode; /** * The ONE sanctioned compact variant: icon `h-4 w-4`, title `text-sm` — * for dense correlation/stat tiles that sit several to a row where the * full `text-base` header visually overpowers the tile body. Everything * else stays on the default; do not express a third size via * `titleClassName`. */ size?: "default" | "sm"; className?: string; titleClassName?: string; /** Optional id on the `CardTitle`, e.g. for an `aria-labelledby` link. */ id?: string; /** * Render the title as a real heading element (e.g. `"h2"`) instead of the * default presentational `CardTitle` div, keeping the identical CardTitle * styling. Use on tiles that sit directly under the page's `h1` — a single * owning block (the recovery blocks) or a grid of sibling tiles (dashboard * tiles, insights overview grids) both nest correctly as `h1 → h2, h2, …`; * just don't nest a `titleAs="h2"` tile inside another heading's own * subsection. Default (unset) keeps every other tile a plain CardTitle. */ titleAs?: "h2"; } export function TileHeader({ icon: Icon, title, right, size = "default", className, titleClassName, id, titleAs, }: TileHeaderProps) { const titleClassNames = cn( size === "sm" ? "text-sm" : "text-base", titleClassName, ); return (