UI Metadata Authoring Guide
How to author Page Layouts, List Views, Dashboards, and Form Views using @objectstack/spec/ui schemas
UI Metadata Authoring Guide
HotCRM defines all UI layouts as TypeScript metadata validated against @objectstack/spec/ui schemas. This guide covers the four core UI metadata types and how to author them.
Overview
| Metadata Type | File Suffix | Schema | Purpose |
|---|---|---|---|
| Page Layout | *.page.ts | PageSchema | Record detail pages with sections, tabs, related lists |
| List View | *.view.ts | ViewSchema | Record list tables with columns, filters, sorting |
| Dashboard | *.dashboard.ts | DashboardSchema | Metric dashboards with widget grid layouts |
| Form View | *.form.ts | FormViewSchema | Data entry forms with sections and field layout |
All UI metadata files follow the same validation pattern:
import type { Page } from '@objectstack/spec/ui';
import { PageSchema } from '@objectstack/spec/ui';
export const MyPage = {
// ... page definition
} satisfies Page;
PageSchema.parse(MyPage); // Runtime validation
export default MyPage;Page Layout Authoring
Page layouts define record detail pages — the view shown when opening a single record.
Structure
A page layout consists of regions, each containing components:
// packages/crm/src/account.page.ts
import type { Page } from '@objectstack/spec/ui';
import { PageSchema } from '@objectstack/spec/ui';
export const AccountPage = {
name: 'account_detail',
object: 'account',
type: 'record' as const,
label: 'Account Detail Page',
regions: [
{
name: 'header',
components: [
{
type: 'record:highlights' as const,
properties: {
fields: ['name', 'type', 'industry', 'annual_revenue', 'phone']
}
}
]
},
{
name: 'tabs',
components: [
{
type: 'page:tabs' as const,
properties: {
tabs: ['account_info', 'address_info', 'additional_info']
}
}
]
},
{
name: 'account_info',
components: [
{
type: 'record:details' as const,
label: 'Account Information',
properties: {
fields: [
'name', 'account_number', 'type', 'industry',
'phone', 'website', 'annual_revenue', 'number_of_employees'
],
columns: 2
}
}
]
},
{
name: 'related_lists',
components: [
{
type: 'record:related_list' as const,
label: 'Opportunities',
properties: {
object: 'opportunity',
columns: ['name', 'stage', 'amount', 'close_date', 'probability'],
filters: [['stage', '!=', 'closed_lost']],
sort: [{ field: 'close_date', direction: 'asc' }],
actions: ['new', 'edit', 'delete']
}
}
]
}
]
} satisfies Page;
PageSchema.parse(AccountPage);
export default AccountPage;Available Component Types
| Component Type | Purpose |
|---|---|
record:highlights | Key fields displayed prominently in the page header |
record:details | Field section with configurable column layout |
page:tabs | Tab navigation between page sections |
record:related_list | Child/related object table with filters and actions |
Region Naming Convention
header— Page top, typically containsrecord:highlightstabs— Tab navigation region- Named tab regions (e.g.,
account_info,address_info) — Tab content related_lists— Bottom section with related object tables
List View Authoring
List views define record list tables — the grid shown when browsing a collection of records.
Structure
A view defines columns, optional filters, sorting, pagination, and bulk actions:
// packages/crm/src/account.view.ts
import type { View } from '@objectstack/spec/ui';
import { ViewSchema } from '@objectstack/spec/ui';
export const AllAccountsView = {
list: {
name: 'all_accounts',
label: 'All Accounts',
columns: [
{ field: 'name', width: 250, sortable: true, link: true },
{ field: 'type', width: 120, sortable: true },
{ field: 'industry', width: 150, sortable: true },
{ field: 'annual_revenue', width: 150, sortable: true, align: 'right' as const },
{ field: 'phone', width: 150 },
{ field: 'owner', width: 150 },
{ field: 'created_date', width: 150, sortable: true }
],
sort: [{ field: 'name', order: 'asc' as const }],
bulkActions: ['delete', 'update_owner', 'export'],
inlineEdit: true,
pagination: { pageSize: 25, pageSizeOptions: [10, 25, 50, 100] }
}
} satisfies View;
ViewSchema.parse(AllAccountsView);Column Properties
| Property | Type | Description |
|---|---|---|
field | string | Field API name from the object schema |
width | number | Column width in pixels |
sortable | boolean | Whether the column can be sorted |
link | boolean | Whether to render the value as a link to the record |
align | 'left' | 'right' | 'center' | Text alignment |
Filtering
Filters use an array-based syntax compatible with ObjectQL:
// Single filter
filter: [['type', '=', 'customer']]
// Multiple filters (AND)
filter: [
['annual_revenue', '>', 10000000],
['type', '=', 'customer']
]
// Dynamic filter (current user)
filter: [['owner', '=', '${currentUser.id}']]
// Date-based filter
filter: [['created_date', '>=', 'LAST_30_DAYS']]
// IN operator
filter: [['rating', 'IN', ['Hot', 'Warm']]]Conditional Formatting
Apply visual styles based on record data:
export const HotAccountsView = {
list: {
name: 'hot_accounts',
label: 'Hot Accounts',
filter: [['rating', '=', 'Hot']],
columns: [/* ... */],
conditionalFormatting: [
{
condition: 'rating == "Hot"',
style: { backgroundColor: '#FEF2F2', borderLeft: '3px solid #EF4444' }
}
]
}
} satisfies View;Multiple Views per Object
Export multiple named views from a single *.view.ts file:
export const AccountListViews = {
all: AllAccountsView,
my: MyAccountsView,
enterprise: EnterpriseAccountsView,
recent: RecentlyCreatedView,
hot: HotAccountsView,
needAttention: NeedAttentionView
};Dashboard Authoring
Dashboards define metric overview pages with a grid-based widget layout.
Structure
A dashboard contains an array of widgets, each with a type, data source, and grid position:
// packages/crm/src/crm.dashboard.ts
import type { Dashboard } from '@objectstack/spec/ui';
import { DashboardSchema } from '@objectstack/spec/ui';
export const CrmDashboard = {
name: 'crm_dashboard',
label: 'Sales Dashboard',
description: 'Key sales metrics and pipeline overview',
widgets: [
{
title: 'Total Pipeline Value',
type: 'metric' as const,
object: 'opportunity',
valueField: 'amount',
aggregate: 'sum' as const,
filter: ['stage', '!=', 'closed_lost'],
layout: { x: 0, y: 0, w: 3, h: 2 }
},
{
title: 'Pipeline by Stage',
type: 'funnel' as const,
object: 'opportunity',
categoryField: 'stage',
valueField: 'amount',
aggregate: 'sum' as const,
layout: { x: 0, y: 2, w: 6, h: 4 }
},
{
title: 'Win Rate by Month',
type: 'line' as const,
object: 'opportunity',
categoryField: 'close_date',
valueField: 'probability',
aggregate: 'avg' as const,
layout: { x: 6, y: 2, w: 6, h: 4 }
},
{
title: 'Deals Closing This Month',
type: 'table' as const,
object: 'opportunity',
filter: ['stage', '!=', 'closed_lost'],
layout: { x: 6, y: 6, w: 6, h: 4 }
}
]
} satisfies Dashboard;
DashboardSchema.parse(CrmDashboard);
export default CrmDashboard;Widget Types
| Type | Purpose | Required Properties |
|---|---|---|
metric | Single numeric value (e.g., total revenue) | object, aggregate, optionally valueField |
kpi | Key performance indicator with target/trend | object, valueField, aggregate |
funnel | Funnel chart for stage progression | object, categoryField, valueField, aggregate |
line | Line chart for trends over time | object, categoryField, valueField, aggregate |
bar | Bar chart for comparisons | object, categoryField, valueField, aggregate |
pie | Pie chart for distribution | object, categoryField, valueField, aggregate |
table | Data table with records | object |
Grid Layout
The dashboard uses a 12-column grid system:
| Property | Description |
|---|---|
x | Column start position (0–11) |
y | Row start position |
w | Width in columns (1–12) |
h | Height in rows |
Aggregation Functions
| Aggregate | Description |
|---|---|
sum | Total of all values |
count | Number of records |
avg | Average value |
min | Minimum value |
max | Maximum value |
Form View Authoring
Form views define data entry layouts for creating and editing records.
Structure
A form contains sections, each with a column layout and field list:
// packages/crm/src/contact.form.ts
import type { FormView } from '@objectstack/spec/ui';
import { FormViewSchema } from '@objectstack/spec/ui';
export const ContactForm = {
type: 'simple' as const,
data: {
provider: 'object' as const,
object: 'contact'
},
sections: [
{
label: 'Contact Information',
columns: '2' as const,
fields: [
{ field: 'first_name', required: true },
{ field: 'last_name', required: true },
{ field: 'salutation' },
{ field: 'account_id', label: 'Account' },
{ field: 'title' },
{ field: 'department' },
{ field: 'email', required: true },
{ field: 'phone' }
]
},
{
label: 'Communication Preferences',
columns: '2' as const,
fields: [
{ field: 'mobile_phone' },
{ field: 'fax' },
{ field: 'preferred_contact' },
{ field: 'last_contact_date' }
]
},
{
label: 'Role Details',
columns: '2' as const,
fields: [
{ field: 'level' },
{ field: 'is_decision_maker' },
{ field: 'influence_level' },
{ field: 'relationship_strength' },
{ field: 'notes', colSpan: 2 }
]
}
]
} satisfies FormView;
FormViewSchema.parse(ContactForm);
export default ContactForm;Section Properties
| Property | Type | Description |
|---|---|---|
label | string | Section heading |
columns | '1' | '2' | Number of columns in the section |
fields | FormField[] | Array of field definitions |
Field Properties
| Property | Type | Description |
|---|---|---|
field | string | Field API name from the object schema |
label | string | Override display label |
required | boolean | Mark as required (supplements object-level required) |
colSpan | number | Columns to span (use 2 for full-width in a 2-column layout) |
Schema Validation
All UI metadata must be validated at export time. This ensures type safety and catches errors before deployment.
// Import the type (for satisfies) and the schema (for parse)
import type { Page } from '@objectstack/spec/ui';
import { PageSchema } from '@objectstack/spec/ui';
// Use satisfies for compile-time checking
export const MyPage = { /* ... */ } satisfies Page;
// Use parse() for runtime validation
PageSchema.parse(MyPage);Schema Summary
| Schema | Import | Validates |
|---|---|---|
PageSchema | @objectstack/spec/ui | Page layouts with regions and components |
ViewSchema | @objectstack/spec/ui | List views with columns, filters, pagination |
DashboardSchema | @objectstack/spec/ui | Dashboard widgets with grid layout |
FormViewSchema | @objectstack/spec/ui | Form sections with field layout |
Common Patterns
Use as const for string literal types to satisfy the schema:
type: 'record' as const, // Page type
align: 'right' as const, // Column alignment
order: 'asc' as const, // Sort order
type: 'metric' as const, // Widget type
columns: '2' as const, // Form columns
provider: 'object' as const, // Form data provider