Design and implement consistent, production-grade backend/backoffice interfaces using the @open-mercato/ui component library...
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. Pair this skill with packages/ui/AGENTS.md and packages/ui/src/backend/AGENTS.md for the current design-system and backend-host rules.
Backend UI prioritizes usability, consistency, and productivity over creative expression:
StatusBadge, Alert, FormField, SectionHeader, CollapsibleSection, EmptyState). No hardcoded status colors or arbitrary text sizes.ALWAYS import from @open-mercato/ui. Reference the component documentation at .ai/specs/SPEC-001-2026-01-21-ui-reusable-components.md.
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>
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:
TruncatedCell with meta.maxWidth for long contentBooleanIconEnumBadge with severity presetsRowActions for context menusUse 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.)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} onOpenChange={onClose}>
<DialogContent className="sm:max-w-2xl [&_.grid]:!grid-cols-1">
<DialogHeader>
<DialogTitle>Edit Item</DialogTitle>
</DialogHeader>
<CrudForm
fields={fields}
groups={groups}
initialValues={initialValues}
onSubmit={handleSubmit}
embedded={true}
submitLabel="Save"
/>
</DialogContent>
</Dialog>
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'
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.
import { Spinner } from '@open-mercato/ui/primitives/spinner'
import { DataLoader } from '@open-mercato/ui/primitives/DataLoader'
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'
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'
Before writing any backend UI code, verify:
CrudForm for forms (not custom form implementations)DataTable for lists (not custom tables)flash() for notifications (not alert/toast)embedded={true}LoadingMessage or DataLoaderErrorMessage or ErrorNoticeEmptyStatemeta.truncate and meta.maxWidthBooleanIconEnumBadgeRowActions componentp-4 for cards, p-6 for page sectionsgap-4 or gap-6 for flex/grid layoutsspace-y-4 or space-y-6variant="destructive" on buttonsuseSeverityPreset() for consistent coloringPageHeadertext-lg font-semiboldimport { 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)
})
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'
Only create new components when:
@open-mercato/uiIf creating something new, it should eventually be added to the shared library, not kept in a single module.