import { Children, isValidElement, type ReactNode } from "react"; import { LogoTileSizeProvider } from "./logo-tile"; import { cn } from "../lib/cn"; export { BackLink, DetailPage, type DetailBackLink } from "./detail-sheet"; /* ---------------------------------------------------------------------------- The detail page anatomy (the decided detail pick, B). Anything you open - a variable set, a model account, a person, an API key, a schedule, a knowledge entry, a workspace - is its own page inside the content area, never a side sheet: "← Variable sets" } title="Production" meta line, actions chips={Organization} meta={["by Maja Berg", "12 variables", "updated 3 days ago"]} actions={} tabs={…} underline tabs /> …}> main column + … quiet right card DetailSection, DetailFacts and DetailFooter from detail-sheet.tsx render in their page form inside a DetailPage. -------------------------------------------------------------------------- */ export interface DetailPageHeaderProps { /** A LogoTile or avatar. Rendered at 40px. */ leading?: ReactNode | undefined; title: ReactNode; /** Small chips right of the title: a StatusBadge, a MetaChip ("New", "Organization"). */ chips?: ReactNode | undefined; /** The meta line: "by Maja Berg · in Platform engineering · updated Sep 15". Pass parts as an array. */ meta?: ReactNode | undefined; /** The page's actions: one primary or secondary button and a ⋯ menu. */ actions?: ReactNode | undefined; /** An underline tab row (LineTabsNav or LineTabsList) under the header. */ tabs?: ReactNode | undefined; className?: string | undefined; } /** Joins meta parts with a middle dot. Falsy parts are skipped. */ export function DetailMeta({ children, className }: { children: ReactNode; className?: string }) { // Children.toArray already drops null, undefined and booleans. const parts = Children.toArray(children).filter((part) => part !== ""); return (

{/* Each part sits after a gap that holds its dot. The row is shifted left by one gap, so a part that starts a wrapped line has its dot outside the clipped edge: no line starts or ends on a dangling separator. A part moves to the next line whole; only a part longer than the line wraps inside. */} {parts.map((part, index) => ( {index > 0 ? ( ) : null} {part} ))}

); } export function DetailPageHeader({ leading, title, chips, meta, actions, tabs, className, }: DetailPageHeaderProps) { const metaLine = Array.isArray(meta) ? {meta} : meta; return (
{leading ? (
{leading}
) : null}
{/* Focusable from script only: a page opened in place moves focus to its title. */}

{title}

{chips}
{metaLine ?
{metaLine}
: null}
{actions ? (
{actions}
) : null}
{tabs ?
{tabs}
: null}
); } /** * The page body: one main column and an optional quiet aside card on the * right (Created by, Scope, Used by). The aside drops under the main column * below 620px of width. */ export function DetailPageBody({ aside, children, className, }: { aside?: ReactNode | undefined; children: ReactNode; className?: string | undefined; }) { return (
{/* Hairlines only between visible sections, so hidden tab panels leave no stray rule. */}
{children}
{aside ?
{aside}
: null}
); } /** The quiet metadata card: label/value items, no border, surface fill. */ export function DetailAside({ children, label = "Details", className, }: { children: ReactNode; /** Accessible name of the card. */ label?: string | undefined; className?: string | undefined; }) { // A labelled section, not