feat(ui): add operational core primitives

This commit is contained in:
Saravana Achu Mac 2026-05-06 11:10:08 -07:00
parent e619fa8eb5
commit 23e140009f
7 changed files with 304 additions and 0 deletions

View File

@ -0,0 +1,31 @@
import * as React from 'react';
import { clsx } from 'clsx';
export interface DiffCardProps extends React.HTMLAttributes<HTMLDivElement> {
before: React.ReactNode;
after: React.ReactNode;
beforeLabel?: string;
afterLabel?: string;
}
export function DiffCard({
before,
after,
beforeLabel = 'Before',
afterLabel = 'After',
className,
...props
}: DiffCardProps) {
return (
<div className={clsx('grid gap-3 md:grid-cols-2', className)} {...props}>
<div className="grid gap-2 rounded-md border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-3">
<strong className="text-sm text-[var(--bl-text-primary)]">{beforeLabel}</strong>
<div className="whitespace-pre-wrap text-sm text-[var(--bl-text-secondary)]">{before}</div>
</div>
<div className="grid gap-2 rounded-md border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-3">
<strong className="text-sm text-[var(--bl-text-primary)]">{afterLabel}</strong>
<div className="whitespace-pre-wrap text-sm text-[var(--bl-text-secondary)]">{after}</div>
</div>
</div>
);
}

View File

@ -0,0 +1,26 @@
import * as React from 'react';
import { clsx } from 'clsx';
import { Button, type ButtonProps } from './Button.js';
export interface IconButtonProps extends Omit<ButtonProps, 'children'> {
icon: React.ReactNode;
label: string;
}
export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
({ icon, label, className, size = 'sm', variant = 'ghost', ...props }, ref) => (
<Button
ref={ref}
size={size}
variant={variant}
aria-label={label}
title={label}
className={clsx('aspect-square px-0', className)}
{...props}
>
{icon}
</Button>
)
);
IconButton.displayName = 'IconButton';

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import { clsx } from 'clsx';
export interface ListItemButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
selected?: boolean;
}
export const ListItemButton = React.forwardRef<HTMLButtonElement, ListItemButtonProps>(
({ selected, className, children, ...props }, ref) => (
<button
ref={ref}
type="button"
aria-pressed={selected}
className={clsx(
'w-full rounded-md border p-3 text-left transition-colors',
'border-[var(--bl-border)] bg-[var(--bl-surface-muted)] text-[var(--bl-text-primary)]',
'hover:border-[var(--bl-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--bl-accent)]',
selected && 'border-[var(--bl-accent)] bg-[var(--bl-accent-muted,var(--bl-surface-card))]',
className
)}
{...props}
>
{children}
</button>
)
);
ListItemButton.displayName = 'ListItemButton';

View File

@ -0,0 +1,77 @@
import * as React from 'react';
import { clsx } from 'clsx';
export interface PanelProps extends React.HTMLAttributes<HTMLElement> {
as?: 'section' | 'aside' | 'article' | 'div';
density?: 'compact' | 'normal' | 'spacious';
}
const panelPadding: Record<NonNullable<PanelProps['density']>, string> = {
compact: 'p-3',
normal: 'p-5',
spacious: 'p-6',
};
export function Panel({
as: Comp = 'section',
density = 'normal',
className,
children,
...props
}: PanelProps) {
return (
<Comp
className={clsx(
'rounded-lg border bg-[var(--bl-surface-card)] border-[var(--bl-border)] shadow-sm',
panelPadding[density],
className
)}
{...props}
>
{children}
</Comp>
);
}
export type PanelHeaderProps = React.HTMLAttributes<HTMLDivElement>;
export function PanelHeader({ className, children, ...props }: PanelHeaderProps) {
return (
<div className={clsx('flex items-center justify-between gap-3', className)} {...props}>
{children}
</div>
);
}
export type PanelBodyProps = React.HTMLAttributes<HTMLDivElement>;
export function PanelBody({ className, children, ...props }: PanelBodyProps) {
return (
<div className={clsx('grid gap-3', className)} {...props}>
{children}
</div>
);
}
export type PanelTitleProps = React.ComponentPropsWithoutRef<'h2'>;
export function PanelTitle({ className, children, ...props }: PanelTitleProps) {
return (
<h2
className={clsx('m-0 text-base font-semibold text-[var(--bl-text-primary)]', className)}
{...props}
>
{children}
</h2>
);
}
export type PanelDescriptionProps = React.ComponentPropsWithoutRef<'p'>;
export function PanelDescription({ className, children, ...props }: PanelDescriptionProps) {
return (
<p className={clsx('m-0 text-sm text-[var(--bl-text-secondary)]', className)} {...props}>
{children}
</p>
);
}

View File

