| name | backend-ui-design |
| description | Design and implement consistent backend/backoffice interfaces using @open-mercato/ui. Use when building admin pages, CRUD interfaces, data tables, forms, detail pages, or any backoffice UI. |
Backend UI Design
Guide for creating consistent, production-grade backend interfaces using the @open-mercato/ui component library. All implementations must use existing components for visual and behavioral consistency.
For complete component API reference, see references/ui-components.md.
Design Principles
- Consistency First: Every page should feel like part of the same application.
- Component Reuse: Never create custom implementations when a shared component exists.
- Data Density: Admin users need information-rich interfaces. Optimize for scanning.
- Keyboard Navigation:
Cmd/Ctrl+Enter for primary actions, Escape to cancel.
- Clear Hierarchy: Page → Section → Content. Use
PageHeader, PageBody, consistent spacing.
Required Component Library
ALWAYS import from @open-mercato/ui.
Core Layout
import { Page, PageHeader, PageBody } from '@open-mercato/ui/backend/Page'
<Page>
<PageHeader>{/* Title, actions, breadcrumbs */}</PageHeader>
<PageBody>{/* Main content */}</PageBody>
</Page>
Data Display (Lists)
Use DataTable for ALL tabular data. Never implement custom tables.
import { DataTable } from '@open-mercato/ui/backend/DataTable'
import type { FilterDef } from '@open-mercato/ui/backend/FilterBar'
import { RowActions } from '@open-mercato/ui/backend/RowActions'
import { TruncatedCell } from '@open-mercato/ui/backend/TruncatedCell'
import { BooleanIcon, EnumBadge } from '@open-mercato/ui/backend/ValueIcons'
Column patterns:
- Text:
TruncatedCell with meta.maxWidth
- Boolean:
BooleanIcon
- Status/enum:
EnumBadge with severity presets
- Actions:
RowActions for context menus
DataTable Pagination
DataTable MUST be configured with pagination props to display all data correctly. Without these, the table only shows the first page with no way to navigate:
<DataTable
columns={columns}
data={items}
page={page}
pageSize={pageSize}
totalCount={totalCount}
onPageChange={setPage}
/>
When using a custom API (not makeCrudRoute), ensure the list response always returns:
items — array of records for the current page
totalCount — total records matching the query (not just the current page)
page — current page number (1-based)
pageSize — records per page
The default pageSize is 25. Keep at or below 100. If you see fewer records than expected, verify your API returns totalCount and the DataTable has pagination props wired.
Forms
Use CrudForm for ALL forms. Never build from scratch.
import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
Field types: text, textarea, number, email, password, select, multiselect, combobox, checkbox, switch, date, datetime, custom.
Form Headers & Footers
import { FormHeader, FormFooter, FormActionButtons, ActionsDropdown } from '@open-mercato/ui/backend/forms'
FormHeader mode="edit" — compact header for CrudForm pages
FormHeader mode="detail" — large header for view/detail pages with entity type label, title, status badge, and Actions dropdown
FormFooter — footer wrapping FormActionButtons
ActionsDropdown — groups additional context actions (Convert, Send, Print). Delete is never inside the dropdown.
Dialogs
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@open-mercato/ui/primitives/dialog'
<CrudForm fields={fields} onSubmit={handleSubmit} embedded={true} submitLabel="Save" />
Detail Pages
import { DetailFieldsSection, LoadingMessage, ErrorMessage, TabEmptyState } from '@open-mercato/ui/backend/detail'
import { NotesSection } from '@open-mercato/ui/backend/detail/NotesSection'
import { TagsSection } from '@open-mercato/ui/backend/detail/TagsSection'
import { CustomDataSection } from '@open-mercato/ui/backend/detail/CustomDataSection'
Notifications
import { flash } from '@open-mercato/ui/backend/FlashMessages'
flash('Record saved successfully', 'success')
flash('Failed to save record', 'error')
flash('This action cannot be undone', 'warning')
NEVER use alert(), console.log(), or custom toast implementations.
Loading & Error States
import { Spinner } from '@open-mercato/ui/primitives/spinner'
import { DataLoader } from '@open-mercato/ui/primitives/DataLoader'
import { Notice } from '@open-mercato/ui/primitives/Notice'
import { ErrorNotice } from '@open-mercato/ui/primitives/ErrorNotice'
import { EmptyState } from '@open-mercato/ui/backend/EmptyState'
import { LoadingMessage, ErrorMessage } from '@open-mercato/ui/backend/detail'
Primitives
import { Button } from '@open-mercato/ui/primitives/button'
import { Input } from '@open-mercato/ui/primitives/input'
import { Label } from '@open-mercato/ui/primitives/label'
import { Badge } from '@open-mercato/ui/primitives/badge'
import { Switch } from '@open-mercato/ui/primitives/switch'
import { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'
API Integration
import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
import { createCrud, updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
import { createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
const handleCreate = async (values: FormValues) => {
const result = await createCrud<ResponseType>('module/resource', values)
if (result.ok) {
flash('Created successfully', 'success')
router.push(`/backend/module/${result.result.id}`)
}
return result
}
Custom Fields Integration
import { useCustomFieldDefinitions } from '@open-mercato/ui/backend/utils/customFieldDefs'
import { buildCustomFieldFormFields } from '@open-mercato/ui/backend/utils/customFieldForms'
import { buildCustomFieldColumns } from '@open-mercato/ui/backend/utils/customFieldColumns'
import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
Implementation Checklist
Anti-Patterns
- Custom form implementations — use
CrudForm
- Manual table markup — use
DataTable
- Custom toast/notification — use
flash()
- Inline styles — use Tailwind classes
- Hardcoded colors — use theme variables
- Missing loading states — every async operation needs feedback
- Missing error handling — every failure needs messaging
- Missing keyboard shortcuts — all dialogs need
Cmd+Enter and Escape
- Custom truncation — use
TruncatedCell with meta.maxWidth
- Direct
fetch() — use apiCall/apiCallOrThrow
Visual Guidelines
Spacing
p-4 for cards, p-6 for page sections
gap-4 or gap-6 for flex/grid layouts
space-y-4 or space-y-6 for vertical rhythm
Colors
- Use semantic colors from theme (no hardcoded hex)
- Destructive:
variant="destructive" on buttons
- Status badges:
useSeverityPreset()
Layout Patterns
- List pages: FilterBar + DataTable + Pagination
- Detail pages: Header + Tabs/Sections + Related data
- Create/Edit: Full-page CrudForm or Dialog with embedded CrudForm
- Settings: Grouped sections with inline editing