name: om-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. Pair this skill with .ai/guides/ui.md when present and with the standalone AGENTS.md rules for DataTable hosts, design-system primitives, and backend page conventions.
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+Enterfor primary actions,Escapeto cancel. - Clear Hierarchy: Page → Section → Content. Use
PageHeader,PageBody, consistent spacing. - Design System Discipline: Use semantic status tokens plus shared primitives like
StatusBadge,Alert,FormField,SectionHeader,CollapsibleSection, andEmptyState. No hardcoded status colors or arbitrary text sizes.
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:
TruncatedCellwithmeta.maxWidth - Boolean:
BooleanIcon - Status/enum:
EnumBadgewith severity presets - Actions:
RowActionsfor context menus
Preferred DataTable Host Pattern
For standard CRUD lists, prefer the built-in host pattern instead of manually fetching and shaping rows:
<DataTable
entityId="tickets.ticket"
apiPath="tickets/tickets"
extensionTableId="tickets.ticket"
columns={columns}
createHref="/backend/tickets/tickets/new"
emptyState={{
title: t('tickets.list.empty.title'),
description: t('tickets.list.empty.description'),
}}
/>
Keep extensionTableId stable so DataTable injections remain backward-compatible.
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 pagetotalCount— 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 pagesFormHeader mode="detail"— large header for view/detail pages with entity type label, title, status badge, and Actions dropdownFormFooter— footer wrappingFormActionButtonsActionsDropdown— 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'
// Dialog forms MUST use embedded={true}
<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
- Forms use
CrudForm(not custom) - Tables use
DataTable(not custom) - Notifications use
flash()(not alert/toast) - Dialog forms have
embedded={true} - Keyboard:
Cmd/Ctrl+Enter(submit),Escape(cancel) - Loading states use
LoadingMessageorDataLoader - Error states use
ErrorMessage,ErrorNotice, orNotice variant="error" - Empty states use
EmptyState - Status displays use
StatusBadgeorEnumBadge, not hardcoded colors - Standalone inputs use
FormField; detail sections useSectionHeader/CollapsibleSectionwhen applicable - Column truncation uses
meta.truncateandmeta.maxWidth - Boolean values use
BooleanIcon - Status/enum values use
EnumBadge - Row actions use
RowActionswith stableidvalues - API calls use
apiCall/apiCallOrThrow(not rawfetch)
Anti-Patterns
- Custom form implementations — use
CrudForm - Manual table markup — use
DataTable - Custom toast/notification — use
flash() - Inline styles — use Tailwind classes
- Hardcoded colors or status classes — use theme variables and semantic status tokens
- Missing loading states — every async operation needs feedback
- Missing error handling — every failure needs messaging
- Missing keyboard shortcuts — all dialogs need
Cmd+EnterandEscape - Custom truncation — use
TruncatedCellwithmeta.maxWidth - Direct
fetch()— useapiCall/apiCallOrThrow
Visual Guidelines
Spacing
p-4for cards,p-6for page sectionsgap-4orgap-6for flex/grid layoutsspace-y-4orspace-y-6for 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
Page Navigation Metadata
Every backend page needs correct page.meta.ts for sidebar placement.
See .ai/skills/om-module-scaffold/references/navigation-patterns.md for:
- Complete field reference (
pageGroup,pageOrder,pageContext,navHidden) - Settings page pattern (
pageContext: 'settings' as const+navHidden: true) - Common anti-patterns (missing group, mismatched keys, broken icons)