blob: 6a795ed57c348c766c9f258488cae53d34c2b220 [file]
import type { ReactNode } from 'react';
import type { Search } from './icons.js';
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from './primitives/empty.js';
import { Button as UiButton, cn } from './ui.js';
type EmptyStateIcon = typeof Search;
/**
* PR-EMPTY-STATE-COMPONENT-0: shared empty-state container. Folds the
* 4 visual duplicates (skills empty / sessions empty / module fallbacks /
* plan reminders empty) into a single declaration so the next empty
* surface lands consistent by default and the icon-sizing /
* paragraph-spacing / CTA-placement decisions only live in one
* place. The `.maka-empty-state*` CSS family is unchanged.
*
* Body accepts `ReactNode` so callers can keep inline `<code>` for
* the skills install instructions; CTAs are rendered as the canonical
* the shared Button primitive so we never grow a competing empty-state
* action variant.
*/
interface EmptyStateProps {
/** Required for the card variant; unused by inline. */
Icon?: EmptyStateIcon;
title: string;
body: ReactNode;
/** Convergence R5: `inline` renders a single quiet muted line (no icon,
* no card) for in-flow "waiting/none yet" notes; `card` is the full
* empty surface. */
variant?: 'card' | 'inline';
cta?: { label: string; onClick: () => void; disabled?: boolean };
secondaryCta?: { label: string; onClick: () => void; disabled?: boolean };
/** Optional extra class on the container (e.g. `maka-plan-empty`). */
extraClassName?: string;
/** Optional `data-empty-view` passthrough for e2e-fixture selectors. */
dataEmptyView?: string;
}
export function EmptyState(props: EmptyStateProps) {
if (props.variant === 'inline') {
return (
<p
className={cn(
'm-0 text-[length:var(--font-size-ui)] leading-normal text-[color:var(--muted-foreground)]',
props.extraClassName,
)}
data-slot="empty-state-inline"
data-empty-view={props.dataEmptyView}
>
{props.title}
{props.body != null && props.body !== '' ? <> · {props.body}</> : null}
</p>
);
}
// `border` before `border-border`: on its own, `border-border` sets only a
// colour, so the card shipped at border-width 0 and the edge the author
// meant to draw never rendered. What was actually outlining the card was
// the `0 0 0 1px var(--border)` ring inside `shadow-maka-panel` — and that
// shadow is the OVERLAY family (mention popup, model picker, select popup,
// dialog). This is an in-page card, so it carried a floating panel's
// drop shadow for no reason and leaned on its ring for the missing border.
// One real hairline instead, same token as .settingsRows / the skill cards.
const className = cn(
'maka-empty-state rounded-md border border-border bg-card/70 p-8 text-card-foreground',
props.extraClassName,
);
return (
<Empty className={className} data-empty-view={props.dataEmptyView}>
<EmptyHeader>
<EmptyMedia variant="icon" className="maka-empty-state-media">
{props.Icon && <props.Icon className="maka-empty-state-icon size-6 text-muted-foreground" />}
</EmptyMedia>
<EmptyTitle className="maka-empty-state-title">{props.title}</EmptyTitle>
<EmptyDescription className="maka-empty-state-body">{props.body}</EmptyDescription>
</EmptyHeader>
{(props.cta || props.secondaryCta) && (
<EmptyContent className="maka-empty-state-actions mt-0">
{props.cta && (
<UiButton
className="maka-empty-state-cta"
type="button"
onClick={props.cta.onClick}
disabled={props.cta.disabled}
>
{props.cta.label}
</UiButton>
)}
{props.secondaryCta && (
<UiButton
variant="ghost"
className="maka-empty-state-cta"
type="button"
onClick={props.secondaryCta.onClick}
disabled={props.secondaryCta.disabled}
>
{props.secondaryCta.label}
</UiButton>
)}
</EmptyContent>
)}
</Empty>
);
}