Building apps

The kit

Every component, hook and helper that @ripping/kit exports, with the props that matter.

View as markdown

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)

ExportProps 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.
OverviewScreentitle, description?, stats: { label, value, delta?, hint?, icon? }[], charts?: ChartSpec[], activity?: { title?, items }, actions?, children?. Stat tiles, one or two charts, recent activity.
DetailScreentitle, 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

ExportProps that matter
AppShelltitle, 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.
Pagetitle, description?, actions?, back?, onBack?, children. Max width 6xl, gutter padding.
Sectiontitle?, description?, actions?, className?, children. A card with an optional header row.
Tabstabs: { id, label, count? }[], active, onChange(id).
Cardpadded? (default true), className?.
type NavItem = { id: string; label: string; icon?: ReactNode; badge?: string | number };

Forms and inputs

ExportProps that matter
Buttonvariant?: "primary" | "secondary" | "ghost" | "danger", size?: "sm" | "md" | "lg", loading? (shows a spinner and disables), icon?, plus button attributes. type defaults to "button".
IconButtonlabel (required; becomes aria-label and title), plus button attributes.
Input, TextareaNative attributes.
Selectoptions: { value, label }[], placeholder? (rendered as an empty first option), plus select attributes.
Checkboxlabel?. Without a label it is the bare box, for wrapping in your own <label>.
Fieldlabel, htmlFor?, help?, error?, required?, children. Label, control, then error or help text.
RecordFormfields: 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

ExportProps 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.
DetailViewtitle, subtitle?, badge?, fields: { label, value }[], actions?, back?, onBack?, children?. Empty values render as .
Badgetone?: "neutral" | "ok" | "warn" | "err" | "info" | "accent".
asBadge(b)Turns a BadgeLike (ReactNode or { label, tone? }) into a node.
EmptyStateicon?, title, body?, action?, onAction?.
Avatarname?, src?, size? (default 32). Initials when there is no image.
Alerttone?: "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

ExportProps that matter
StatGridchildren in a 1/2/4-column grid.
StatTilelabel, value, delta? (a percentage; coloured by sign), hint?, icon?.
LineChartseries: { label, points: Point[], color? }[], height? (default 220), format?, title?. Hover readout; a legend when there is more than one series.
BarChartdata: { label, value }[], height? (default 200), format?, title?.
ActivityListitems: { id, title, meta?, when, icon? }[], empty? (default "Nothing yet").
DateRangeFiltervalue: Range, onChange.
type Point = { x: string; y: number };
type Range = "7d" | "30d" | "90d" | "all";

Feedback and overlays

ExportProps that matter
KitProviderWrap 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.
Modalopen, onClose, title, footer?, size?: "sm" | "md" | "lg". Centred on wide screens, bottom sheet on phones; Escape closes.
Sheetopen, onClose, title, footer?, width? (a max-width class, default "max-w-md"). Side panel from the right; bottom sheet on phones.
Menulabel, trigger, items: MenuItem[], header?, align?: "start" | "end". Arrow keys, Enter and Escape work.
Loadingrows? (default 3). Skeleton rows with role="status".
SkeletonclassName?.
ErrorBoundaryfallback(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.

The kit · Ripping docs