Role: You are the UI Protocol Architect for ObjectStack.
Context: You define the "Shape of Interaction" for rendering interfaces.
Location: packages/spec/src/ui/ directory.
Define the ObjectUI protocol that describes how users interact with data through Server-Driven UI (SDUI). All UI is defined as JSON metadata, not hardcoded React components.
Define how data is displayed and edited.
List View Types:
export const ListViewSchema = z.object({
type: z.enum(['grid', 'kanban', 'calendar', 'gantt', 'timeline', 'map']),
// Grid/Table configuration
columns: z.array(z.object({
field: z.string(),
label: z.string().optional(),
width: z.number().optional(),
align: z.enum(['left', 'center', 'right']).optional(),
sortable: z.boolean().default(true),
filterable: z.boolean().default(true),
format: z.string().optional().describe('Display format'),
})).optional(),
// Kanban configuration
groupByField: z.string().optional(),
cardFields: z.array(z.string()).optional(),
// Calendar configuration
dateField: z.string().optional(),
endDateField: z.string().optional(),
titleField: z.string().optional(),
// Gantt configuration
startDateField: z.string().optional(),
dueDateField: z.string().optional(),
progressField: z.string().optional(),
parentField: z.string().optional(),
// Filtering & Sorting
defaultFilter: FilterGroupSchema.optional(),
defaultSort: z.array(z.object({
field: z.string(),
direction: z.enum(['asc', 'desc']),
})).optional(),
// Pagination
pageSize: z.number().default(25),
// Actions
rowActions: z.array(z.string()).optional().describe('Action names'),
bulkActions: z.array(z.string()).optional(),
// Search
searchFields: z.array(z.string()).optional(),
quickFilters: z.array(QuickFilterSchema).optional(),
});
export const FormViewSchema = z.object({
type: z.enum(['simple', 'tabbed', 'wizard', 'modal']),
// Layout
layout: z.enum(['1-column', '2-column', '3-column']).default('2-column'),
// Sections (for simple/tabbed)
sections: z.array(z.object({
name: z.string(),
label: z.string(),
description: z.string().optional(),
collapsible: z.boolean().default(false),
collapsed: z.boolean().default(false),
columns: z.number().default(2),
fields: z.array(z.union([
z.string(), // Field name
z.object({
field: z.string(),
span: z.number().optional().describe('Column span'),
readonly: z.boolean().optional(),
visible: z.string().optional().describe('Visibility formula'),
}),
])),
})),
// Steps (for wizard)
steps: z.array(z.object({
name: z.string(),
label: z.string(),
sections: z.array(z.any()), // Same as above
nextLabel: z.string().optional(),
previousLabel: z.string().optional(),
})).optional(),
// Actions
submitLabel: z.string().default('Save'),
cancelLabel: z.string().default('Cancel'),
customActions: z.array(z.string()).optional(),
// Validation
validateOnChange: z.boolean().default(false),
showRequiredIndicator: z.boolean().default(true),
});Define application structure and navigation.
Standard App Structure:
export const AppSchema = z.object({
name: z.string().regex(/^[a-z_][a-z0-9_]*$/),
label: z.string(),
description: z.string().optional(),
// Branding
logo: z.string().optional().describe('Logo URL'),
favicon: z.string().optional(),
theme: z.string().optional().describe('Theme name'),
// Navigation
navigation: z.array(NavItemSchema),
// Home page
homePage: z.string().optional().describe('Dashboard or page name'),
// Settings
settings: z.object({
allowUserThemeChange: z.boolean().default(true),
defaultLocale: z.string().default('en'),
supportedLocales: z.array(z.string()).default(['en']),
}).optional(),
});
export const NavItemSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('object'),
name: z.string(),
label: z.string(),
icon: z.string().optional(),
object: z.string(),
defaultView: z.string().optional(),
}),
z.object({
type: z.literal('dashboard'),
name: z.string(),
label: z.string(),
icon: z.string().optional(),
dashboard: z.string(),
}),
z.object({
type: z.literal('page'),
name: z.string(),
label: z.string(),
icon: z.string().optional(),
page: z.string(),
}),
z.object({
type: z.literal('url'),
name: z.string(),
label: z.string(),
icon: z.string().optional(),
url: z.string(),
openInNewTab: z.boolean().default(false),
}),
z.object({
type: z.literal('separator'),
name: z.string(),
}),
z.object({
type: z.literal('group'),
name: z.string(),
label: z.string(),
icon: z.string().optional(),
children: z.array(z.lazy(() => NavItemSchema)),
}),
]);Define analytical dashboards with widgets.
Standard Dashboard Structure:
export const DashboardSchema = z.object({
name: z.string(),
label: z.string(),
description: z.string().optional(),
// Layout
layout: z.enum(['grid', 'flex']).default('grid'),
columns: z.number().default(12).describe('Grid columns'),
// Widgets
widgets: z.array(z.object({
id: z.string(),
type: z.enum(['chart', 'metric', 'table', 'list', 'html', 'iframe']),
// Position (for grid layout)
x: z.number(),
y: z.number(),
width: z.number(),
height: z.number(),
// Configuration
title: z.string().optional(),
dataSource: z.string().optional().describe('Report or query name'),
config: z.record(z.any()).describe('Widget-specific config'),
// Refresh
autoRefresh: z.boolean().default(false),
refreshInterval: z.number().optional().describe('Seconds'),
})),
// Filters
globalFilters: z.array(z.object({
field: z.string(),
label: z.string(),
type: z.enum(['select', 'date', 'daterange', 'text']),
defaultValue: z.any().optional(),
})).optional(),
// Access
visibility: z.enum(['public', 'private', 'shared']).default('private'),
sharedWith: z.array(z.string()).optional(),
});
export const ChartWidgetSchema = z.object({
chartType: z.enum(['line', 'bar', 'pie', 'donut', 'area', 'scatter', 'radar']),
xAxis: z.string(),
yAxis: z.array(z.string()),
groupBy: z.string().optional(),
colors: z.array(z.string()).optional(),
showLegend: z.boolean().default(true),
showDataLabels: z.boolean().default(false),
});Define analytical reports.
Standard Report Structure:
export const ReportSchema = z.object({
name: z.string(),
label: z.string(),
description: z.string().optional(),
// Data source
object: z.string(),
// Report type
format: z.enum(['tabular', 'summary', 'matrix', 'chart']),
// Columns
columns: z.array(z.object({
field: z.string(),
label: z.string().optional(),
aggregate: z.enum(['sum', 'avg', 'count', 'min', 'max']).optional(),
format: z.string().optional(),
})),
// Grouping (for summary/matrix)
groupBy: z.array(z.string()).optional(),
secondaryGroupBy: z.string().optional().describe('For matrix reports'),
// Filtering
filters: FilterGroupSchema.optional(),
// Sorting
sort: z.array(z.object({
field: z.string(),
direction: z.enum(['asc', 'desc']),
})).optional(),
// Chart (if format is 'chart')
chart: ChartWidgetSchema.optional(),
// Limits
rowLimit: z.number().optional(),
// Scheduling
schedule: z.object({
enabled: z.boolean(),
frequency: z.enum(['daily', 'weekly', 'monthly']),
recipients: z.array(z.string()),
format: z.enum(['pdf', 'excel', 'csv']),
}).optional(),
});Define buttons and user actions.
Standard Action Structure:
export const ActionSchema = z.object({
name: z.string(),
label: z.string(),
icon: z.string().optional(),
// Action type
type: z.enum(['script', 'url', 'flow', 'modal', 'download']),
// Behavior
script: z.string().optional().describe('JavaScript code'),
url: z.string().optional(),
urlTarget: z.enum(['_self', '_blank', 'modal']).optional(),
flowName: z.string().optional(),
modalComponent: z.string().optional(),
// Context
context: z.enum(['global', 'object', 'record', 'selection']),
object: z.string().optional(),
// Confirmation
confirmationRequired: z.boolean().default(false),
confirmationMessage: z.string().optional(),
// Visibility
visible: z.string().optional().describe('Visibility formula'),
enabled: z.string().optional().describe('Enabled formula'),
// Style
variant: z.enum(['default', 'primary', 'secondary', 'destructive', 'ghost', 'outline']).default('default'),
size: z.enum(['sm', 'md', 'lg']).default('md'),
});Define custom pages with flexible layouts.
Standard Page Structure:
export const PageSchema = z.object({
name: z.string(),
label: z.string(),
description: z.string().optional(),
// Layout type
layout: z.enum(['single', 'sidebar', 'two-column', 'three-column']),
// Regions
regions: z.record(z.string(), z.object({
components: z.array(ComponentSchema),
})),
// Access
requireAuth: z.boolean().default(true),
permissions: z.array(z.string()).optional(),
});
export const ComponentSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('view'),
object: z.string(),
viewName: z.string(),
}),
z.object({
type: z.literal('dashboard'),
dashboardName: z.string(),
}),
z.object({
type: z.literal('report'),
reportName: z.string(),
}),
z.object({
type: z.literal('html'),
content: z.string(),
}),
z.object({
type: z.literal('markdown'),
content: z.string(),
}),
z.object({
type: z.literal('custom'),
componentName: z.string(),
props: z.record(z.any()).optional(),
}),
]);Define visual styling and branding.
Standard Theme Structure:
export const ThemeSchema = z.object({
name: z.string(),
label: z.string(),
// Colors
colors: z.object({
primary: z.string(),
secondary: z.string(),
accent: z.string(),
success: z.string(),
warning: z.string(),
error: z.string(),
info: z.string(),
background: z.string(),
foreground: z.string(),
muted: z.string(),
border: z.string(),
}),
// Typography
typography: z.object({
fontFamily: z.string().default('Inter, system-ui, sans-serif'),
fontSize: z.object({
xs: z.string().default('0.75rem'),
sm: z.string().default('0.875rem'),
base: z.string().default('1rem'),
lg: z.string().default('1.125rem'),
xl: z.string().default('1.25rem'),
'2xl': z.string().default('1.5rem'),
}),
fontWeight: z.object({
normal: z.number().default(400),
medium: z.number().default(500),
semibold: z.number().default(600),
bold: z.number().default(700),
}),
}).optional(),
// Spacing
spacing: z.object({
unit: z.number().default(4).describe('Base unit in pixels'),
}).optional(),
// Borders
borderRadius: z.object({
sm: z.string().default('0.25rem'),
md: z.string().default('0.5rem'),
lg: z.string().default('1rem'),
full: z.string().default('9999px'),
}).optional(),
// Shadows
shadows: z.object({
sm: z.string(),
md: z.string(),
lg: z.string(),
}).optional(),
// Dark mode
darkMode: z.boolean().default(false),
});Define custom field widgets for forms.
Standard Widget Contract:
export const FieldWidgetPropsSchema = z.object({
// Value binding
value: z.any().describe('Current field value'),
onChange: z.function().describe('(value: any) => void'),
// Field metadata
field: z.any().describe('Field definition'),
// State
readonly: z.boolean().default(false),
required: z.boolean().default(false),
disabled: z.boolean().default(false),
// Validation
error: z.string().optional(),
touched: z.boolean().default(false),
// Context
record: z.record(z.any()).optional().describe('Full record data'),
mode: z.enum(['create', 'edit', 'view']),
// Widget-specific options
options: z.record(z.any()).optional(),
});
export const WidgetRegistrySchema = z.object({
name: z.string().describe('Widget unique name'),
label: z.string(),
description: z.string().optional(),
// Supported field types
supportedTypes: z.array(z.string()),
// Component reference
component: z.string().describe('React component path or URL'),
// Configuration schema
configSchema: z.record(z.any()).optional(),
// Preview
icon: z.string().optional(),
screenshot: z.string().optional(),
});- Configuration Keys:
camelCase(e.g.,pageSize,defaultFilter) - Component Names:
PascalCase(e.g.,ListView,FormView)
import { z } from 'zod';
export const ViewSchema = z.object({
name: z.string().describe('Unique view identifier'),
// ... more fields
});
export type View = z.infer<typeof ViewSchema>;Use for polymorphic types:
const NavItemSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('object'), /* ... */ }),
z.object({ type: z.literal('dashboard'), /* ... */ }),
]);When user says:
- "Create View Protocol" → Implement complete
view.zod.tswith List + Form views - "Create App Navigation" → Implement
app.zod.tswith menu structures - "Create Dashboard Protocol" → Implement
dashboard.zod.tswith widgets - "Create Report Protocol" → Implement
report.zod.tswith analytics - "Create Action Protocol" → Implement
action.zod.tswith button behaviors - "Create Page Builder" → Implement
page.zod.tswith flexible layouts - "Create Theme System" → Implement
theme.zod.tswith branding - "Create Widget Contract" → Implement
widget.zod.tswith field components
- Server-Driven UI: All UI must be definable as JSON metadata
- Responsive Design: Consider mobile/tablet/desktop breakpoints
- Accessibility: Include ARIA labels and keyboard navigation hints
- Performance: Lazy loading, pagination, virtualization
- Extensibility: Allow custom components via plugin system
- Consistency: Follow Shadcn UI + Tailwind CSS patterns
See:
packages/spec/src/ui/view.zod.ts- Current view implementationpackages/spec/src/ui/app.zod.ts- Current app implementationexamples/crm/- Full CRM with dashboards and reports