Building apps
The kit
Every component, hook and helper that @ripping/kit exports, with the props that matter.
What the kit is
@ripping/kit is the UI every generated app imports instead of writing. It is one file (src/kit/index.tsx on Ripping), bundled and served at /kit/kit-<hash>.js; the app never installs it. Everything in it is styled with the project's design tokens (bg-bg, bg-surface, text-ink, text-mute, bg-accent, text-accent-fg, rounded-card, rounded-control, p-gutter), so it follows src/design.css and the dark class.
import { ListScreen, useToast, Button } from "@ripping/kit";
Wrap the app once in <KitProvider> (the skeleton's App.tsx already does): it provides toasts and confirms to every screen.
Screens (one call each)
| Export | Props that matter |
|---|---|
ListScreen<T> | title, description?, rows, columns, searchKeys?, filters?, fields (the form's FieldSpec[]), onCreate?(values), onUpdate?(id, values), onDelete?(id), detail?(row) → { title, subtitle?, badge?, fields, children? }, itemName?, createLabel?, empty?, actions?, toolbar?, toForm?(row), initialSort?, bulkActions?, dense?. Table with search and filters, New opens a form in a Sheet, a row opens its detail with Edit and Delete, bulk delete with confirm, toasts. Delete is added to the bulk actions when onDelete is given. |
OverviewScreen | title, description?, stats: { label, value, delta?, hint?, icon? }[], charts?: ChartSpec[], activity?: { title?, items }, actions?, children?. Stat tiles, one or two charts, recent activity. |
DetailScreen | title, subtitle?, badge?, fields: { label, value }[], actions?, back?, onBack?, sections?: { title, children }[], children?. |
type ChartSpec =
| { title: string; type: "line"; series: { label: string; points: Point[]; color?: string }[]; format?: (n: number) => string }
| { title: string; type: "bar"; data: { label: string; value: number }[]; format?: (n: number) => string };
Wire a list screen to the store directly: rows={items} onCreate={add} onUpdate={update} onDelete={remove}.
Shell and layout
| Export | Props that matter |
|---|---|
AppShell | title, subtitle?, nav: NavItem[], active, onNavigate(id), actions?, footer?, children. Sidebar and top bar on wide screens; header and a bottom tab bar (first 5 items, the rest behind a More button) on phones. |
Page | title, description?, actions?, back?, onBack?, children. Max width 6xl, gutter padding. |
Section | title?, description?, actions?, className?, children. A card with an optional header row. |
Tabs | tabs: { id, label, count? }[], active, onChange(id). |
Card | padded? (default true), className?. |
type NavItem = { id: string; label: string; icon?: ReactNode; badge?: string | number };
Forms and inputs
| Export | Props that matter |
|---|---|
Button | variant?: "primary" | "secondary" | "ghost" | "danger", size?: "sm" | "md" | "lg", loading? (shows a spinner and disables), icon?, plus button attributes. type defaults to "button". |
IconButton | label (required; becomes aria-label and title), plus button attributes. |
Input, Textarea | Native attributes. |
Select | options: { value, label }[], placeholder? (rendered as an empty first option), plus select attributes. |
Checkbox | label?. Without a label it is the bare box, for wrapping in your own <label>. |
Field | label, htmlFor?, help?, error?, required?, children. Label, control, then error or help text. |
RecordForm | fields: FieldSpec[], value?, onSubmit(values), onCancel?, submitLabel? (default "Save"), cancelLabel?, saving?. Validation inline; disabled while saving. |
type FieldSpec = {
key: string; label: string;
type?: "text" | "email" | "number" | "money" | "date" | "select" | "textarea" | "checkbox" | "url" | "tel";
required?: boolean; options?: { value: string; label: string }[]; placeholder?: string; help?: string;
min?: number; max?: number; validate?: (value: unknown, values: Record<string, unknown>) => string | undefined;
};
RecordForm hands onSubmit a money field as cents (the input shows dollars), a number field as a number (or null when empty), everything else as entered. Built-in messages: Required, Enter a valid email, Enter a number, At least N, At most N.
Data display
| Export | Props that matter |
|---|---|
DataTable<T extends { id: string }> | rows, columns: Column<T>[], searchKeys?, searchPlaceholder?, filters?: Filter<T>[], pageSize? (default 10), initialSort?: { key, dir? }, onRowClick?, rowActions?(row), selectable?, bulkActions?: { label, onClick(ids), danger? }[], empty?: { title, body?, action?, onAction? }, toolbar?, dense?. Table on wide screens, cards on phones. |
DetailView | title, subtitle?, badge?, fields: { label, value }[], actions?, back?, onBack?, children?. Empty values render as —. |
Badge | tone?: "neutral" | "ok" | "warn" | "err" | "info" | "accent". |
asBadge(b) | Turns a BadgeLike (ReactNode or { label, tone? }) into a node. |
EmptyState | icon?, title, body?, action?, onAction?. |
Avatar | name?, src?, size? (default 32). Initials when there is no image. |
Alert | tone?: "info" | "warn" | "err" | "ok", title?. |
type Column<T> = { key: string; header: string; render?: (row: T) => ReactNode; sortable?: boolean; align?: "left" | "right"; width?: string; primary?: boolean };
type Filter<T> = { key: string; label: string; options: { value: string; label: string }[]; test?: (row: T, value: string) => boolean };
key may be a dotted path ("client.name"). The primary column is the card title on phones; without one, the first column is.
Dashboard pieces
| Export | Props that matter |
|---|---|
StatGrid | children in a 1/2/4-column grid. |
StatTile | label, value, delta? (a percentage; coloured by sign), hint?, icon?. |
LineChart | series: { label, points: Point[], color? }[], height? (default 220), format?, title?. Hover readout; a legend when there is more than one series. |
BarChart | data: { label, value }[], height? (default 200), format?, title?. |
ActivityList | items: { id, title, meta?, when, icon? }[], empty? (default "Nothing yet"). |
DateRangeFilter | value: Range, onChange. |
type Point = { x: string; y: number };
type Range = "7d" | "30d" | "90d" | "all";
Feedback and overlays
| Export | Props that matter |
|---|---|
KitProvider | Wrap the app once: ToastProvider + ConfirmProvider. |
useToast() | Returns toast. toast("Saved"), toast("No", "err"), toast.success(text), toast.error(text); const { toast } = useToast() also works. Toasts last 2.8 s. |
useConfirm() | await confirm({ title, body?, confirmLabel?, danger? }) resolves to a boolean. |
Modal | open, onClose, title, footer?, size?: "sm" | "md" | "lg". Centred on wide screens, bottom sheet on phones; Escape closes. |
Sheet | open, onClose, title, footer?, width? (a max-width class, default "max-w-md"). Side panel from the right; bottom sheet on phones. |
Menu | label, trigger, items: MenuItem[], header?, align?: "start" | "end". Arrow keys, Enter and Escape work. |
Loading | rows? (default 3). Skeleton rows with role="status". |
Skeleton | className?. |
ErrorBoundary | fallback(reset, error), onError?(error, info). |
type MenuItem = { label: string; icon?: ReactNode; onSelect: () => void; danger?: boolean } | "divider";
Theme
function useTheme(): [Theme, (t: Theme) => void]; // Theme = "light" | "dark" | "system"
function ThemeToggle({ className }: { className?: string }): JSX.Element;
The theme is saved in localStorage under theme, shared by every screen, and applied as <html class="dark"> with color-scheme. The skeleton calls useTheme() once at the root.
Helpers
function cn(...xs: (string | false | null | undefined)[]): string;
function money(cents: number, currency = "USD", locale = "en-US"): string; // money(1999) → "$19.99"
function formatDate(d: string | Date | null | undefined, opts?: Intl.DateTimeFormatOptions): string;
function relativeTime(d: string | Date, now = Date.now()): string; // "just now", "5 min ago", "3d ago"
function groupByMonth<T>(rows: T[], dateKey: keyof T, valueKey?: keyof T, months = 6): { label: string; value: number }[];
function inRange<T>(rows: T[], dateKey: keyof T, range: Range, now = Date.now()): T[];
function useLocal<T>(key: string, initial: T): [T, (v: T) => void]; // one value in localStorage
function useKeyActivate(fn: () => void): (e: React.KeyboardEvent) => void; // Enter or Space runs fn
groupByMonth sums valueKey per month, or counts rows when it is omitted. useLocal is for per-browser UI state only; app data belongs in src/lib/store.ts.
Exported types
ButtonProps, BadgeTone, BadgeLike, MenuItem, Theme, ToastFn, NavItem, Column, Filter, DataTableProps, FieldSpec, Point, Range, ListScreenProps, ChartSpec.