"use client";
import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { IconAlertTriangle, IconX } from "@tabler/icons-react";
import { cn } from "../lib/utils";
import { staggerDelay, transitions } from "../lib/motion";
import { Button } from "../components/button";
/**
* Page-level patterns for workspace apps: the frame, toolbars, the
* loading/empty/error trio, list + detail, boards, and settings. They
* carry Coline's spacing and type so apps look native without styling.
*/
export function AppPage({
title,
description,
eyebrow,
actions,
children,
width = "wide",
className,
}: {
title: string;
description?: React.ReactNode;
eyebrow?: React.ReactNode;
actions?: React.ReactNode;
children: React.ReactNode;
width?: "wide" | "reading";
className?: string;
}) {
const reduceMotion = useReducedMotion();
return (
{eyebrow ?
{eyebrow}
: null}
{title}
{description ?
{description}
: null}
{actions ? {actions}
: null}
{children}
);
}
export function PageToolbar({
children,
label = "View controls",
className,
}: {
children: React.ReactNode;
label?: string;
className?: string;
}) {
return (
{children}
);
}
export function EmptyState({
title,
description,
action,
icon,
className,
}: {
title: string;
description?: React.ReactNode;
action?: React.ReactNode;
icon?: React.ReactNode;
className?: string;
}) {
const reduceMotion = useReducedMotion();
return (
{icon ? (
{icon}
) : null}
{title}
{description ? {description}
: null}
{action ? {action}
: null}
);
}
export function AppErrorState({ message, onRetry }: { message: string; onRetry?: () => void }) {
return (
Something went wrong
{message}
{onRetry ? (
) : null}
);
}
export function AppLoadingState({ label = "Loading", rows = 5 }: { label?: string; rows?: number }) {
return (
{Array.from({ length: Math.max(1, Math.min(rows, 12)) }, (_, index) => (
))}
{label}
);
}
const STATUS_TONE = {
neutral: "bg-muted text-muted-foreground",
info: "bg-action/10 text-action",
success: "bg-positive/10 text-positive-fg",
warning: "bg-attention/12 text-attention-fg",
danger: "bg-risk/10 text-risk-fg",
} as const;
export function StatusBadge({
children,
tone = "neutral",
}: {
children: React.ReactNode;
tone?: keyof typeof STATUS_TONE;
}) {
return (
{children}
);
}
export function SplitView({
list,
detail,
label = "Details",
}: {
list: React.ReactNode;
detail?: React.ReactNode;
label?: string;
}) {
const reduceMotion = useReducedMotion();
return (
{detail ? (
{detail}
) : null}
);
}
export function DetailPanel({
title,
subtitle,
onClose,
children,
actions,
}: {
title: string;
subtitle?: React.ReactNode;
onClose?: () => void;
children: React.ReactNode;
actions?: React.ReactNode;
}) {
return (
{subtitle ?
{subtitle}
: null}
{title}
{onClose ? (
) : null}
{children}
{actions ? : null}
);
}
export interface RecordColumn {
key: string;
label: string;
render: (record: T) => React.ReactNode;
secondary?: boolean;
align?: "start" | "end";
}
/**
* A light list-style table. For sorting, selection, keyboard navigation,
* and large data, use `DataGrid`.
*/
export function RecordTable({
records,
columns,
rowKey,
onOpen,
label = "Records",
empty,
}: {
records: readonly T[];
columns: readonly RecordColumn[];
rowKey: (record: T) => string;
onOpen?: (record: T) => void;
label?: string;
empty?: React.ReactNode;
}) {
const reduceMotion = useReducedMotion();
if (!records.length) return <>{empty ?? }>;
return (
{columns.map((column) => (
|
{column.label}
|
))}
{records.map((record, index) => (
onOpen(record) : undefined}
>
{columns.map((column, columnIndex) => (
{columnIndex === 0 && onOpen ? (
) : (
column.render(record)
)}
|
))}
))}
);
}
/**
* Static board layout. For drag and drop between columns, use
* `KanbanBoard`.
*/
export function Board({ children, label = "Board" }: { children: React.ReactNode; label?: string }) {
return (
{children}
);
}
export function BoardColumn({
title,
count,
children,
action,
}: {
title: string;
count?: number;
children: React.ReactNode;
action?: React.ReactNode;
}) {
return (
{title}
{count !== undefined ? (
{count}
) : null}
{action ? {action}
: null}
{children}
);
}
export function BoardCard({
title,
children,
footer,
onOpen,
}: {
title: string;
children?: React.ReactNode;
footer?: React.ReactNode;
onOpen?: () => void;
}) {
return (
{onOpen ? (
) : (
title
)}
{children ? {children}
: null}
{footer ? (
) : null}
);
}
export function SettingsSection({
title,
description,
children,
}: {
title: string;
description?: string;
children: React.ReactNode;
}) {
const id = React.useId();
return (
{title}
{description ?
{description}
: null}
{children}
);
}