UI
Page-composition primitives — shell, section, empty state. Presentational and *not* `"use client"`, so they render in a Server Component.
Generated from @cosmoops/connect-sdk/ui's TypeScript source and its own doc comments - see Reference for the other entry points. Regenerated on every deploy; don't hand-edit.
Interfaces
Interface: ConnectEmptyStateProps
Properties
| Property | Type | Description |
|---|---|---|
icon? | ReactNode | Rendered at size-8. Any icon element - lucide, an SVG, a glyph. |
title | ReactNode | What is absent, in the user's words. One line. |
description? | ReactNode | - |
action? | ReactNode | The one control that resolves the emptiness - "New contact" for a list that has never had anything in it, "Clear filter" for one whose contents are merely hidden. |
variant? | "card" | "dashed" | "plain" | - |
className? | string | - |
Interface: ConnectMetricChartProps
Properties
| Property | Type | Description |
|---|---|---|
series | Pick<ApplicationMetricSeries, "unit" | "label" | "points"> & Partial<ApplicationMetricSeries> | One series from analytics.metrics(). Pass the whole series; the chart reads its points, unit and label. |
variant? | "line" | "column" | column (default) for a flow - deals won in a day - and line for a level a gauge reports. A line between two flow buckets invents the values in between; a column per level reads as a sum nobody took. Defaults to line for a gauge series and column for everything else. |
formatValue? | (value: number) => string | How a value is written - on the scale and in each hover. Defaults to formatMetricValue by the series' unit. |
formatBucket? | (bucket: string) => string | How a bucket is written on the axis and in each hover. Defaults to the bucket key (2026-10-01). |
summary? | string | Announced to a screen reader in place of the drawing. Defaults to the label and the range. |
className? | string | - |
Interface: ConnectPageHeaderProps
Properties
| Property | Type | Description |
|---|---|---|
title | ReactNode | - |
description? | ReactNode | - |
action? | ReactNode | Rendered opposite the title. Keep it to one control - a page has one primary action, and a second here has nothing to align against. |
clamp? | boolean | Set false when the description carries a link that must stay readable on a phone. |
className? | string | - |
Interface: ConnectPageShellProps
Properties
| Property | Type | Description |
|---|---|---|
title? | ReactNode | - |
description? | ReactNode | - |
action? | ReactNode | - |
clamp? | boolean | - |
above? | ReactNode | Rendered above the header - a breadcrumb trail, a back link, a banner. |
className? | string | Merged onto the page root, so a utility here beats the default rather than sitting beside it - className="space-y-8" really does widen the page gap. See cn. |
transition? | boolean | The route transition - the outgoing page fades out, the arriving one fades in and rises. On by default; pass false where a host already animates its routes, or the two play over each other. See RouteTransition. |
children? | ReactNode | - |
Interface: ConnectSectionProps
Properties
| Property | Type | Description |
|---|---|---|
title | ReactNode | - |
description? | ReactNode | What the block is for. Omit when the title says everything. |
action? | ReactNode | Rendered opposite the title. One control - a section has one action. |
titleId? | string | Set to reference the heading from an aria-labelledby in the body. |
className? | string | - |
bodyClassName? | string | - |
children? | ReactNode | - |
Interface: ConnectSettingRowProps
Properties
| Property | Type | Description |
|---|---|---|
title | ReactNode | - |
description? | ReactNode | - |
action? | ReactNode | The control this row is about - a switch, a button, a value with an edit link. |
children? | ReactNode | Rendered under both, full width - a validation message, or a nested field the row reveals. |
className? | string | - |
Interface: ConnectSetupChecklistProps
Properties
| Property | Type | Description |
|---|---|---|
progress | SetupProgress | - |
granted | readonly string[] | The viewer's permissions. A step's action is drawn only for a viewer holding its permission. |
compact? | boolean | One line per step - no descriptions, no progress header. For a recap inside another card. |
readOnly? | boolean | Draw no actions and no owner notes - a recap for somebody who can act on none of it (the waiting page), where "Ask an admin" on every row is noise. |
renderLink? | ConnectSetupLinkRenderer | The host router's link - see ConnectSetupLinkRenderer. |
className? | string | - |
Interface: ConnectSetupDoneProps
Properties
| Property | Type | Description |
|---|---|---|
appName | string | "Support", "CRM". |
children? | ReactNode | The try-it actions - a test ticket, the sample data, the app's one primary "Go to …". |
progress | SetupProgress | - |
granted? | readonly string[] | The viewer's permissions. Given, every unfinished row links back to its step; omitted, the recap is read-only. |
renderLink? | ConnectSetupLinkRenderer | The host router's link - see ConnectSetupLinkRenderer. |
className? | string | - |
Interface: ConnectSetupWaitingProps
Properties
| Property | Type | Description |
|---|---|---|
appName | string | "Support", "CRM" - the application being set up. |
ownerLabel? | string | Who is doing it, as a sentence opens - "Your admin" (the default), "Your workspace owner". |
progress? | SetupProgress<string> | Where setup has got to. Drawn as a read-only recap when given. |
action? | ReactNode | One way onward, for a member the app is not locked for - "Go to tickets". Omit while locked. |
className? | string | - |
Interface: ConnectSetupWizardFooter
Properties
| Property | Type | Description |
|---|---|---|
back? | string | null | The previous step's path, or null/omitted on the first. Rendered as a tertiary "Back". |
skip? | ReactNode | The app's own "Skip for now" control (a server-action button). Omit on a step that cannot be skipped. |
primary | ReactNode | The step's one primary control - a submit button, a link, whatever the step's verb is. |
Interface: ConnectSetupWizardProps
Properties
| Property | Type | Description |
|---|---|---|
steps | readonly ConnectSetupWizardStep[] | - |
current | string | The key of the step this page is. The route knows it - pass it, never read the pathname. |
title | ReactNode | The page's own heading. Rendered as the document's h1 - this frame is the whole page. |
description? | ReactNode | - |
children? | ReactNode | - |
aside? | ReactNode | An illustration beside the content at desktop width. Hidden below lg, where it would only push the form down. |
footer | ConnectSetupWizardFooter | - |
exitHref? | string | Where "Exit setup" goes. Omit while the app is locked - there is nowhere to exit to. |
exitLabel? | string | The exit link's words. |
renderLink? | ConnectSetupLinkRenderer | The host router's link - see ConnectSetupLinkRenderer. A plain <a> without it. |
className? | string | - |
Interface: ConnectSetupWizardStep
One marker on the wizard's step strip.
Properties
| Property | Type | Description |
|---|---|---|
key | string | - |
label | string | Short - the strip's label, and the "Step 2 of 5 · <label>" line on a phone. |
path | string | - |
state | SetupStepState | - |
Interface: ConnectStatStripProps
Properties
| Property | Type | Description |
|---|---|---|
children | ReactNode | ConnectStatTiles. |
columns? | 6 | 2 | 5 | 4 | 3 | How many tiles sit abreast from lg up (two abreast below sm, and an odd last tile spans the row, so a strip never leaves an orphan). |
className? | string | - |
Interface: ConnectStatTileProps
Properties
| Property | Type | Description |
|---|---|---|
label | ReactNode | - |
value | ReactNode | Already formatted - formatMetricValue(series.total, series.unit) for a metric. A null total formats as a dash; pass partial alongside it so the hint can say why. |
hint? | ReactNode | What the number is of. Omit only if the label already says it. |
delta? | MetricDelta | null | How it moved against the comparison period - metricDelta(current, previous). Omit, or pass null, when there is nothing honest to compare against: a "+0%" pill would read as a measured result. |
polarity? | "higher-is-better" | "lower-is-better" | Whether a rise is good news - decides the pill's colour, never its arrow. |
partial? | boolean | The figure leaves out days that are not measured yet (series.partial). Adds a short note under the figure rather than presenting a part as the whole. |
inset? | boolean | Drawn on --surface-secondary with no shadow - for a tile inside another card. |
className? | string | - |
Interface: ConnectStatusPresentation<TStatus>
How one domain's status union is presented: a tone and a human label per member.
Both are total Records, and that is the load-bearing property - adding
a member to the status union fails the type gate until it has been given a
presentation. A loose Record<string, …> map silently renders the new
status as undefined.
Type Parameters
| Type Parameter |
|---|
TStatus extends string |
Properties
| Property | Modifier | Type |
|---|---|---|
tone | readonly | Readonly<Record<TStatus, ConnectStatusTone>> |
label | readonly | Readonly<Record<TStatus, string>> |
Interface: SetupStepView
What one step offers one viewer.
Properties
| Property | Type | Description |
|---|---|---|
action | | { href: string; label: string; } | null | The button - only for somebody who may act, and only on a state with something to do. |
ownerNote | string | null | "Ask an admin to do this one." - for a viewer who cannot act on an open step. |
line | string | The step's second line: the fact once done, the reason in review, what it waits on when blocked. |
Type Aliases
Type Alias: ConnectStatusTone
type ConnectStatusTone = "success" | "warning" | "danger" | "accent" | "default";The closed set of tones a status may resolve to.
Deliberately five, and deliberately narrower than HeroUI's Chip colour
union: a status is either fine, needs attention, is broken, is in progress,
or is inert. A sixth tone would have to mean one of those five again, which
is where hand-written status chips start disagreeing with each other.
Variables
Variable: cn
const cn: <T>(...classnames: T) => CnReturn;Combines class names and merges conflicting Tailwind classes (default config).
Type Parameters
| Type Parameter |
|---|
T extends CnOptions |
Parameters
| Parameter | Type |
|---|---|
...classnames | T |
Returns
CnReturn
Variable: NOT_YET_MEASURED
const NOT_YET_MEASURED: "not yet measured" = "not yet measured";What a gap says, in the hover and in the legend under the chart.
Variable: SETUP_STATE_PRESENTATION
const SETUP_STATE_PRESENTATION: ConnectStatusPresentation<SetupStepState>;Every step state as a chip. Total, so a new state fails the type gate until it is given one.
Functions
Function: ConnectDeltaPill()
function ConnectDeltaPill(__namedParameters: Readonly<{
delta: MetricDelta;
polarity?: "higher-is-better" | "lower-is-better";
className?: string;
}>): Element;How a figure moved, as the tile draws it - exported for a chart card that leads with a figure of its own.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ delta: MetricDelta; polarity?: "higher-is-better" | "lower-is-better"; className?: string; }> |
Returns
Element
Function: ConnectEmptyState()
function ConnectEmptyState(__namedParameters: Readonly<ConnectEmptyStateProps>): Element;What a list, grid or table shows when it has nothing to show.
The two empty states are different facts and the copy must say so. "You
have no contacts" and "no contacts match this filter" call for different
words and a different action - create the thing, versus undo the filter
hiding it. Both render through here; the caller decides which by what it
passes as action. Getting this wrong costs a user real time: an
unfiltered-looking empty state in front of a filtered list reads as data
loss.
variant chooses the container by what the state stands in: card (the
default), dashed for a region that will be filled in place - a file grid,
a board column - where the outline is what says "things appear here", and
plain for a slot already inside a card that would otherwise nest two
borders.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectEmptyStateProps> |
Returns
Element
Function: ConnectMetricChart()
function ConnectMetricChart(__namedParameters: Readonly<ConnectMetricChartProps>): Element | null;One metric series as columns or a line, drawn on the server - no chart
library and no client component, because the geometry is arithmetic and the
hover is a title on each bucket's full-height slot.
Rules the drawing obeys
nullis a gap, never a zero. A bucket the rollup has not reached, or adistinctmetric today, keeps its slot and draws a dotted floor - and a line breaks there rather than diving to the axis. The hover says "not yet measured", and a legend line under the chart says what the dots mean whenever there is one. Drawing those as zeros is how a dev deployment, where the nightly rollup never runs, looks like a dead product.- The baseline is zero, so a column's height reads as its magnitude.
- Semantic tokens only - the mark is
--accent, the rules--separator, the textmuted- so the chart takes whatever palette the host defines. - Labels are HTML around the drawing, never
<text>in the stretched viewBox, so they stay their real size on a phone.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectMetricChartProps> |
Returns
Element | null
Function: ConnectPageHeader()
function ConnectPageHeader(__namedParameters: Readonly<ConnectPageHeaderProps>): Element;The title / description / action block a page opens with.
The structure is the point, not the styling. The action is a peer of the title, not of the block, so nothing below the first line can move it - the common hand-rolled shape puts the action after the description, which makes the vertical position of a page's primary control a function of how long someone wrote the paragraph above it. On a phone that pushes it most of a thumb's reach down the screen.
Mobile-first, and the two axes are separate decisions: the title row is a
justify-between at every width (title left, action right, where a thumb
expects it), while the description is clamped to two lines on a phone and
released from sm up.
An h2, because the document's h1 is the application itself.
No hooks and no state, so this renders from a Server Component - which is
why it lives in @cosmoops/connect-sdk/ui rather than /client.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectPageHeaderProps> |
Returns
Element
Function: ConnectPageShell()
function ConnectPageShell(__namedParameters: Readonly<ConnectPageShellProps>): Element;The frame a page opens with: its vertical rhythm, its header, and the suspended regions beneath.
The header is a prop, so it renders in the static shell - above and
outside every <Suspense> boundary. That is the one bug this component
exists to make unrepresentable: rendering the header inside the async child
makes a page's own title wait on a data read, and under Next's partial
prefetching the route's shared App Shell loses it as well. There is no
arrangement of children that reaches the header slot.
A detail route is the honest exception, and it has its own door:
ConnectPageShell.Header renders the identical header from inside an async
child, for a page whose title is data it does not have yet. Pair it with a
skeleton header in the fallback. The rule is that a static title must
never be made to wait, not that every title can be static.
space-y-5 is the page gap, owned here so it has exactly one definition.
It is also where the route transition lives, because it is the one
thing every page opens with. A transition has to wrap the page: a layout
persists across navigation, so its enter and exit never fire, and a
template.tsx remounts its subtree on every navigation, discarding the
form drafts and scroll positions Next's <Activity> keeps. Where a layout
draws the shell instead - a record with tabs - entering and leaving the
record animates and switching tabs does not, which is right: a tab is the
same place. default="none" keeps it still during anything else - a
filter written to the URL, a router.refresh() after a save. The
animations are connect-page-enter/connect-page-exit in theme.css.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectPageShellProps> |
Returns
Element
Function: ConnectSection()
function ConnectSection(__namedParameters: Readonly<ConnectSectionProps>): Element;One titled block inside a page: a card, its heading, what it is for, and the one control that acts on it.
The heading is an h3 - ConnectPageHeader renders the page title as an
h2, so a block inside a page is a level below it. Hand-rolled sections
reliably use h2, which makes every settings block a sibling of the page it
sits in and flattens the outline a screen reader announces.
As in the page header, the action is a peer of the title rather than of the block, so its position does not move with the length of the description.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectSectionProps> |
Returns
Element
Function: ConnectSettingRow()
function ConnectSettingRow(__namedParameters: Readonly<ConnectSettingRowProps>): Element;One labelled setting inside a ConnectSection.
Stacks on a phone and goes side-by-side from sm up, rather than shrinking
a two-column row until neither column is readable. The control keeps its
intrinsic width so truncation lands on the description.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectSettingRowProps> |
Returns
Element
Function: ConnectSetupChecklist()
function ConnectSetupChecklist(__namedParameters: Readonly<ConnectSetupChecklistProps>): Element;Every onboarding step, in order, with where it stands - the /onboarding
overview, and the recap on the waiting and done screens.
- State chips: done, skipped (amber - "not now", not done), in review (with the reason), waiting (grey, with what it waits on).
- An action only for a step the viewer may act on. Anybody else is told whose job it is ("Ask an admin to do this one.") rather than offered a button that ends in a 403.
- One primary:
progress.next's action. Every other action is secondary, and a done step offers a quiet "Change".
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectSetupChecklistProps> |
Returns
Element
Function: ConnectSetupDone()
function ConnectSetupDone(__namedParameters: Readonly<ConnectSetupDoneProps>): Element;The wizard's last screen, /onboarding/done: what was set up - every step
done, skipped or still open, each unfinished one a link back when the
viewer may act - and then the app's own try-it actions (children).
Anything skipped stays on the landing page's "Get started" card.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectSetupDoneProps> |
Returns
Element
Function: ConnectSetupStepStrip()
function ConnectSetupStepStrip(__namedParameters: Readonly<{
steps: readonly ConnectSetupWizardStep[];
current: string;
renderLink?: ConnectSetupLinkRenderer;
className?: string;
}>): Element | null;Where you are in the wizard. At phone width it is one line - "Step 2 of 5 · Support email" - because five labelled markers do not fit in 343px and a wrapped strip reads as two. Done, skipped and in-review steps stay links, so somebody who picked the wrong industry can go back and say so.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ steps: readonly ConnectSetupWizardStep[]; current: string; renderLink?: ConnectSetupLinkRenderer; className?: string; }> |
Returns
Element | null
Function: ConnectSetupWaiting()
function ConnectSetupWaiting(__namedParameters: Readonly<ConnectSetupWaitingProps>): Element;What a member without setup permission sees at /onboarding/waiting: one
short card, not a wizard they cannot complete and not a 403. Setup is
somebody else's job, and the useful thing to say is so - and, given
progress, what is already in place. Reads nothing itself, so a
forbidden.tsx can draw it too.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectSetupWaitingProps> |
Returns
Element
Function: ConnectSetupWizard()
function ConnectSetupWizard(__namedParameters: Readonly<ConnectSetupWizardProps>): Element;The setup wizard's full-page frame: the step strip, the step's title and description, its content, and a sticky footer - Back on the left, Skip and the step's one primary on the right.
It needs no app shell, because the lock page has none: a workspace
held on /onboarding must not be shown a sidebar full of destinations it
cannot reach. It is centred in its own min-h-dvh column and brings its
own background, so it also works inside a shell for an app that keeps one.
A Server Component with no state. The current step is a fact about the
route and the page passes it as current; reading usePathname() here
would put URL data in a shared shell.
Only primary is primary. Back is tertiary and the app's skip control
should be secondary, so a step reads as one decision with two ways out.
The skip and primary controls are the app's own nodes - typically
server-action buttons that record the skip and move on (nextSetupPath).
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectSetupWizardProps> |
Returns
Element
Function: ConnectStatStrip()
function ConnectStatStrip(__namedParameters: Readonly<ConnectStatStripProps>): Element;A row of stat tiles - the headline strip above a report's charts.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectStatStripProps> |
Returns
Element
Function: ConnectStatTile()
function ConnectStatTile(__namedParameters: Readonly<ConnectStatTileProps>): Element;A label, a figure, how it moved, and one line saying what the figure counts.
Presentational and server-renderable, like the rest of this entry point -
the figure comes formatted, the delta comes computed (metricDelta), and
the tile only lays them out. Semantic tokens only: the pill is success /
danger tinted, the label and hint muted.
Absence is said, not drawn. No delta means no pill (not a grey "0%"),
and partial adds "Some days not measured yet" so a figure summed over the
days the rollup reached is not read as the whole period.
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<ConnectStatTileProps> |
Returns
Element
Function: ConnectStatusChip()
function ConnectStatusChip<TStatus>(__namedParameters: Readonly<{
status: TStatus;
presentation: ConnectStatusPresentation<TStatus>;
className?: string;
}>): Element;One status, rendered the way every status should be.
Domain knowledge stays in the domain: each caller exports its own
ConnectStatusPresentation next to the union it describes, and this renders
it. A status whose rendering carries real logic - a scheduled cancellation
outranking a running trial, say - keeps its own component and calls this for
the final chip. A map cannot express precedence.
Type Parameters
| Type Parameter |
|---|
TStatus extends string |
Parameters
| Parameter | Type |
|---|---|
__namedParameters | Readonly<{ status: TStatus; presentation: ConnectStatusPresentation<TStatus>; className?: string; }> |
Returns
Element
Function: setupStepView()
function setupStepView(step: SetupStepStatus, granted: readonly string[]): SetupStepView;The one rule every surface reads for a step's button and note.
done- no action (the checklist adds its own "Change"); the line is the fact that made it done.blocked- no action for anybody; "Waiting on <x>".in_review- "View" for somebody who may act, the review reason as the line, no owner note (there is nothing for the owner to do either).skipped- "Finish";todo- the step's own verb. Anybody else is told whose job it is.
Parameters
| Parameter | Type |
|---|---|
step | SetupStepStatus |
granted | readonly string[] |