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}
))}
{/* 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