@ -0,0 +1,59 @@
import * as React from 'react';
import { clsx } from 'clsx';
export type StatusTone = 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'accent';
export interface StatusBadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
tone?: StatusTone;
dot?: boolean;
}
const toneClasses: Record<StatusTone, string> = {
success:
'bg-[var(--bl-success-muted,var(--bl-surface-muted))] text-[var(--bl-success)] border-[var(--bl-success)]',
warning:
'bg-[var(--bl-warning-muted,var(--bl-surface-muted))] text-[var(--bl-warning)] border-[var(--bl-warning)]',
danger:
'bg-[var(--bl-danger-muted,var(--bl-surface-muted))] text-[var(--bl-danger)] border-[var(--bl-danger)]',
info: 'bg-[var(--bl-info-muted,var(--bl-surface-muted))] text-[var(--bl-info,var(--bl-accent))] border-[var(--bl-info,var(--bl-accent))]',
neutral: 'bg-[var(--bl-surface-muted)] text-[var(--bl-text-secondary)] border-[var(--bl-border)]',
accent:
'bg-[var(--bl-accent-muted,var(--bl-surface-muted))] text-[var(--bl-text-primary)] border-[var(--bl-accent)]',
};
export function StatusDot({
tone = 'neutral',
className,
}: {
tone?: StatusTone;
className?: string;
}) {
return (
<span
aria-hidden="true"
className={clsx('h-1.5 w-1.5 rounded-full bg-current', toneClasses[tone], className)}
/>
);
}
export function StatusBadge({
tone = 'neutral',
dot,
className,
children,
...props
}: StatusBadgeProps) {
return (
<span
className={clsx(
'inline-flex items-center gap-1.5 rounded-full border px-2 py-0.5 text-xs font-medium',
toneClasses[tone],
className
)}
{...props}
>
{dot && <StatusDot tone={tone} />}
{children}
</span>
);
}

View File

@ -0,0 +1,61 @@
import * as React from 'react';
import { clsx } from 'clsx';
import { StatusBadge, type StatusTone } from './StatusBadge.js';
export interface TimelineItem {
id: string;
title: React.ReactNode;
description?: React.ReactNode;
meta?: React.ReactNode;
status?: React.ReactNode;
tone?: StatusTone;
}
export interface TimelineProps extends React.HTMLAttributes<HTMLElement> {
items: TimelineItem[];
emptyLabel?: string;
}
export function Timeline({
items,
emptyLabel = 'No activity yet.',
className,
...props
}: TimelineProps) {
if (items.length === 0) {
return (
<section className={clsx('text-sm text-[var(--bl-text-secondary)]', className)} {...props}>
{emptyLabel}
</section>
);
}
return (
<section className={clsx('grid gap-3', className)} {...props}>
{items.map(item => (
<article key={item.id} className="grid grid-cols-[auto_1fr] gap-3">
<span
aria-hidden="true"
className="mt-1 h-2.5 w-2.5 rounded-full bg-[var(--bl-accent)]"
/>
<div className="grid gap-1 rounded-md border border-[var(--bl-border)] bg-[var(--bl-surface-muted)] p-3">
<div className="flex flex-wrap items-start justify-between gap-2">
<strong className="text-sm text-[var(--bl-text-primary)]">{item.title}</strong>
{item.meta && (
<span className="text-xs text-[var(--bl-text-secondary)]">{item.meta}</span>
)}
</div>
{item.description && (
<div className="text-sm text-[var(--bl-text-secondary)]">{item.description}</div>
)}
{item.status && (
<StatusBadge tone={item.tone} className="w-fit">
{item.status}
</StatusBadge>
)}
</div>
</article>
))}
</section>
);
}

View File

@ -1,4 +1,5 @@
export { Button, type ButtonProps } from './components/Button';
export { IconButton, type IconButtonProps } from './components/IconButton';
export {
Toast,
ToastProvider,
@ -10,10 +11,31 @@ export {
export { Modal, type ModalProps } from './components/Modal';
export { ConfirmDialog, type ConfirmDialogProps } from './components/ConfirmDialog';
export { Badge, type BadgeProps } from './components/Badge';
export {
StatusBadge,
StatusDot,
type StatusBadgeProps,
type StatusTone,
} from './components/StatusBadge';
export { EmptyState, type EmptyStateProps } from './components/EmptyState';
export { Input, type InputProps } from './components/Input';
export { Textarea, type TextareaProps } from './components/Textarea';
export { Card, CardHeader, CardTitle, CardDescription, type CardProps } from './components/Card';
export {
Panel,
PanelBody,
PanelDescription,
PanelHeader,
PanelTitle,
type PanelBodyProps,
type PanelDescriptionProps,
type PanelHeaderProps,
type PanelProps,
type PanelTitleProps,
} from './components/Panel';
export { ListItemButton, type ListItemButtonProps } from './components/ListItemButton';
export { Timeline, type TimelineItem, type TimelineProps } from './components/Timeline';
export { DiffCard, type DiffCardProps } from './components/DiffCard';
export { Label, type LabelProps } from './components/Label';
export { Select, type SelectProps } from './components/Select';
export { Separator, type SeparatorProps } from './components/Separator';