# @arkite-ui/core v0.23.1 > React components for multi-tenant SaaS admin panels. Tailwind CSS v4 + > Radix UI + TypeScript. Pure UI only: no business logic, auth, stores, or > routing — data comes in via props, events go out via callbacks. > > Arkite UI is an open-source project by Foson (https://foson.co), a Taiwan-based > software company. Docs: https://ui.foson.co · Repo: https://github.com/foson-co/arkite-ui > Not affiliated with Arkite NV (arkite.com, industrial operator guidance). ## Setup ```bash pnpm add @arkite-ui/core # peer deps: react react-dom tailwindcss zustand lucide-react # optional: framer-motion (Animated* variants) ``` ```css /* app.css — Tailwind v4, tokens included */ @import "tailwindcss"; @import "@arkite-ui/core/styles.css"; ``` ## Rules for generated code 1. Import UI from `@arkite-ui/core` — never rebuild primitives that exist (see inventory below). 2. Use semantic token utilities only (`bg-card`, `text-muted-foreground`, `border-border`) — never raw palette classes (`bg-red-50`), never `dark:` color overrides (tokens handle dark mode), never arbitrary values (`text-[13px]`). 3. Components follow the controlled/uncontrolled React convention: `value`+`onChange` controls, `defaultValue` doesn't. 4. Keep API calls, routes, and business models in the app layer. 5. Building a whole page? Match it against the recipes below **before** assembling one from scratch, then **install it instead of writing it**: `npx arkite-ui add ` writes the tested composition into the project (`--out`, `--name`, `--force`), leaving you only the sample data and API calls to replace. Generating the wiring by hand is the step that introduces bugs the recipe already solved. Full design rules (color/spacing/typography/layout recipes): DESIGN.md, shipped inside this package — also embedded in llms-full.txt. ## Core patterns ### Server-paginated table `useServerTable` owns the six controlled props server mode needs; you supply `data` + `totalRows` and fetch on `queryKey`: ```tsx import { DataTable, useServerTable, type Column } from '@arkite-ui/core' const table = useServerTable({ initialPageSize: 20 }) const [page, setPage] = useState({ items: [], total: 0 }) useEffect(() => { fetchUsers(table.query).then(setPage) // query = { page, pageSize, sort, filters } }, [table.queryKey]) r.id} {...table.props} /> ``` Client-side tables need none of that: pass `data` and the table filters, sorts, and paginates itself. ### Toasts ```tsx import { useToast, ToastContainer } from '@arkite-ui/core' // once, near the app root: const toast = useToast() toast.success('Saved', { description: 'Changes are live.' }) // error handling in catch blocks — wire the app's parser once at startup: toast.configure({ formatError: getErrorMessage }) // then every call site is one line (title = prefix, description = parsed message): catch (err) { toast.fromError(err, { prefix: 'Failed to save' }) } ``` ### Theming ```tsx import { createTheme, applyTheme } from '@arkite-ui/core' applyTheme(createTheme({ primary: '#7c3aed' })) // HSL tokens from one hex ``` A project's brand belongs in a committed `arkite.theme.json` (`{ name, primary, accent, radius }`) rather than hand-copied CSS variables. Generate the CSS from it with `npx arkite-ui theme apply`, or in-process via `themeFileToCSS(parseThemeFile(json))` from `@arkite-ui/core/theme` — a server-safe entry that loads no components. `parseThemeFile` rejects malformed hex and unknown keys, which `createTheme` alone would pass through as `NaN` tokens. ## Recipes — page-level compositions Whole pages assembled from the library, each one rendered and tested in CI. Machine-readable index: registry.json (ships in the package). Install one with `npx arkite-ui add ` — do not retype it. ### crud-list-page — CRUD List Page Search + filters over a sortable, selectable, paginated table, with bulk actions and a guarded destructive path. - Use when: A list page whose rows are already in memory and the page needs filtering, sorting, multi-select, bulk actions, or a delete confirmation. - Not when: The server does the filtering/sorting/slicing — use `server-side-table`. A read-only handful of rows with no interaction — use `Table` directly, no recipe needed. - Composes: Badge, BulkActionBar, Button, DataTable, DeleteConfirmDialog, FilterBar, FilterBarActions, FilterBarFilters, FilterBarSearch, FilterSelect, PageHeader, ToastContainer, toast - Source (ships in the package, copy it): src/stories/recipes/CrudListPage.demo.tsx — docs: https://ui.foson.co/?path=/docs/recipes-crud-list-page--docs ### server-side-table — Server-Side Table A table whose sorting and pagination round-trip to the server: the page owns the query state, the API returns one slice plus a total. - Use when: The API returns one pre-sorted page plus a total count, so the table can never know the full ordering — sorting and pagination must be controlled. - Not when: All rows fit in memory — leave the table uncontrolled and use `crud-list-page`. - Composes: Badge, DataTable, PageHeader, useServerTable - Source (ships in the package, copy it): src/stories/recipes/ServerSideTable.demo.tsx — docs: https://ui.foson.co/?path=/docs/recipes-server-side-table--docs ### dashboard-page — Dashboard A KPI row over compact widget cards — sparkline trends and a recent-activity table in one responsive grid. - Use when: An overview page that answers 'is everything okay?' at a glance and links out for detail. - Not when: The page IS the detail view, or the numbers need drill-down in place — build a list or detail page instead. Full charting belongs in a dedicated chart library, not `Sparkline`. - Composes: Badge, Card, CardContent, CardHeader, DataTable, PageHeader, Sparkline, StatCard, StatGroup - Source (ships in the package, copy it): src/stories/recipes/DashboardPage.demo.tsx — docs: https://ui.foson.co/?path=/docs/recipes-dashboard--docs ### detail-drawer-page — Detail + Drawer Edit A record's read view with an edit drawer, a dirty-close guard, and a confirmed destructive action. - Use when: Editing a record while the user still needs the record on screen for reference. - Not when: The edit should interrupt and take over (short one-shot forms, confirmations) — use `Modal`. A full create/edit page with many sections — use `form-page`. - Composes: Badge, Button, Card, ConfirmDialog, DescriptionItem, DescriptionList, Drawer, Form, FormActions, FormControl, FormField, FormLabel, FormMessage, Input, PageHeader, Textarea, ToastContainer, toast - Source (ships in the package, copy it): src/stories/recipes/DetailDrawerPage.demo.tsx — docs: https://ui.foson.co/?path=/docs/recipes-detail-drawer-edit--docs ### form-page — Form Page A sectioned create/edit page: grouped fields, mixed control types, submit-time validation, and a guarded submit. - Use when: A create or edit page big enough to need sections, help text, and inline validation messages. - Not when: The form is short enough to live in a drawer next to a read view — use `detail-drawer-page`. - Composes: Button, Form, FormActions, FormControl, FormDescription, FormField, FormLabel, FormMessage, FormSection, Input, PageHeader, Select, Switch, Textarea, ToastContainer, toast - Source (ships in the package, copy it): src/stories/recipes/FormPage.demo.tsx — docs: https://ui.foson.co/?path=/docs/recipes-form-page--docs ### tenant-admin-shell — Tenant Admin Shell The app frame: localized AdminLayout with permission-aware navigation, the signed-in user, and a tenant switcher. - Use when: Starting a multi-tenant admin app — this is the outermost shell every page renders inside. - Not when: A single-tenant app with no tenant scope — use `AdminLayout` without `TenantSwitcher`. - Composes: AdminLayout, Card, CardContent, CardHeader, DescriptionItem, DescriptionList, LocaleProvider, SegmentedControl, TenantSwitcher, toast, zhTW - Source (ships in the package, copy it): src/stories/recipes/TenantAdminShell.demo.tsx — docs: https://ui.foson.co/?path=/docs/recipes-tenant-admin-shell--docs ## Export inventory Components: ActionButtons, AdminLayout, Alert, Avatar, AvatarGroup, Badge, Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbItemComponent, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbRoot, BreadcrumbSeparator, BulkActionBar, Button, Calendar, Card, CardContent, CardField, CardFooter, CardGrid, CardHeader, CardSurfaceContext, Checkbox, CheckboxCard, CircularProgress, CollapsibleSection, ColorPicker, Combobox, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, ConfirmDialog, Container, CopyButton, CopyInput, DataTable, DatePicker, DateRangePicker, DeleteConfirmDialog, DescriptionItem, DescriptionList, Divider, Drawer, DrawerBody, DrawerFooter, DrawerHeader, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, ErrorBoundary, ErrorState, FileTrigger, FileUpload, FileUploadButton, FilterBar, FilterBarActions, FilterBarFilters, FilterBarGroup, FilterBarSearch, FilterSelect, FilterToolbarContext, Form, FormActions, FormControl, FormDescription, FormField, FormLabel, FormMessage, FormSection, HStack, ImageUpload, ImperativeToastContainer, InfiniteScroll, InlineCode, Input, Kbd, Label, LoadingOverlay, LocaleProvider, Modal, ModalBody, ModalFooter, ModalHeader, Navbar, NavbarBrand, NavbarContent, NavbarDivider, NavbarItem, NavbarLink, NavbarSpacer, NoData, NoResults, NumberInput, PageHeader, Pagination, PasswordInput, PinInput, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, Radio, RadioGroup, ScrollFade, SearchInput, SegmentedControl, Select, SheetSelect, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarHeader, SidebarItem, SidebarToggle, SimpleTooltip, Skeleton, SkeletonAvatar, SkeletonCard, SkeletonTable, SkeletonText, Sparkline, Spinner, Stack, Stat, StatCard, StatGroup, StatusDot, Steps, Switch, Table, TableBody, TableCaption, TableCell, TableEmpty, TableFooter, TableHead, TableHeader, TableLoading, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, TagInput, TenantSwitcher, Textarea, ThemeFileError, Timeline, Toast, ToastContainer, Toggle, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, Tree, VStack, ViewToggle, VirtualList Hooks: useCommandPalette, useFormContext, useFormFieldContext, useLocale, useServerTable, useSidebar, useToast, useToastStore Utilities: applyDarkTheme, applyTheme, chartColorList, chartColors, cn, createTheme, defaultTheme, enUS, forestTheme, neutralTheme, oceanTheme, parseThemeFile, themeFileToCSS, themePresets, themeToCSS, toast, zhTW Full typed signatures for every export: llms-full.txt (or api-report.txt in the repo). --- # DESIGN.md — Arkite UI Design System > Machine-readable design spec for `@arkite-ui/core`. Feed this file to AI coding agents (and humans) building admin UIs with this library, so generated pages follow one consistent visual language instead of inventing their own. > > Source of truth: `src/styles/index.css` (tokens), `@arkite-ui/core/tokens` (JS values), Storybook (live docs). This file summarizes the rules; when in doubt, the code wins. ## Identity **Arkite UI** is a component library for **multi-tenant SaaS admin panels**: data-dense, utilitarian, calm. React 18/19 + Tailwind CSS v4 + Radix UI. Light and dark mode are equal citizens — dark mode is automatic via tokens, never hand-tuned per page. Design philosophy: - **Clarity over decoration** — restrained color, generous whitespace inside a dense information layout - **Semantic, not literal** — every color/radius/shadow flows through named tokens; nothing is hardcoded - **Composable** — compound components (Card + CardHeader…), slots and render-props over configuration flags ## Hard rules (for AI agents) 1. **Import UI from `@arkite-ui/core`** (styles: `@arkite-ui/core/styles.css`). Never rebuild primitives that exist — check the component inventory below first. 2. **Never use raw Tailwind palette classes** (`bg-red-50`, `text-blue-800`, `text-gray-500`…). Use semantic utilities only: `bg-destructive-soft`, `text-muted-foreground`, `border-border`. 3. **Never write `dark:` color overrides.** Tokens already resolve dark mode. `dark:` is acceptable only for non-color adjustments in rare cases. 4. **Never use arbitrary values** (`text-[13px]`, `shadow-[…]`, `rounded-[…]`). Use the token scale; if a size seems missing, use the nearest step. 5. **Domain logic stays out of shared components.** API calls, routes, auth, and business models belong to the app layer; UI components receive data via props and emit events via callbacks. ## Color system Colors are HSL triplets in CSS variables (`:root` light, `.dark` dark), exposed as Tailwind utilities via `@theme`. Always pair a background token with its `-foreground`. ### Core | Token | Utility example | Use | | ------------------------------------ | ------------------------------------ | ------------------------------ | | `background` / `foreground` | `bg-background text-foreground` | Page base | | `card` / `card-foreground` | `bg-card` | Elevated surfaces | | `muted` / `muted-foreground` | `text-muted-foreground` | Secondary text, subtle fills | | `primary` / `primary-foreground` | `bg-primary text-primary-foreground` | Brand actions (violet) | | `primary-hover`, `destructive-hover` | `hover:bg-primary-hover` | Solid-fill hover (darkened) | | `secondary` / `secondary-foreground` | `bg-secondary` | Low-emphasis actions | | `accent` / `accent-foreground` | `bg-accent` | Highlights (teal) | | `border`, `input`, `ring` | `border-border`, `ring-ring` | Hairlines, form borders, focus | ### Status (solid + soft layer) Four statuses: `success` (green), `warning` (amber), `destructive` (red), `info` (blue). Each has: - **Solid**: `bg-{status} text-{status}-foreground` — buttons, badges, dots - **Soft layer**: `bg-{status}-soft text-{status}-soft-foreground border-{status}-border` — alerts, toasts, tinted panels Naming rule: the "dangerous/negative" semantic is always **`destructive`** (never `error`) in variant props and tokens. ### Contrast (WCAG AA) - Every built-in fg/bg pair — tokens, all four presets, light and dark, solid and soft — meets **WCAG AA (4.5:1)**, enforced by regression tests (`tokens.test.ts`, `presets.test.ts`). This is why rule 2 exists: paired tokens are guaranteed readable; hand-picked palette classes are not. - Foreground follows **background luminance, not semantics**: light backgrounds get dark text, dark backgrounds get light text (e.g. dark-mode `info` is black-on-blue, not white). Never re-pair a `-foreground` onto a different background. - `createTheme()` picks black/white foregrounds by real contrast ratio — any brand color yields AA-passing pairs automatically. - Custom palettes (raw CSS variable overrides): validate with the Storybook **Foundation / Design Tokens → Contrast Audit** page. - **Solid-fill hover darkens, never fades.** Use `hover:bg-primary-hover` / `hover:bg-destructive-hover`, not `hover:bg-primary/90`: on a white page the opacity form lightens the fill and drops white text below AA (default primary 5.11 → 4.31:1, #27). The `-hover` tokens are `color-mix(in oklab, 88%, black)`, so they follow any `--primary` / `--destructive` override automatically and only raise text contrast; the hover pair is part of the `presets.test.ts` guard. Tune the shade by overriding `--color-primary-hover` / `--color-destructive-hover`. ### Charts & presence `chart-1`…`chart-5` for data series (use in order). `status-online/offline/busy/away` for presence dots (use `StatusDot`). ## Typography - Sans: Inter (system fallback). Mono: JetBrains Mono — code, IDs, `InlineCode`, `Kbd`. - Scale: `text-2xs` (10px, micro labels) → `text-xs` (12px, captions/badges) → `text-sm` (14px, **default body in admin UIs**) → `text-base` (16px) → `text-xl`/`text-2xl`/`text-3xl` (headings via `PageHeader size`). - Numbers in tables/counters: `tabular-nums` (Badge `count` variant has it built in). ## Spacing, radius, elevation - **Spacing**: 4px grid (Tailwind scale). Cards: compound children pad `p-4`; the root's `padding` prop defaults to `none` (`padding="lg"` = `p-6`). `density="compact"` for dashboard widgets. Forms stack on `gap-4`; sections on `gap-6`. - **Radius**: everything derives from one `--radius` (0.5rem): `rounded-lg` = var, `md` = −2px, `sm` = −4px. Pills/avatars use `rounded-full`. Changing `--radius` rethemes the whole library. - **Elevation**: soft low-alpha shadow scale `shadow-xs` → `shadow-2xl`. Cards sit at `xs/sm`; popovers `md/lg`; modals `xl`. Special: `shadow-sticky-left`, `shadow-sticky-right`, `shadow-sticky-header` for sticky table edges. - **Motion**: durations `fast` 100ms / `normal` 150ms / `slow` 300ms; easing tokens incl. `bounce`. Optional framer-motion components live in `@arkite-ui/core/motion`. ## Component inventory — what to reach for | Need | Use | Not | | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | | Page title + actions | `PageHeader` (`size`, `badge`, `onBack`) | Hand-rolled flex headers | | Section on a page | `Card` (+`CardHeader actions`, `density="compact"` for widgets) | Bare bordered divs | | **Any tabular data — including plain read-only lists** | `Table` family (`Table`/`TableHeader`/`TableRow`/`TableHead`/`TableBody`/`TableCell` + `TableEmpty`/`TableLoading` auto-colSpan rows) — styled `` with tokens, dark mode, `compact` density, `variant="striped"`, `hoverable`, cell `align`/`numeric` (tabular figures), `stickyHeader`, `stickyLead`/`stickyAction` frozen columns built in | Raw `
` (hardcodes palette + dark mode by hand); hand-written `colSpan` empty rows; `text-right` repeated per cell | | Data list w/ sorting/filters/selection/pagination | `DataTable` (column-config driven; `compact` density, `rowClassName` for conditional rows, `Column.pinned: 'left'⎮'right'` frozen columns; `minWidth` for wide tables; server-side via `totalRows` + `useServerTable`) | Rebuilding sort/pagination around a raw table | | A row of pills/tabs/chips that outgrows narrow viewports | `ScrollFade` (edge fades driven by real scroll state, shown only where content is hidden) | Bare `overflow-x-auto` with no affordance; hand-rolled gradient CSS | | Huge lists (1000+ rows) | `VirtualList` | Rendering everything | | KPI numbers | `Stat` / `StatCard` / `StatGroup` + `Sparkline` | Custom stat blocks | | Filters above a table | `FilterBar` (+`FilterBarSearch/Filters/Actions`, `FilterSelect`) | Ad-hoc toolbars | | A **labelled** filter cluster ("Period: 1D 7D 30D") | `FilterBarGroup label="Period"` wrapping a `SegmentedControl` (2–5 mutually exclusive presets). `FilterSelect`'s `label` only prefixes its "all" option, so it cannot show a visible group label | A hand-rolled label `` + flex row (ships without `flex-wrap`, so one group's width pushes the page sideways on mobile); hand-styled active/inactive pills with raw palette classes | | Date range **inside a filter bar** | `DateRangePicker labelPlacement="inside"` — a stacked label adds a line above the inputs, so the default `"top"` leaves them sitting ~10px below the single-line controls next to them | Leaving `labelPlacement` at `"top"` in a toolbar and nudging it back into line with margins | | Forms | `Form` family (`FormField label errorMessage`) + `Input`/`Select`/`Textarea`/`NumberInput`/`DatePicker`/`Combobox`/`TagInput`/`ColorPicker`/`FileUpload`/`ImageUpload` | Uncontrolled raw inputs | | OTP / verification code | `PinInput` (`length`, `type`, `onComplete`; SMS autofill built in) | Hand-styled single inputs with tracking CSS | | File pick from a custom trigger (thumbnail, icon, menu item) | `FileTrigger` (headless — makes any element open the picker) | Hand-rolled hidden `` | | Action styled as a text link (e.g. in a table cell) | `Button variant="link"` | `eslint-disable` + raw `