This skill guides creation of consistent, production-grade backend/backoffice interfaces using the established @open-mercato/ui component library. All implementations must leverage existing components to maintain visual and behavioral consistency across modules.
For complete component documentation, see references/ui-components.md.
Design Principles
Backend UI prioritizes usability, consistency, and productivity over creative expression:
- Consistency First: Every page should feel like part of the same application. Use established patterns.
- Component Reuse: Never create custom implementations when a shared component exists.
- Data Density: Admin users need information-rich interfaces. Optimize for scanning and quick actions.
- Keyboard Navigation: Support Cmd/Ctrl+Enter for primary actions, Escape to cancel, and standard shortcuts.
- Clear Hierarchy: Page → Section → Content. Use PageHeader, PageBody, and consistent spacing.
Required Component Library
ALWAYS import from @open-mercato/ui. Reference the component documentation at .ai/specs/SPEC-001-2026-01-21-ui-reusable-components.md.
Core Layout Pattern
import { Page, PageHeader, PageBody } from '@open-mercato/ui/backend/Page'
import { AppShell } from '@open-mercato/ui/backend/AppShell'
// Every backend page follows this structure
<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 configuration patterns:
- Text columns: Use
TruncatedCellwithmeta.maxWidthfor long content - Boolean columns: Use
BooleanIcon - Status/enum columns: Use
EnumBadgewith severity presets - Actions column: Use
RowActionsfor context menus
Forms
Use CrudForm for ALL forms. Never build forms from scratch.
import { CrudForm, type CrudField, type CrudFormGroup } from '@open-mercato/ui/backend/CrudForm'
import { JsonBuilder } from '@open-mercato/ui/backend/JsonBuilder'
Form field types available:
text,textarea,number,email,passwordselect,multiselect,comboboxcheckbox,switchdate,datetimecustom(for JsonBuilder, TagsInput, etc.)
Form Headers & Footers
Use FormHeader and FormFooter for all page headers/footers. Never build inline header layouts manually.
import { FormHeader, FormFooter, FormActionButtons, ActionsDropdown } from '@open-mercato/ui/backend/forms'
FormHeader mode="edit"-- compact header for CrudForm pages (used automatically by CrudForm internally)FormHeader mode="detail"-- large header for view/detail pages with entity type label, title, status badge, and Actions dropdownFormFooter-- footer wrappingFormActionButtonswith embedded/dialog awarenessFormActionButtons-- atomic button bar:[extraActions] [Delete] [Cancel] [Save]ActionsDropdown-- groups additional context actions (Convert, Send, Print) into a dropdown. Only visible when items are provided. Delete is never inside the dropdown.
Detail mode example:
<FormHeader
mode="detail"
backHref="/backend/sales/quotes"
entityTypeLabel="Sales quote"
title={<InlineTextEditor value={number} />}
statusBadge={<Badge variant="secondary">Sent</Badge>}
menuActions={[
{ id: 'convert', label: 'Convert to order', icon: ArrowRightLeft, onSelect: handleConvert },
{ id: 'send', label: 'Send to customer', icon: Send, onSelect: handleSend },
]}
/>
See SPEC-016 for full API.
Dialogs
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@open-mercato/ui/primitives/dialog'
import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
// Dialog forms MUST use embedded={true}
<Dialog open={isOpen}
<DialogContent className="sm:max-w-2xl [&_.grid]:!grid-cols-1">
<DialogHeader>
<DialogTitle>Edit Item</DialogTitle>
</DialogHeader>
<CrudForm
fields={fields}
groups={groups}
initialValues={initialValues}
embedded={true}
submitLabel="Save"
/>
</DialogContent>
</Dialog>
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'
// Success
flash('Record saved successfully', 'success')
// Error
flash('Failed to save record', 'error')
// Warning/Info
flash('This action cannot be undone', 'warning')
flash('Processing in background', 'info')
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'
// Notice variants: 'error' | 'info' | 'warning'
// Use compact mode for inline hints:
<Notice compact>{t('audit_logs.hint.view_self_only')}</Notice>
// Use full mode for prominent messages:
<Notice variant="warning" title="Warning" message="This action cannot be undone." />
// ErrorNotice is a convenience wrapper:
<ErrorNotice title="Something went wrong" message="Unable to load data." />
Primitives (use sparingly, prefer backend components)
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 { Alert, AlertTitle, AlertDescription } from '@open-mercato/ui/primitives/alert'
import { Separator } from '@open-mercato/ui/primitives/separator'
import { Switch } from '@open-mercato/ui/primitives/switch'
import { SimpleTooltip } from '@open-mercato/ui/primitives/tooltip'
Implementation Checklist
Before writing any backend UI code, verify:
- Using
CrudFormfor forms (not custom form implementations) - Using
DataTablefor lists (not custom tables) - Using
flash()for notifications (not alert/toast) - Dialog forms have
embedded={true} - Keyboard shortcuts: Cmd/Ctrl+Enter (submit), Escape (cancel)
- Loading states use
LoadingMessageorDataLoader - Error states use
ErrorMessage,ErrorNotice, or<Notice variant="error"> - Info/warning hints use
<Notice compact>or<Notice variant="warning"> - Empty states use
EmptyState - Column truncation configured with
meta.truncateandmeta.maxWidth - Boolean values use
BooleanIcon - Status/enum values use
EnumBadge - Row actions use
RowActionscomponent
Visual Guidelines
Spacing
- Use consistent padding:
p-4for cards,p-6for page sections - Use
gap-4orgap-6for flex/grid layouts - Maintain vertical rhythm with
space-y-4orspace-y-6
Colors
- Use semantic colors from the theme (don't hardcode hex values)
- Destructive actions:
variant="destructive"on buttons - Status badges: Use
useSeverityPreset()for consistent coloring
Typography
- Page titles: Handled by
PageHeader - Section titles:
text-lg font-semibold - Labels: Handled by form components
- Body text: Default sizing, avoid custom font sizes
Layout Patterns
- List pages: FilterBar + DataTable + Pagination
- Detail pages: Header + Tabs or Sections + Related data
- Create/Edit: Full-page CrudForm or Dialog with embedded CrudForm
- Settings: Grouped sections with inline editing
Anti-Patterns to Avoid
- Custom form implementations - Always use CrudForm
- Manual table markup - Always use DataTable
- Custom toast/notification - Always 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 user-friendly messaging
- Missing keyboard shortcuts - All dialogs need Cmd+Enter and Escape
- Custom truncation logic - Use TruncatedCell with meta.maxWidth
- Direct fetch() calls - Use apiCall/apiCallOrThrow from utils
API Integration Pattern
import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
import { createCrud, updateCrud, deleteCrud } from '@open-mercato/ui/backend/utils/crud'
import { mapCrudServerErrorToFormErrors, createCrudFormError } from '@open-mercato/ui/backend/utils/serverErrors'
// For CRUD operations
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
}
// For custom endpoints
const result = await apiCall<ResponseType>('/api/custom-endpoint', {
method: 'POST',
body: JSON.stringify(data)
})
Custom Fields Integration
When building CRUD interfaces that support custom fields:
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'
When to Create New Components
Only create new components when:
- No existing component serves the use case
- The pattern will be reused across 3+ modules
- Approved for addition to
@open-mercato/ui
If creating something new, it should eventually be added to the shared library, not kept in a single module.
Converted and distributed by TomeVault — claim your Tome and manage your conversions.