# 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.

```ts
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?`. |

```ts
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?`. |

```ts
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. |

```ts
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?`. |

```ts
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`. |

```ts
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)`. |

```ts
type MenuItem = { label: string; icon?: ReactNode; onSelect: () => void; danger?: boolean } | "divider";
```

## Theme

```ts
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

```ts
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`.
