# ObjectUI Documentation # ObjectUI Documentation [#objectui-documentation] ObjectUI is a schema-driven UI engine for React. It renders JSON metadata into accessible, themeable components built with Tailwind CSS, Shadcn UI, and Radix primitives. Use it when your application needs server-driven pages, metadata-defined forms, reusable enterprise views, or an embeddable renderer that stays independent from any single backend. ## Start Here [#start-here] 1. [Quick Start](/docs/guide/quick-start) - install ObjectUI and render a first schema. 2. [Schema Rendering](/docs/guide/schema-rendering) - learn how JSON becomes React UI. 3. [Data Connectivity](/docs/guide/data-source) - connect a backend through the `DataSource` contract. 4. [Schema Reference](/docs/api/schema-reference) - inspect the supported schema shapes. ## A Small Schema [#a-small-schema] ```json { "type": "data-table", "caption": "Users", "className": "rounded-lg border", "columns": [ { "header": "Name", "accessorKey": "name", "sortable": true }, { "header": "Email", "accessorKey": "email" } ], "data": [ { "name": "Ada Lovelace", "email": "ada@example.com" }, { "name": "Grace Hopper", "email": "grace@example.com" } ] } ``` That schema renders through `SchemaRenderer` after the component package is imported once for registration side effects. ## Build Paths [#build-paths] ### Render Schemas [#render-schemas] * [Schema Renderer](/docs/core/schema-renderer) explains the runtime component. * [Component Registry](/docs/guide/component-registry) explains how `type` maps to React components. * [Expressions](/docs/guide/expressions) covers visibility, disabled state, and dynamic values. ### Build Applications [#build-applications] * [App Schema](/docs/core/app-schema) defines app navigation, branding, and layout. * [Layout Guide](/docs/guide/layout) covers page structure and layout primitives. * [Building a CRUD App](/docs/guide/building-crud-app) walks through a full task manager. ### Connect Data [#connect-data] * [Data Connectivity](/docs/guide/data-source) covers the backend adapter contract. * [ObjectStack Adapter](/docs/utilities/data-objectstack) connects ObjectUI to ObjectStack backends. * [User State Persistence](/docs/guide/user-state-persistence) covers favorites and recent items. ### Extend ObjectUI [#extend-objectui] * [Plugin Guide](/docs/guide/plugins) explains lazy-loaded feature packages. * [Plugin Development](/docs/guide/plugin-development) walks through a custom plugin. * [Theming](/docs/guide/theming) covers design tokens, Tailwind, and runtime themes. ## Reference [#reference] * [Components](/docs/components) - core renderers grouped by category. * [Fields](/docs/guide/fields) - field widgets and cell renderers. * [Plugins](/docs/plugins) - heavier views such as grids, kanban, charts, maps, and reports. * [Utilities](/docs/utilities) - CLI, runner, plugin scaffolding, and editor tooling. ## Console [#console] The ObjectUI Console is the reference application for rendering ObjectStack metadata as an admin UI. Start with [Console](/docs/guide/console), then use [Console Architecture](/docs/guide/console-architecture) and [Metadata Diagnostics](/docs/guide/metadata-diagnostics) when integrating a real backend. # API Reference # API Reference [#api-reference] Comprehensive reference documentation for all ObjectUI types, schemas, and APIs. ## Schema Types [#schema-types] ### [Schema Type Reference](/docs/api/schema-reference) [#schema-type-reference] Complete reference for every ObjectUI schema type with annotated JSON examples covering: * **Base** — `SchemaNode`, `BaseSchema` * **Layout** — `PageSchema`, `DivSchema`, `CardSchema`, `GridSchema`, `TabsSchema` * **Forms** — `FormSchema`, `InputSchema`, `SelectSchema`, `ButtonSchema` * **Data Display** — `TableSchema`, `ChartSchema`, `TreeViewSchema` * **CRUD** — `CRUDSchema`, `ActionSchema`, `DetailSchema` * **ObjectQL** — `ObjectGridSchema`, `ObjectFormSchema`, `ObjectViewSchema` * **Complex** — `KanbanSchema`, `DashboardSchema`, `CalendarViewSchema` * **Views** — `DetailViewSchema`, `ViewSwitcherSchema` # Schema Type Reference # Schema Type Reference [#schema-type-reference] This reference documents every ObjectUI schema type with annotated JSON examples. Each schema extends `BaseSchema` and can be rendered by the ObjectUI engine from pure JSON. > **Import:** All types are available from `@object-ui/types`. > > ```typescript > import type { PageSchema, FormSchema, TableSchema, /* ... */ } from '@object-ui/types'; > ``` *** ## Base Schema [#base-schema] ### SchemaNode [#schemanode] The foundational building block of ObjectUI. Every component in the system is described by a `SchemaNode`. It can be a full schema object, or a primitive value rendered as text. ```typescript // Type definition type SchemaNode = BaseSchema | string | number | boolean | null | undefined; ``` ### BaseSchema [#baseschema] All schema types extend `BaseSchema`. These shared properties are available on every component. ```json { "type": "div", "id": "my-component", "name": "wrapper", "label": "Wrapper", "description": "A container element", "className": "p-4 bg-white rounded-lg", "visible": true, "visibleOn": "${data.showWrapper}", "disabled": false, "disabledOn": "${data.isLocked}", "testId": "wrapper-element", "ariaLabel": "Content wrapper", "body": [] } ``` | Property | Type | Description | | ------------------------- | ---------------------------- | ----------------------------------------------------------------------------- | | `type` | `string` | **Required.** Component type identifier (e.g. `"page"`, `"form"`, `"table"`). | | `id` | `string` | Unique instance identifier. | | `name` | `string` | Component name, used for form fields and data binding. | | `label` | `string` | Human-readable display label. | | `description` | `string` | Help text or tooltip content. | | `className` | `string` | Tailwind CSS utility classes. | | `body` | `SchemaNode \| SchemaNode[]` | Child components rendered inside this component. | | `children` | `SchemaNode \| SchemaNode[]` | Alias for `body`. | | `visible` / `visibleOn` | `boolean` / `string` | Control visibility. `visibleOn` accepts expression strings. | | `hidden` / `hiddenOn` | `boolean` / `string` | Inverse of visible. | | `disabled` / `disabledOn` | `boolean` / `string` | Control disabled state. | | `testId` | `string` | Test identifier for automated testing. | | `ariaLabel` | `string` | Accessibility label. | *** ## Layout Schemas [#layout-schemas] ### PageSchema [#pageschema] Top-level page container. Defines a full page with optional regions (header, sidebar, footer). ```json { "type": "page", "title": "User Dashboard", "icon": "LayoutDashboard", "description": "Overview of user activity", "pageType": "detail", "object": "User", "variables": [ { "name": "userId", "type": "string", "defaultValue": "current" } ], "regions": [ { "name": "header", "body": [{ "type": "text", "body": "Welcome back" }] } ], "body": [ { "type": "card", "title": "Activity", "body": [] } ] } ``` | Property | Type | Description | | ------------------ | ---------------- | ------------------------------------------------------------------------------------ | | `title` | `string` | Page title displayed in the header. | | `icon` | `string` | Lucide icon name for the page. | | `pageType` | `PageType` | Page purpose: `"list"`, `"detail"`, `"form"`, `"dashboard"`, `"report"`, `"custom"`. | | `object` | `string` | ObjectQL object name this page operates on. | | `template` | `string` | Template name for page layout. | | `variables` | `PageVariable[]` | Page-level variables with types and defaults. | | `regions` | `PageRegion[]` | Named layout regions (header, sidebar, footer). | | `body` | `SchemaNode[]` | Main page content. | | `isDefault` | `boolean` | Whether this is the default page for the object. | | `assignedProfiles` | `string[]` | Security profiles that can access this page. | **Related:** [AppSchema](/docs/core/app-schema), [DivSchema](#divschema), [GridSchema](#gridschema) *** ### DivSchema [#divschema] A generic container element. The simplest layout primitive. ```json { "type": "div", "className": "flex items-center gap-4 p-6", "children": [ { "type": "text", "body": "Hello World" }, { "type": "button", "label": "Click me" } ] } ``` | Property | Type | Description | | ---------- | ---------------------------- | ---------------------------------------- | | `children` | `SchemaNode \| SchemaNode[]` | Child elements to render inside the div. | **Related:** [GridSchema](#gridschema), [CardSchema](#cardschema) *** ### CardSchema [#cardschema] A styled container with optional header, body, and footer regions. ```json { "type": "card", "title": "Revenue Summary", "description": "Monthly revenue breakdown", "variant": "outline", "hoverable": true, "header": [ { "type": "badge", "label": "Live", "variant": "success" } ], "body": [ { "type": "statistic", "label": "Total Revenue", "value": "$12,400" } ], "footer": [ { "type": "button", "label": "View Details", "variant": "ghost" } ] } ``` | Property | Type | Description | | ------------- | ----------------------------------- | ------------------------------------ | | `title` | `string` | Card title text. | | `description` | `string` | Subtitle / description text. | | `variant` | `"default" \| "outline" \| "ghost"` | Visual style variant. | | `hoverable` | `boolean` | Add hover elevation effect. | | `clickable` | `boolean` | Make the entire card a click target. | | `header` | `SchemaNode \| SchemaNode[]` | Content rendered in the card header. | | `body` | `SchemaNode \| SchemaNode[]` | Main card content. | | `footer` | `SchemaNode \| SchemaNode[]` | Content rendered in the card footer. | **Related:** [DivSchema](#divschema), [GridSchema](#gridschema) *** ### GridSchema [#gridschema] A responsive grid layout. Columns can be a fixed number or responsive breakpoints. ```json { "type": "grid", "columns": { "sm": 1, "md": 2, "lg": 3 }, "gap": 6, "children": [ { "type": "card", "title": "Card 1", "body": [] }, { "type": "card", "title": "Card 2", "body": [] }, { "type": "card", "title": "Card 3", "body": [] } ] } ``` | Property | Type | Description | | ---------- | ---------------------------------- | ---------------------------------------------------------------------- | | `columns` | `number \| Record` | Number of columns, or responsive map (e.g. `{ sm: 1, md: 2, lg: 3 }`). | | `gap` | `number` | Gap between grid items (Tailwind spacing scale). | | `children` | `SchemaNode \| SchemaNode[]` | Grid items. | **Related:** [DivSchema](#divschema), [CardSchema](#cardschema), [DashboardSchema](#dashboardschema) *** ### TabsSchema [#tabsschema] A tabbed interface for organizing content into switchable panels. ```json { "type": "tabs", "defaultValue": "overview", "orientation": "horizontal", "items": [ { "value": "overview", "label": "Overview", "icon": "Info", "content": { "type": "div", "body": [{ "type": "text", "body": "Overview content" }] } }, { "value": "settings", "label": "Settings", "icon": "Settings", "content": { "type": "form", "fields": [] } } ] } ``` | Property | Type | Description | | -------------- | ---------------------------- | ---------------------------------------------------------------------------------------- | | `defaultValue` | `string` | Initially active tab value. | | `value` | `string` | Controlled active tab value. | | `orientation` | `"horizontal" \| "vertical"` | Tab bar orientation. | | `items` | `TabItem[]` | Tab definitions, each with `value`, `label`, `icon`, `content`, and optional `disabled`. | **Related:** [CardSchema](#cardschema), [PageSchema](#pageschema) *** ## Form Schemas [#form-schemas] ### FormSchema [#formschema] A complete form with fields, validation, layout, and actions. ```json { "type": "form", "layout": "horizontal", "columns": 2, "validationMode": "onBlur", "submitLabel": "Save Changes", "showCancel": true, "cancelLabel": "Discard", "defaultValues": { "name": "", "email": "", "role": "viewer" }, "fields": [ { "name": "name", "label": "Full Name", "type": "text", "required": true }, { "name": "email", "label": "Email", "type": "email", "required": true }, { "name": "role", "label": "Role", "type": "select", "options": [ { "label": "Admin", "value": "admin" }, { "label": "Editor", "value": "editor" }, { "label": "Viewer", "value": "viewer" } ]} ] } ``` | Property | Type | Description | | ---------------- | -------------------------------------------------------------- | ------------------------------------------ | | `fields` | `FormField[]` | Field definitions for the form. | | `defaultValues` | `Record` | Initial form values. | | `layout` | `"vertical" \| "horizontal"` | Field label placement. | | `columns` | `number` | Number of columns for field layout. | | `validationMode` | `"onSubmit" \| "onBlur" \| "onChange" \| "onTouched" \| "all"` | When validation triggers. | | `submitLabel` | `string` | Text for the submit button. | | `cancelLabel` | `string` | Text for the cancel button. | | `showCancel` | `boolean` | Whether to show a cancel button. | | `showActions` | `boolean` | Whether to show the action buttons row. | | `resetOnSubmit` | `boolean` | Reset form after successful submit. | | `mode` | `"edit" \| "read" \| "disabled"` | Form interaction mode. | | `actions` | `SchemaNode[]` | Custom action buttons to replace defaults. | **Related:** [InputSchema](#inputschema), [SelectSchema](#selectschema), [ObjectFormSchema](#objectformschema) *** ### InputSchema [#inputschema] A text input field supporting multiple input types with validation. ```json { "type": "input", "name": "email", "label": "Email Address", "inputType": "email", "placeholder": "you@example.com", "required": true, "description": "We'll never share your email", "maxLength": 255 } ``` | Property | Type | Description | | ------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | Field name for form data binding. | | `inputType` | `string` | HTML input type: `"text"`, `"email"`, `"password"`, `"number"`, `"tel"`, `"url"`, `"search"`, `"date"`, `"time"`, `"datetime-local"`. | | `placeholder` | `string` | Placeholder text. | | `required` | `boolean` | Whether the field is required. | | `readOnly` | `boolean` | Render as read-only. | | `error` | `string` | Error message to display. | | `min` / `max` | `number` | Numeric range constraints. | | `step` | `number` | Step increment for number inputs. | | `maxLength` | `number` | Maximum character length. | | `pattern` | `string` | Regex pattern for validation. | **Related:** [FormSchema](#formschema), [SelectSchema](#selectschema) *** ### SelectSchema [#selectschema] A dropdown select field with predefined options. ```json { "type": "select", "name": "priority", "label": "Priority", "placeholder": "Choose priority...", "required": true, "options": [ { "label": "🔴 Critical", "value": "critical" }, { "label": "🟠 High", "value": "high" }, { "label": "🟡 Medium", "value": "medium" }, { "label": "🟢 Low", "value": "low" } ], "defaultValue": "medium" } ``` | Property | Type | Description | | -------------- | ---------------- | ---------------------------------------- | | `name` | `string` | Field name for form data binding. | | `options` | `SelectOption[]` | Array of `{ label, value }` objects. | | `placeholder` | `string` | Placeholder text when no value selected. | | `required` | `boolean` | Whether selection is required. | | `defaultValue` | `string` | Initial selected value. | | `error` | `string` | Error message to display. | **Related:** [FormSchema](#formschema), [InputSchema](#inputschema) *** ### ButtonSchema [#buttonschema] An interactive button with variants, icons, and loading states. ```json { "type": "button", "label": "Deploy to Production", "variant": "default", "size": "lg", "icon": "Rocket", "iconPosition": "left", "loading": false, "buttonType": "submit" } ``` | Property | Type | Description | | -------------- | ----------------------------------------------------------------------------- | --------------------------------------------- | | `label` | `string` | Button text. | | `variant` | `"default" \| "secondary" \| "destructive" \| "outline" \| "ghost" \| "link"` | Visual style. | | `size` | `"default" \| "sm" \| "lg" \| "icon"` | Button size. | | `icon` | `string` | Lucide icon name. | | `iconPosition` | `"left" \| "right"` | Icon placement relative to label. | | `loading` | `boolean` | Show loading spinner and disable interaction. | | `buttonType` | `"button" \| "submit" \| "reset"` | HTML button type. | **Related:** [ActionSchema](#actionschema), [FormSchema](#formschema) *** ## Data Display Schemas [#data-display-schemas] ### TableSchema [#tableschema] A data table with columns, optional striping, and hover effects. ```json { "type": "table", "caption": "Recent Orders", "hoverable": true, "striped": true, "columns": [ { "name": "id", "label": "#", "width": 60 }, { "name": "customer", "label": "Customer" }, { "name": "amount", "label": "Amount", "align": "right" }, { "name": "status", "label": "Status" } ], "data": [ { "id": 1, "customer": "Acme Corp", "amount": "$1,200", "status": "Paid" }, { "id": 2, "customer": "Globex Inc", "amount": "$3,400", "status": "Pending" } ], "footer": { "type": "text", "body": "Showing 2 of 156 orders" } } ``` | Property | Type | Description | | ----------- | ---------------------- | -------------------------------------------------------------------------------- | | `caption` | `string` | Table caption / title text. | | `columns` | `TableColumn[]` | Column definitions with `name`, `label`, `width`, `align`, `sortable`, `render`. | | `data` | `any[]` | Array of row data objects. | | `footer` | `SchemaNode \| string` | Footer content below the table. | | `hoverable` | `boolean` | Highlight rows on hover. | | `striped` | `boolean` | Alternate row background colors. | **Related:** [CRUDSchema](#crudschema), [ObjectGridSchema](#objectgridschema) *** ### ChartSchema [#chartschema] A chart visualization supporting multiple chart types. ```json { "type": "chart", "chartType": "bar", "title": "Monthly Revenue", "description": "Revenue by month for 2024", "height": 350, "showLegend": true, "showGrid": true, "animate": true, "categories": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], "series": [ { "name": "Revenue", "data": [4200, 5100, 4800, 6200, 5800, 7100], "color": "#3b82f6" }, { "name": "Expenses", "data": [3100, 3400, 3200, 3800, 3600, 4000], "color": "#ef4444" } ] } ``` | Property | Type | Description | | ------------------ | --------------------- | --------------------------------------------------------------------------------------------------- | | `chartType` | `ChartType` | **Required.** `"bar"`, `"line"`, `"area"`, `"pie"`, `"donut"`, `"radar"`, `"scatter"`, `"heatmap"`. | | `title` | `string` | Chart title. | | `description` | `string` | Chart description / subtitle. | | `categories` | `string[]` | X-axis category labels. | | `series` | `ChartSeries[]` | Data series, each with `name`, `data` array, and optional `color`. | | `height` / `width` | `string \| number` | Chart dimensions. | | `showLegend` | `boolean` | Display the legend. | | `showGrid` | `boolean` | Display grid lines. | | `animate` | `boolean` | Enable entry animations. | | `config` | `Record` | Additional chart library configuration. | **Related:** [DashboardSchema](#dashboardschema), [CardSchema](#cardschema) *** ### TreeViewSchema [#treeviewschema] A hierarchical tree component for nested data with expand/collapse and selection. ```json { "type": "tree-view", "multiSelect": false, "showLines": true, "defaultExpandedIds": ["root", "src"], "data": [ { "id": "root", "label": "project", "icon": "Folder", "children": [ { "id": "src", "label": "src", "icon": "Folder", "children": [ { "id": "app", "label": "App.tsx", "icon": "FileCode" }, { "id": "index", "label": "index.ts", "icon": "FileCode" } ] }, { "id": "readme", "label": "README.md", "icon": "FileText" } ] } ] } ``` | Property | Type | Description | | -------------------- | ------------ | -------------------------------------------------------------------------------------------- | | `data` | `TreeNode[]` | **Required.** Nested tree data. Each node has `id`, `label`, optional `icon` and `children`. | | `defaultExpandedIds` | `string[]` | Node IDs expanded on initial render. | | `defaultSelectedIds` | `string[]` | Node IDs selected on initial render. | | `expandedIds` | `string[]` | Controlled expanded state. | | `selectedIds` | `string[]` | Controlled selection state. | | `multiSelect` | `boolean` | Allow selecting multiple nodes. | | `showLines` | `boolean` | Show tree connector lines. | **Related:** [TableSchema](#tableschema) *** ## CRUD Schemas [#crud-schemas] ### CRUDSchema [#crudschema] A complete CRUD (Create, Read, Update, Delete) interface with table, toolbar, filters, pagination, and batch/row actions. ```json { "type": "crud", "title": "Products", "resource": "products", "api": "/api/products", "selectable": "multiple", "defaultSort": "name", "defaultSortOrder": "asc", "columns": [ { "name": "name", "label": "Product Name", "sortable": true }, { "name": "price", "label": "Price", "align": "right" }, { "name": "stock", "label": "Stock", "sortable": true }, { "name": "status", "label": "Status" } ], "fields": [ { "name": "name", "label": "Product Name", "type": "text", "required": true }, { "name": "price", "label": "Price", "type": "number", "required": true }, { "name": "stock", "label": "Stock", "type": "number" }, { "name": "status", "label": "Status", "type": "select", "options": [ { "label": "Active", "value": "active" }, { "label": "Draft", "value": "draft" } ]} ], "operations": { "create": true, "read": true, "update": true, "delete": true, "export": true }, "toolbar": { "showSearch": true, "showFilters": true, "showExport": true, "actions": [ { "type": "action", "label": "Add Product", "level": "primary", "icon": "Plus" } ] }, "filters": [ { "name": "status", "label": "Status", "type": "select", "options": [ { "label": "Active", "value": "active" }, { "label": "Draft", "value": "draft" } ]} ], "pagination": { "pageSize": 20, "pageSizeOptions": [10, 20, 50, 100] }, "rowActions": [ { "type": "action", "label": "Edit", "icon": "Pencil", "actionType": "dialog" }, { "type": "action", "label": "Delete", "icon": "Trash2", "level": "danger", "actionType": "confirm" } ], "batchActions": [ { "type": "action", "label": "Delete Selected", "level": "danger", "actionType": "confirm" } ] } ``` | Property | Type | Description | | ---------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------- | | `title` | `string` | CRUD view title. | | `resource` | `string` | Resource identifier for API calls. | | `api` | `string` | Base API endpoint URL. | | `columns` | `TableColumn[]` | **Required.** Column definitions for the table view. | | `fields` | `FormField[]` | Field definitions for create/edit forms. | | `operations` | `object` | Toggle CRUD operations: `create`, `read`, `update`, `delete`, `export`, `import`. | | `toolbar` | `CRUDToolbar` | Toolbar configuration with search, filters, and custom actions. | | `filters` | `CRUDFilter[]` | Filter definitions. | | `pagination` | `CRUDPagination` | Pagination settings with `pageSize` and `pageSizeOptions`. | | `selectable` | `boolean \| "single" \| "multiple"` | Row selection mode. | | `rowActions` | `ActionSchema[]` | Actions available on each row. | | `batchActions` | `ActionSchema[]` | Actions for selected rows. | | `defaultSort` / `defaultSortOrder` | `string` / `"asc" \| "desc"` | Default sort field and direction. | | `mode` | `"table" \| "grid" \| "list" \| "kanban"` | Display mode for the CRUD view. | | `emptyState` | `SchemaNode` | Custom empty state content. | **Related:** [ActionSchema](#actionschema), [TableSchema](#tableschema), [ObjectGridSchema](#objectgridschema) *** ### ActionSchema [#actionschema] A powerful action definition supporting API calls, confirmations, dialogs, chaining, and conditional execution. ```json { "type": "action", "label": "Submit Order", "level": "primary", "icon": "Send", "actionType": "ajax", "api": "/api/orders", "method": "POST", "data": { "status": "submitted" }, "confirm": { "title": "Submit Order?", "message": "This will send the order to the warehouse.", "confirmText": "Yes, Submit", "confirmVariant": "default" }, "successMessage": "Order submitted successfully", "errorMessage": "Failed to submit order", "chain": [ { "type": "action", "label": "Refresh", "actionType": "button", "reload": true } ], "chainMode": "sequential", "condition": { "expression": "${data.items.length > 0}" }, "retry": { "maxAttempts": 3, "delay": 1000 } } ``` | Property | Type | Description | | --------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------- | | `label` | `string` | **Required.** Action display text. | | `level` | `string` | Semantic level: `"primary"`, `"secondary"`, `"success"`, `"warning"`, `"danger"`, `"info"`. | | `icon` | `string` | Lucide icon name. | | `actionType` | `string` | Action kind: `"button"`, `"link"`, `"dropdown"`, `"ajax"`, `"confirm"`, `"dialog"`. | | `api` | `string` | API endpoint for `ajax` actions. | | `method` | `string` | HTTP method: `"GET"`, `"POST"`, `"PUT"`, `"DELETE"`, `"PATCH"`. | | `data` | `any` | Request body data. | | `confirm` | `object` | Confirmation dialog with `title`, `message`, `confirmText`, `cancelText`. | | `dialog` | `object` | Modal dialog with `title`, `content`, `size`, `actions`. | | `chain` | `ActionSchema[]` | Actions to execute after this action completes. | | `chainMode` | `"sequential" \| "parallel"` | How chained actions execute. | | `condition` | `ActionCondition` | Conditional execution with `expression`, `then`, `else`. | | `successMessage` / `errorMessage` | `string` | Toast messages on success/failure. | | `reload` | `boolean` | Reload data after action completes. | | `redirect` | `string` | URL to navigate to after action. | | `retry` | `object` | Retry config with `maxAttempts` and `delay`. | **Related:** [CRUDSchema](#crudschema), [ButtonSchema](#buttonschema) *** ### DetailSchema [#detailschema] A single-record detail view with grouped fields, actions, and tabs. ```json { "type": "detail", "title": "Order #1042", "api": "/api/orders/1042", "showBack": true, "groups": [ { "title": "Customer Info", "fields": [ { "name": "customer", "label": "Customer", "type": "text" }, { "name": "email", "label": "Email", "type": "link" }, { "name": "created", "label": "Created", "type": "date", "format": "MMM d, yyyy" } ] }, { "title": "Order Details", "fields": [ { "name": "total", "label": "Total", "type": "text" }, { "name": "status", "label": "Status", "type": "badge" } ] } ], "actions": [ { "type": "action", "label": "Edit", "icon": "Pencil", "level": "primary" }, { "type": "action", "label": "Delete", "icon": "Trash2", "level": "danger", "actionType": "confirm" } ], "tabs": [ { "key": "items", "label": "Line Items", "content": { "type": "table", "columns": [], "data": [] } }, { "key": "history", "label": "History", "content": { "type": "timeline", "events": [] } } ] } ``` | Property | Type | Description | | ------------ | ------------------ | ------------------------------------------------------------- | | `title` | `string` | Detail page title. | | `api` | `string` | API endpoint to fetch record data. | | `resourceId` | `string \| number` | ID of the record to display. | | `groups` | `array` | Field groups, each with `title`, `description`, and `fields`. | | `actions` | `ActionSchema[]` | Available actions (edit, delete, etc.). | | `tabs` | `array` | Additional tabbed content with `key`, `label`, and `content`. | | `showBack` | `boolean` | Show a back navigation button. | | `loading` | `boolean` | Show loading state. | **Related:** [DetailViewSchema](#detailviewschema), [CRUDSchema](#crudschema) *** ## ObjectQL Schemas [#objectql-schemas] These schemas integrate with [ObjectStack](https://objectstack.ai) for automatic data fetching, but work with any backend through the `data` prop. ### ObjectGridSchema [#objectgridschema] A data grid that auto-fetches from an ObjectQL object definition. Includes search, filters, pagination, grouping, and inline editing. ```json { "type": "object-grid", "objectName": "Contact", "title": "All Contacts", "description": "Manage your contacts", "showSearch": true, "showFilters": true, "showPagination": true, "pageSize": 25, "resizableColumns": true, "striped": true, "columns": [ "name", "email", "company", "phone", { "name": "status", "label": "Status", "sortable": true } ], "defaultSort": { "field": "name", "order": "asc" }, "operations": { "create": true, "read": true, "update": true, "delete": true, "export": true }, "rowActions": ["edit", "delete"], "selection": { "enabled": true, "mode": "multiple" }, "pagination": { "enabled": true, "pageSize": 25, "pageSizeOptions": [10, 25, 50, 100] } } ``` | Property | Type | Description | | ---------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `objectName` | `string` | **Required.** ObjectQL object API name. | | `columns` | `string[] \| ListColumn[]` | Columns to display. Strings auto-resolve from object metadata. | | `filter` | `any[]` | Pre-applied filter conditions. | | `sort` | `string \| SortConfig[]` | Default sort configuration. | | `searchableFields` | `string[]` | Fields included in search. | | `selection` | `SelectionConfig` | Row selection configuration. | | `pagination` | `PaginationConfig` | Pagination settings. | | `operations` | `object` | Enabled CRUD operations. | | `rowActions` / `bulkActions` | `string[]` | Action identifiers for rows and batch selection. `bulkActions` is the spec-aligned key; `batchActions` is a legacy alias that takes precedence when both are set. | | `editable` | `boolean` | Enable inline cell editing. | | `grouping` | `GroupingConfig` | Row grouping configuration. | | `frozenColumns` | `number` | Number of columns frozen on scroll. | | `navigation` | `ViewNavigationConfig` | SPA navigation configuration. | **Related:** [ObjectViewSchema](#objectviewschema), [CRUDSchema](#crudschema), [TableSchema](#tableschema) *** ### ObjectFormSchema [#objectformschema] A smart form that auto-generates fields from an ObjectQL object. Supports simple, tabbed, wizard, split, drawer, and modal layouts. ```json { "type": "object-form", "objectName": "Contact", "mode": "create", "formType": "tabbed", "title": "New Contact", "layout": "vertical", "columns": 2, "fields": ["firstName", "lastName", "email", "phone", "company"], "sections": [ { "label": "Basic Info", "fields": ["firstName", "lastName", "email"] }, { "label": "Details", "fields": ["phone", "company", "address"] } ], "showSubmit": true, "submitText": "Create Contact", "showCancel": true, "cancelText": "Cancel" } ``` | Property | Type | Description | | ----------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `objectName` | `string` | **Required.** ObjectQL object API name. | | `mode` | `"create" \| "edit" \| "view"` | **Required.** Form interaction mode. | | `formType` | `string` | Layout type: `"simple"`, `"tabbed"`, `"wizard"`, `"split"`, `"drawer"`, `"modal"`. Aligned with `@objectstack/spec` `FormViewSchema.type`. | | `recordId` | `string \| number` | Record ID for edit/view modes. | | `fields` | `string[]` | Field API names to include (auto-resolved from object metadata). | | `customFields` | `FormField[]` | Manually defined fields that override auto-generated ones. | | `sections` | `ObjectFormSection[]` | Field sections (simple groups, tabs, or wizard steps depending on `formType`). Spec-aligned key. | | `groups` | `array` | **Deprecated.** Legacy alias of `sections` (spec defines `groups` as an alias); normalized into `sections` when `sections` is absent. Legacy shape: `title`→`label`, `defaultCollapsed`→`collapsed`. | | `layout` | `string` | Label layout: `"vertical"`, `"horizontal"`, `"inline"`, `"grid"`. | | `columns` | `number` | Number of form columns. | | `submitText` / `cancelText` | `string` | Button labels. | | `showSubmit` / `showCancel` / `showReset` | `boolean` | Toggle action buttons. | | `drawerSide` | `string` | Drawer position: `"top"`, `"bottom"`, `"left"`, `"right"`. | | `modalSize` | `string` | Modal size: `"sm"`, `"default"`, `"lg"`, `"xl"`, `"full"`. | #### Spec alignment & extension keys [#spec-alignment--extension-keys] `ObjectFormSchema` keys fall into three classes (#2545): * **Spec-aligned** — same name and semantics as `@objectstack/spec` `FormViewSchema`: `title`, `description`, `layout`, `columns`, `sections`, `defaultTab`, `tabPosition`, `allowSkip`, `showStepIndicator`, `splitDirection`/`splitSize`/`splitResizable`, `drawerSide`/`drawerWidth`, `modalSize`, `subforms`, `submitBehavior` (plus `formType` ↔ spec `type`). Metadata using these keys round-trips through the SpecBridge without loss. * **ObjectUI extensions** — serializable extras with no spec backing yet: `showSubmit`/`submitText`, `showCancel`/`cancelText`, `showReset`, `nextText`/`prevText`, `successMessage`, `navigateOnSuccess`, `resetOnSuccess`, `modalCloseButton`, `className`, `initialValues`, `fields`, `customFields`. Sanctioned and documented here; candidates for upstreaming into the spec are tracked in #2545. * **Runtime-only** — non-serializable renderer concerns that never appear in view metadata: `mode`, `recordId`, `open`/`onOpenChange`, `readOnly`, and all callbacks (`onSuccess`, `onError`, `onCancel`, `onStepChange`, `submitHandler`). **Related:** [FormSchema](#formschema), [ObjectViewSchema](#objectviewschema) *** ### ObjectViewSchema [#objectviewschema] A complete object management interface combining grid, form, search, filters, and view switching. ```json { "type": "object-view", "objectName": "Deal", "title": "Sales Pipeline", "description": "Manage your deals", "defaultViewType": "kanban", "showSearch": true, "showFilters": true, "showCreate": true, "showRefresh": true, "showViewSwitcher": true, "operations": { "create": true, "read": true, "update": true, "delete": true }, "searchableFields": ["name", "company", "owner"], "filterableFields": ["stage", "owner", "value"], "listViews": { "all": { "label": "All Deals", "filter": [], "sort": [{ "field": "value", "order": "desc" }] }, "my-deals": { "label": "My Deals", "filter": [["owner", "=", "${currentUser.id}"]], "default": true } }, "table": { "columns": ["name", "stage", "value", "owner", "closeDate"], "pageSize": 25 }, "form": { "formType": "drawer", "drawerSide": "right", "fields": ["name", "stage", "value", "owner", "closeDate", "notes"] } } ``` | Property | Type | Description | | ----------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------ | | `objectName` | `string` | **Required.** ObjectQL object API name. | | `title` | `string` | View title. | | `defaultViewType` | `string` | Initial view: `"grid"`, `"kanban"`, `"gallery"`, `"calendar"`, `"timeline"`, `"gantt"`, `"map"`. | | `listViews` | `Record` | Named list views with filters and sort. | | `defaultListView` | `string` | Key of the default list view. | | `table` | `Partial` | Grid configuration overrides. | | `form` | `Partial` | Form configuration overrides. | | `showSearch` / `showFilters` / `showCreate` / `showRefresh` | `boolean` | Toggle toolbar features. | | `showViewSwitcher` | `boolean` | Show view type toggle (grid, kanban, etc.). | | `operations` | `object` | Enabled CRUD operations. | | `navigation` | `ViewNavigationConfig` | SPA-aware navigation. | **Related:** [ObjectGridSchema](#objectgridschema), [ObjectFormSchema](#objectformschema), [ViewSwitcherSchema](#viewswitcherschema) *** ## Complex Schemas [#complex-schemas] ### KanbanSchema [#kanbanschema] A drag-and-drop Kanban board with columns and cards. ```json { "type": "kanban", "draggable": true, "columns": [ { "id": "todo", "title": "To Do", "color": "#6366f1", "cards": [ { "id": "task-1", "title": "Design mockups", "description": "Create wireframes for new feature" }, { "id": "task-2", "title": "Write tests", "description": "Unit tests for auth module" } ] }, { "id": "in-progress", "title": "In Progress", "color": "#f59e0b", "cards": [ { "id": "task-3", "title": "API integration", "description": "Connect to payment gateway" } ] }, { "id": "done", "title": "Done", "color": "#22c55e", "cards": [] } ] } ``` | Property | Type | Description | | ------------- | ---------------- | --------------------------------------------------------------------------- | | `columns` | `KanbanColumn[]` | **Required.** Board columns, each with `id`, `title`, `color`, and `cards`. | | `draggable` | `boolean` | Enable drag-and-drop between columns. | | `onCardMove` | `function` | Callback when a card is moved: `(cardId, fromColumn, toColumn, position)`. | | `onCardClick` | `function` | Callback when a card is clicked. | | `onColumnAdd` | `function` | Callback when a new column is added. | | `onCardAdd` | `function` | Callback when a new card is added to a column. | **Related:** [ObjectViewSchema](#objectviewschema), [CRUDSchema](#crudschema) *** ### DashboardSchema [#dashboardschema] A widget-based dashboard with configurable grid layout and auto-refresh. ```json { "type": "dashboard", "columns": 4, "gap": 6, "refreshInterval": 30000, "widgets": [ { "id": "revenue", "title": "Total Revenue", "description": "Monthly revenue", "colSpan": 1, "rowSpan": 1, "body": { "type": "statistic", "label": "Revenue", "value": "$48,200", "trend": { "value": 12, "direction": "up" } } }, { "id": "chart", "title": "Sales Trend", "colSpan": 2, "rowSpan": 1, "body": { "type": "chart", "chartType": "area", "categories": ["Mon", "Tue", "Wed", "Thu", "Fri"], "series": [{ "name": "Sales", "data": [120, 180, 150, 210, 190] }] } }, { "id": "tasks", "title": "Recent Tasks", "colSpan": 1, "rowSpan": 1, "body": { "type": "list", "items": [] } } ] } ``` | Property | Type | Description | | ----------------- | ------------------------- | -------------------------------------------------------------------------------------- | | `columns` | `number` | Number of grid columns. | | `gap` | `number` | Gap between widgets (Tailwind spacing scale). | | `widgets` | `DashboardWidgetSchema[]` | **Required.** Widget definitions with `id`, `title`, `colSpan`, `rowSpan`, and `body`. | | `refreshInterval` | `number` | Auto-refresh interval in milliseconds. | Each widget supports `colSpan` and `rowSpan` to control its size in the grid. The `body` can be any `SchemaNode`. **Related:** [GridSchema](#gridschema), [ChartSchema](#chartschema), [CardSchema](#cardschema) *** ### CalendarViewSchema [#calendarviewschema] A multi-view calendar for displaying and managing events. ```json { "type": "calendar-view", "defaultView": "month", "views": ["month", "week", "day", "agenda"], "editable": true, "events": [ { "id": "evt-1", "title": "Team Standup", "start": "2024-03-15T09:00:00", "end": "2024-03-15T09:30:00", "color": "#3b82f6" }, { "id": "evt-2", "title": "Sprint Review", "start": "2024-03-15T14:00:00", "end": "2024-03-15T15:00:00", "color": "#8b5cf6", "allDay": false } ] } ``` | Property | Type | Description | | --------------- | -------------------- | --------------------------------------------------------------------------------------- | | `events` | `CalendarEvent[]` | **Required.** Events with `id`, `title`, `start`, `end`, optional `color` and `allDay`. | | `defaultView` | `CalendarViewMode` | Initial view: `"month"`, `"week"`, `"day"`, `"agenda"`. | | `views` | `CalendarViewMode[]` | Available view modes the user can switch between. | | `editable` | `boolean` | Allow creating and modifying events. | | `defaultDate` | `string \| Date` | Initial calendar date. | | `onEventClick` | `function` | Callback when an event is clicked. | | `onEventCreate` | `function` | Callback for new event creation: `(start, end)`. | | `onEventUpdate` | `function` | Callback when an event is modified. | | `onDateChange` | `function` | Callback when the visible date range changes. | **Related:** [ObjectViewSchema](#objectviewschema), [DashboardSchema](#dashboardschema) *** ## View Schemas [#view-schemas] ### DetailViewSchema [#detailviewschema] An enhanced detail view for a single record with sections, tabs, related records, and navigation. ```json { "type": "detail-view", "title": "Contact Details", "objectName": "Contact", "resourceId": "contact-123", "layout": "grid", "columns": 2, "showBack": true, "backUrl": "/contacts", "showEdit": true, "editUrl": "/contacts/contact-123/edit", "showDelete": true, "deleteConfirmation": "Are you sure you want to delete this contact?", "sections": [ { "title": "Personal Information", "icon": "User", "columns": 2, "collapsible": true, "fields": [ { "name": "firstName", "label": "First Name", "type": "text" }, { "name": "lastName", "label": "Last Name", "type": "text" }, { "name": "email", "label": "Email", "type": "link" }, { "name": "avatar", "label": "Photo", "type": "image" } ] } ], "tabs": [ { "key": "activities", "label": "Activities", "icon": "Activity", "badge": 5, "content": { "type": "timeline", "events": [] } } ], "related": [ { "title": "Recent Orders", "type": "table", "api": "/api/contacts/contact-123/orders", "columns": [ { "name": "id", "label": "Order #" }, { "name": "total", "label": "Total" }, { "name": "status", "label": "Status" } ] } ], "actions": [ { "type": "action", "label": "Send Email", "icon": "Mail", "level": "primary" } ] } ``` | Property | Type | Description | | ----------------------------------- | -------------------------------------- | --------------------------------------------------------------- | | `title` | `string` | Detail page title. | | `objectName` | `string` | ObjectQL object name for data binding. | | `resourceId` | `string \| number` | Record ID to display. | | `api` | `string` | API endpoint to fetch record data. | | `data` | `any` | Static data (if not fetching from API). | | `layout` | `"vertical" \| "horizontal" \| "grid"` | Field layout mode. | | `columns` | `number` | Grid columns (for grid layout). | | `sections` | `DetailViewSection[]` | Field groups with `title`, `icon`, `fields`, `collapsible`. | | `fields` | `DetailViewField[]` | Direct fields (without sections). | | `tabs` | `DetailViewTab[]` | Tabbed content with `key`, `label`, `icon`, `badge`, `content`. | | `related` | `array` | Related record sections with `title`, `type`, `api`, `columns`. | | `actions` | `ActionSchema[]` | Available actions. | | `showBack` / `backUrl` | `boolean` / `string` | Back navigation. | | `showEdit` / `editUrl` | `boolean` / `string` | Edit navigation. | | `showDelete` / `deleteConfirmation` | `boolean` / `string` | Delete with confirmation message. | | `header` / `footer` | `SchemaNode` | Custom header/footer content. | **Related:** [DetailSchema](#detailschema), [ObjectViewSchema](#objectviewschema) *** ### ViewSwitcherSchema [#viewswitcherschema] A toggle control that switches between different view types (list, grid, kanban, calendar, etc.). ```json { "type": "view-switcher", "defaultView": "list", "variant": "tabs", "position": "top", "persistPreference": true, "storageKey": "contacts-view-pref", "views": [ { "type": "list", "label": "List View", "icon": "List", "schema": { "type": "object-grid", "objectName": "Contact", "columns": ["name", "email", "phone"] } }, { "type": "grid", "label": "Card View", "icon": "LayoutGrid", "schema": { "type": "grid", "columns": 3, "children": [] } }, { "type": "kanban", "label": "Kanban", "icon": "Kanban", "schema": { "type": "kanban", "columns": [] } } ] } ``` | Property | Type | Description | | ------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- | | `views` | `array` | **Required.** Available views, each with `type`, `label`, `icon`, and `schema`. | | `defaultView` | `ViewType` | Initially active view: `"list"`, `"detail"`, `"grid"`, `"kanban"`, `"calendar"`, `"timeline"`, `"map"`. | | `activeView` | `ViewType` | Controlled active view. | | `variant` | `"tabs" \| "buttons" \| "dropdown"` | Switcher UI style. | | `position` | `"top" \| "bottom" \| "left" \| "right"` | Switcher position relative to content. | | `persistPreference` | `boolean` | Save the user's view preference to storage. | | `storageKey` | `string` | Storage key for persisting the preference. | | `onViewChange` | `string` | Expression or callback invoked on view change. | **Related:** [ObjectViewSchema](#objectviewschema), [KanbanSchema](#kanbanschema), [CalendarViewSchema](#calendarviewschema) *** ## Schema Composition [#schema-composition] Schemas are designed to compose. Nest any `SchemaNode` inside another to build complex interfaces: ```json { "type": "page", "title": "CRM Dashboard", "body": [ { "type": "grid", "columns": { "sm": 1, "lg": 2 }, "gap": 6, "children": [ { "type": "card", "title": "Quick Stats", "body": { "type": "dashboard", "columns": 2, "widgets": [ { "id": "w1", "title": "Leads", "body": { "type": "statistic", "value": "142" } }, { "id": "w2", "title": "Revenue", "body": { "type": "statistic", "value": "$24k" } } ] } }, { "type": "card", "title": "Recent Activity", "body": { "type": "tabs", "items": [ { "value": "deals", "label": "Deals", "content": { "type": "table", "columns": [], "data": [] } }, { "value": "tasks", "label": "Tasks", "content": { "type": "table", "columns": [], "data": [] } } ] } } ] }, { "type": "object-grid", "objectName": "Lead", "title": "All Leads", "showSearch": true, "columns": ["name", "company", "status", "value"] } ] } ``` ## Type Imports [#type-imports] Import only the types you need: ```typescript // Layout import type { PageSchema, DivSchema, CardSchema, GridSchema, TabsSchema } from '@object-ui/types'; // Forms import type { FormSchema, InputSchema, SelectSchema, ButtonSchema } from '@object-ui/types'; // Data Display import type { TableSchema, ChartSchema, TreeViewSchema } from '@object-ui/types'; // CRUD import type { CRUDSchema, ActionSchema, DetailSchema } from '@object-ui/types'; // ObjectQL import type { ObjectGridSchema, ObjectFormSchema, ObjectViewSchema } from '@object-ui/types'; // Complex import type { KanbanSchema, DashboardSchema, CalendarViewSchema } from '@object-ui/types'; // Views import type { DetailViewSchema, ViewSwitcherSchema } from '@object-ui/types'; // Base import type { BaseSchema, SchemaNode } from '@object-ui/types'; ``` ## Next Steps [#next-steps] * **[Schema Overview](/docs/guide/schema-overview)** — High-level guide to ObjectUI schemas * **[Quick Start](/docs/guide/quick-start)** — Build your first ObjectUI application * **[Expressions](/docs/guide/expressions)** — Dynamic expressions with `visibleOn`, `disabledOn` * **[Fields Guide](/docs/guide/fields)** — Deep dive into form fields * **[Plugin Development](/docs/guide/plugin-development)** — Build custom schema renderers # Component Gallery # Component Gallery [#component-gallery] ObjectUI provides a comprehensive set of components built on React, Tailwind CSS, and Shadcn UI. All components are defined through JSON schemas and rendered with pixel-perfect quality. ## Quick Navigation [#quick-navigation] Browse components by category to find what you need: ### [Basic Components](/docs/components/basic/text) [#basic-components] Essential building blocks: Text, Icon, Image, Separator, HTML ### [Form Components](/docs/components/form/button) [#form-components] Interactive inputs: Button, Input, Select, Checkbox, Switch, Textarea, Slider ### [Layout Components](/docs/components/layout/container) [#layout-components] Structure your UI: Container, Card, Grid, Flex, Stack, Tabs ### [Data Display](/docs/components/data-display/badge) [#data-display] Show information: Badge, Avatar, Alert, List ### [Feedback Components](/docs/components/feedback/loading) [#feedback-components] User feedback: Loading, Progress, Skeleton ### [Overlay Components](/docs/components/overlay/dialog) [#overlay-components] Floating elements: Dialog, Drawer, Tooltip, Popover ### [Disclosure Components](/docs/components/disclosure/accordion) [#disclosure-components] Show/hide content: Accordion, Collapsible ### [Complex Components](/docs/components/complex/table) [#complex-components] Advanced patterns: Table (with sorting, filtering, pagination) ## Component Categories [#component-categories] ### Basic Components [#basic-components-1] The foundation of your UI. These are simple, single-purpose components: * **[Text](/docs/components/basic/text)** - Display text with typography control * **[Icon](/docs/components/basic/icon)** - Render icons from Lucide React * **[Image](/docs/components/basic/image)** - Display images with lazy loading * **[Separator](/docs/components/basic/separator)** - Visual divider between content * **[HTML](/docs/components/basic/html)** - Render raw HTML content ### Form Components [#form-components-1] Interactive elements for user input: * **[Button](/docs/components/form/button)** - Trigger actions with multiple variants * **[Input](/docs/components/form/input)** - Text input with validation * **[Select](/docs/components/form/select)** - Dropdown selection * **[Checkbox](/docs/components/form/checkbox)** - Boolean selection * **[Switch](/docs/components/form/switch)** - Toggle switch * **[Textarea](/docs/components/form/textarea)** - Multi-line text input * **[Slider](/docs/components/form/slider)** - Numeric range selection ### Layout Components [#layout-components-1] Structure and organize your interface: * **[Container](/docs/components/layout/container)** - Responsive container with max-width * **[Card](/docs/components/layout/card)** - Content card with header and footer * **[Grid](/docs/components/layout/grid)** - CSS Grid layout * **[Flex](/docs/components/layout/flex)** - Flexbox layout * **[Stack](/docs/components/layout/stack)** - Vertical or horizontal stack * **[Tabs](/docs/components/layout/tabs)** - Tabbed interface ### Data Display [#data-display-1] Present data to users: * **[Badge](/docs/components/data-display/badge)** - Small status indicators * **[Avatar](/docs/components/data-display/avatar)** - User profile images * **[Alert](/docs/components/data-display/alert)** - Contextual messages * **[List](/docs/components/data-display/list)** - Ordered or unordered lists ### Feedback Components [#feedback-components-1] Provide visual feedback: * **[Loading](/docs/components/feedback/loading)** - Loading spinner * **[Progress](/docs/components/feedback/progress)** - Progress bar * **[Skeleton](/docs/components/feedback/skeleton)** - Loading placeholder ### Overlay Components [#overlay-components-1] Floating UI elements: * **[Dialog](/docs/components/overlay/dialog)** - Modal dialog * **[Drawer](/docs/components/overlay/drawer)** - Slide-out drawer * **[Tooltip](/docs/components/overlay/tooltip)** - Hover tooltips * **[Popover](/docs/components/overlay/popover)** - Floating popover ### Disclosure Components [#disclosure-components-1] Expandable content: * **[Accordion](/docs/components/disclosure/accordion)** - Expandable sections * **[Collapsible](/docs/components/disclosure/collapsible)** - Toggle content visibility ### Complex Components [#complex-components-1] Advanced, feature-rich components: * **[Table](/docs/components/complex/table)** - Data table with sorting, filtering, and pagination ## Usage Pattern [#usage-pattern] All ObjectUI components follow the same schema-based pattern: ```json { "type": "component-name", "className": "tailwind-classes", "componentSpecificProperty": "value" } ``` ### Example: Button [#example-button] ```json { "type": "button", "label": "Click Me", "variant": "default", "size": "md", "className": "mt-4" } ``` ### Example: Card with Form [#example-card-with-form] ```json { "type": "card", "title": "User Profile", "className": "max-w-md", "body": { "type": "form", "fields": [ { "type": "input", "name": "name", "label": "Full Name" }, { "type": "input", "name": "email", "label": "Email", "inputType": "email" } ] } } ``` ## Features [#features] All ObjectUI components share these characteristics: * ✅ **Schema-Driven** - Define with JSON, not code * ✅ **Tailwind CSS** - Use utility classes directly in schemas * ✅ **Accessible** - WCAG 2.1 AA compliant * ✅ **Responsive** - Mobile-first design * ✅ **Themeable** - Light/dark mode support * ✅ **Type-Safe** - Full TypeScript support * ✅ **Performant** - Lazy-loaded and tree-shakable ## Next Steps [#next-steps] * **[Quick Start Guide](/docs/guide/quick-start)** - Build your first ObjectUI app * **[Schema Rendering](/docs/guide/schema-rendering)** - Learn how the engine works * **[Component Registry](/docs/guide/component-registry)** - Register custom components * **[Expressions](/docs/guide/expressions)** - Dynamic values with expressions ## Need Help? [#need-help] Can't find what you're looking for? Check out: * [Architecture](/docs/guide/architecture) - Core concepts and package boundaries * [API Reference](/docs/api) - Schema documentation and protocol specs * [GitHub](https://github.com/objectstack-ai/objectui) - Report issues or contribute # Authentication Blocks # Authentication Blocks [#authentication-blocks] Beautiful, accessible authentication forms and flows. Every block below is sourced from the canonical schema catalog (`@object-ui/example-schema-catalog`), which is smoke-tested on every CI run — if a schema breaks, the build fails. Copy the JSON from the **Code** tab and drop it into your application. ## Login Form [#login-form] A clean login form with email and password fields, "remember me", and a social-provider button. ## Sign Up Form [#sign-up-form] Two-column registration form with terms acceptance. ## Forgot Password [#forgot-password] Request a password reset link by email. ## Two-Factor Authentication [#two-factor-authentication] Six-digit code verification with a resend link. ## Customizing [#customizing] All authentication blocks are plain JSON — tweak any node to change the look or behaviour. Common customisations: ### Change Colors [#change-colors] ```json { "type": "button", "className": "bg-blue-600 hover:bg-blue-700" } ``` ### Wire Up Submission [#wire-up-submission] ```json { "type": "button", "label": "Sign In", "action": { "type": "submit", "endpoint": "/api/auth/login", "method": "POST" } } ``` ### Adjust Layout [#adjust-layout] ```json { "type": "card", "className": "max-w-lg p-8" } ``` ## Next Steps [#next-steps] * Browse [Dashboard Blocks](/docs/blocks/dashboard) * Explore [Form Blocks](/docs/blocks/forms) * Learn about [Actions](/docs/core/enhanced-actions) # Block Schema (BlockSchema) # Block Schema [#block-schema] The `BlockSchema` defines reusable component blocks that can be configured with variables, support content injection through slots, and shared via a marketplace. ## Overview [#overview] BlockSchema enables: * **Reusable components** - Create once, use everywhere * **Variable system** - Typed props for customization * **Slot system** - Content injection points * **Block templates** - Predefined component trees * **Marketplace support** - Share and discover blocks * **Version control** - Track block versions ## Interactive Examples [#interactive-examples] ### Feature Card Block [#feature-card-block] ### Block Variations [#block-variations] ### Block Marketplace Card [#block-marketplace-card] ## Basic Usage [#basic-usage] ```plaintext import type { BlockSchema } from '@object-ui/types'; const heroBlock: BlockSchema = { type: 'block', meta: { name: 'hero-section', label: 'Hero Section', description: 'A customizable hero banner with image and CTA', category: 'Marketing' }, variables: [ { name: 'title', type: 'string', defaultValue: 'Welcome', required: true }, { name: 'showButton', type: 'boolean', defaultValue: true } ], slots: [ { name: 'content', label: 'Content Area' } ], template: { type: 'div', className: 'hero-section', children: [] } }; ``` ## Properties [#properties] ### Block Metadata [#block-metadata] ```plaintext interface BlockMetadata { name: string; // Block identifier label?: string; // Display name description?: string; // Block description category?: string; // Category for organization icon?: string; // Icon name tags?: string[]; // Search tags author?: string; // Creator version?: string; // Version number license?: string; // License type repository?: string; // Source code URL preview?: string; // Preview image URL premium?: boolean; // Premium/paid block } ``` ### Block Variables [#block-variables] Variables define configurable properties: ```plaintext interface BlockVariable { name: string; // Variable name label?: string; // Display label type?: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'component'; defaultValue?: any; // Default value description?: string; // Help text required?: boolean; // Is required? validation?: any; // Validation rules enum?: any[]; // Allowed values } ``` ### Block Slots [#block-slots] Slots define content injection points: ```plaintext interface BlockSlot { name: string; // Slot name label?: string; // Display label description?: string; // Description defaultContent?: SchemaNode | SchemaNode[]; allowedTypes?: string[]; // Allowed component types maxChildren?: number; // Maximum children required?: boolean; // Is required? } ``` ## Complete Example [#complete-example] ```plaintext const cardBlock: BlockSchema = { type: 'block', // Metadata meta: { name: 'feature-card', label: 'Feature Card', description: 'A card component for highlighting product features', category: 'Marketing', icon: 'box', tags: ['card', 'feature', 'marketing', 'product'], author: 'ObjectUI Team', version: '1.0.0', license: 'MIT', repository: 'https://github.com/objectui/blocks', preview: '/blocks/feature-card-preview.png' }, // Variables (configurable props) variables: [ { name: 'title', label: 'Title', type: 'string', defaultValue: 'Feature Title', description: 'The card title text', required: true }, { name: 'description', label: 'Description', type: 'string', defaultValue: 'Feature description goes here', description: 'The card description text' }, { name: 'icon', label: 'Icon', type: 'string', defaultValue: 'star', description: 'Lucide icon name', enum: ['star', 'heart', 'check', 'zap', 'shield'] }, { name: 'variant', label: 'Variant', type: 'string', defaultValue: 'default', enum: ['default', 'outline', 'filled'] }, { name: 'showButton', label: 'Show Button', type: 'boolean', defaultValue: true, description: 'Show the action button' }, { name: 'buttonText', label: 'Button Text', type: 'string', defaultValue: 'Learn More' } ], // Slots (content injection points) slots: [ { name: 'header', label: 'Header Content', description: 'Optional header above the card', allowedTypes: ['text', 'image', 'icon'], maxChildren: 1 }, { name: 'content', label: 'Additional Content', description: 'Extra content below description', allowedTypes: ['text', 'list', 'div'] } ], // Template (component tree) template: { type: 'card', className: 'feature-card ${variant}', children: [ { type: 'div', className: 'card-icon', children: [ { type: 'icon', name: '${icon}', size: 48, className: 'text-primary' } ] }, { type: 'div', className: 'card-content', children: [ { type: 'text', variant: 'h3', value: '${title}', className: 'card-title' }, { type: 'text', value: '${description}', className: 'card-description' }, { type: 'slot', name: 'content' }, { type: 'button', label: '${buttonText}', variant: 'outline', visible: '${showButton}', className: 'card-button' } ] } ] }, editable: true }; ``` ## Using Blocks [#using-blocks] ### Block Instance [#block-instance] Use `BlockInstanceSchema` to instantiate a block: ```plaintext import type { BlockInstanceSchema } from '@object-ui/types'; const instance: BlockInstanceSchema = { type: 'block-instance', blockId: 'feature-card', // Override variable values values: { title: 'Fast Performance', description: 'Lightning-fast response times', icon: 'zap', variant: 'filled', showButton: true, buttonText: 'See Benchmarks' }, // Provide slot content slotContent: { content: [ { type: 'list', items: [ 'Sub-second page loads', 'Optimized bundle size', 'Edge caching' ] } ] } }; ``` ## Block Library [#block-library] Use `BlockLibrarySchema` to browse available blocks: ```plaintext import type { BlockLibrarySchema } from '@object-ui/types'; const library: BlockLibrarySchema = { type: 'block-library', apiEndpoint: '/api/blocks', category: 'Marketing', searchQuery: 'hero', showPremium: true, loading: false, blocks: [ { id: 'hero-section-1', meta: { name: 'hero-section', label: 'Hero Section', category: 'Marketing', // ... other metadata }, schema: heroBlock, installs: 1234, rating: 4.8, ratingCount: 156 } ], onInstall: 'handleInstallBlock', onPreview: 'handlePreviewBlock' }; ``` ## Block Editor [#block-editor] Use `BlockEditorSchema` to create/edit blocks: ```plaintext import type { BlockEditorSchema } from '@object-ui/types'; const editor: BlockEditorSchema = { type: 'block-editor', block: cardBlock, showVariables: true, showSlots: true, showTemplate: true, showPreview: true, onSave: 'handleSaveBlock', onCancel: 'handleCancel' }; ``` ## Marketplace Example [#marketplace-example] ```plaintext const marketplace: BlockLibrarySchema = { type: 'block-library', apiEndpoint: 'https://blocks.objectui.com/api', blocks: [ { id: 'pricing-table-pro', meta: { name: 'pricing-table-pro', label: 'Pricing Table Pro', description: 'Professional pricing comparison table', category: 'Marketing', icon: 'dollar-sign', tags: ['pricing', 'saas', 'comparison'], author: 'ObjectUI Team', version: '2.1.0', license: 'MIT', premium: false, preview: 'https://blocks.objectui.com/previews/pricing-table.png' }, schema: { /* block schema */ }, installs: 5420, rating: 4.9, ratingCount: 234, updatedAt: '2024-01-15T10:00:00Z' }, { id: 'testimonial-slider', meta: { name: 'testimonial-slider', label: 'Testimonial Slider', description: 'Animated testimonial carousel', category: 'Marketing', icon: 'Quote', tags: ['testimonial', 'slider', 'social-proof'], author: 'Community', version: '1.3.0', premium: true }, schema: { /* block schema */ }, installs: 2150, rating: 4.7, ratingCount: 89 } ] }; ``` ## Runtime Validation [#runtime-validation] ```plaintext import { BlockSchema } from '@object-ui/types/zod'; const result = BlockSchema.safeParse(myBlock); if (result.success) { console.log('Valid block configuration'); } else { console.error('Validation errors:', result.error); } ``` ## Best Practices [#best-practices] 1. **Clear naming** - Use descriptive names for blocks, variables, and slots 2. **Provide defaults** - Set sensible default values for all variables 3. **Document thoroughly** - Add descriptions for variables and slots 4. **Use enums for choices** - Limit options with enum arrays 5. **Version carefully** - Use semantic versioning (1.0.0) 6. **Test variations** - Try different variable combinations 7. **Add previews** - Include preview images for marketplace listings 8. **Tag appropriately** - Use relevant tags for discoverability ## Use Cases [#use-cases] ### Landing Page Components [#landing-page-components] * Hero sections * Feature grids * Pricing tables * Testimonial carousels * Call-to-action blocks ### Dashboard Widgets [#dashboard-widgets] * Metric cards * Chart containers * Data tables * Status indicators ### Form Sections [#form-sections] * Multi-step wizards * Address forms * Payment forms * Survey sections ### Content Blocks [#content-blocks] * Blog post templates * Documentation sections * FAQ accordions * Timeline components ## Related [#related] * [Component Registry](/docs/guide/component-registry) - Registering components * [Schema Rendering](/docs/guide/schema-rendering) - Rendering blocks * [Plugins](/docs/guide/plugins) - Extending ObjectUI # Dashboard Blocks # Dashboard Blocks [#dashboard-blocks] Professional dashboard components for displaying metrics, statistics, and data visualizations. ## Stats Card [#stats-card] Display key metrics with trend indicators. ## Recent Activity [#recent-activity] Display recent user activities or transactions. ## Overview Dashboard [#overview-dashboard] Complete dashboard layout with multiple sections. ## Usage [#usage] Customize dashboard blocks for your needs: ### Add Real Data [#add-real-data] ```json { "type": "card", "dataSource": { "api": "/api/stats/revenue", "method": "GET" } } ``` ### Customize Colors [#customize-colors] ```json { "type": "text", "content": "+20.1%", "className": "text-emerald-500" } ``` ### Add Actions [#add-actions] ```json { "type": "button", "label": "View Details", "action": { "type": "navigate", "path": "/dashboard/details" } } ``` ## Next Steps [#next-steps] * Explore [Form Blocks](/docs/blocks/forms) * View [Marketing Blocks](/docs/blocks/marketing) * Learn about [Data Sources](/docs/guide/data-source) # E-commerce Blocks # E-commerce Blocks [#e-commerce-blocks] Professional e-commerce components for online stores and marketplaces. ## Product Card [#product-card] Product display with image, title, price, and actions. ## Product Grid [#product-grid] Multiple products in a responsive grid. ## Shopping Cart [#shopping-cart] Cart summary with items and checkout. ## Order Summary [#order-summary] Checkout order review. ## Usage [#usage] Customize e-commerce blocks for your store: ### Add Product Data [#add-product-data] ```json { "type": "card", "dataSource": { "api": "/api/products", "method": "GET" } } ``` ### Add to Cart Action [#add-to-cart-action] ```json { "type": "button", "label": "Add to Cart", "action": { "type": "submit", "endpoint": "/api/cart", "method": "POST" } } ``` ### Customize Pricing Display [#customize-pricing-display] ```json { "type": "text", "content": "$299.99", "className": "text-2xl font-bold text-primary" } ``` ## Next Steps [#next-steps] * Browse [Authentication Blocks](/docs/blocks/authentication) * Explore [Dashboard Blocks](/docs/blocks/dashboard) * Learn about [Actions](/docs/core/enhanced-actions) # Form Blocks # Form Blocks [#form-blocks] Professional form layouts for various use cases. All forms are accessible and include proper validation. ## Contact Form [#contact-form] Simple contact form with name, email, and message fields. ## Settings Form [#settings-form] User settings panel with various input types. ## Newsletter Signup [#newsletter-signup] Simple newsletter subscription form. ## Payment Form [#payment-form] Payment details form with card information. ## Usage [#usage] Customize form blocks for your application: ### Add Validation [#add-validation] ```json { "type": "input", "name": "email", "inputType": "email", "required": true, "validation": { "pattern": "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,}$", "message": "Please enter a valid email address" } } ``` ### Add Submit Action [#add-submit-action] ```json { "type": "button", "label": "Submit", "action": { "type": "submit", "endpoint": "/api/contact", "method": "POST" } } ``` ### Customize Layout [#customize-layout] ```json { "type": "div", "className": "grid grid-cols-1 md:grid-cols-2 gap-6" } ``` ## Next Steps [#next-steps] * Browse [Marketing Blocks](/docs/blocks/marketing) * Explore [E-commerce Blocks](/docs/blocks/ecommerce) * Learn about [Form Validation](/docs/guide/fields) # Building Blocks # Building Blocks for the Web [#building-blocks-for-the-web] Pre-built, production-ready UI blocks powered by ObjectUI's schema-driven architecture. Copy the JSON, paste into your application, and ship faster. ## What are Blocks? [#what-are-blocks] Blocks are complete, ready-to-use UI patterns built with ObjectUI components. Unlike individual components, blocks are composed sections like login forms, pricing tables, dashboard cards, and more—ready to be dropped into your application. ### Quick Preview [#quick-preview] ### Why Use Blocks? [#why-use-blocks] * 🚀 **Ship Faster** - Start with working examples instead of building from scratch * 📋 **Copy & Paste** - Simple JSON schemas you can copy directly into your project * 🎨 **Beautiful Design** - Built with Tailwind CSS and Shadcn UI * ♿️ **Accessible** - WCAG 2.1 AA compliant * 🌓 **Theme Ready** - Supports light and dark modes out of the box * 📱 **Responsive** - Mobile-first design that works everywhere ## Available Block Categories [#available-block-categories] ### [Authentication Blocks](/docs/blocks/authentication) [#authentication-blocks] Login forms, signup pages, password reset flows, and more. ### [Dashboard Blocks](/docs/blocks/dashboard) [#dashboard-blocks] Stats cards, metric displays, chart layouts for admin panels and dashboards. ### [Form Blocks](/docs/blocks/forms) [#form-blocks] Contact forms, multi-step wizards, settings panels, and profile editors. ### [Marketing Blocks](/docs/blocks/marketing) [#marketing-blocks] Pricing tables, feature grids, testimonials, CTAs, and hero sections. ### [E-commerce Blocks](/docs/blocks/ecommerce) [#e-commerce-blocks] Product cards, shopping carts, checkout flows, and order summaries. ## How to Use Blocks [#how-to-use-blocks] 1. **Browse** the block categories to find what you need 2. **Preview** the block in action with live examples 3. **Copy** the JSON schema from the code tab 4. **Paste** into your ObjectUI application 5. **Customize** by modifying the schema to match your needs ### Example: Using a Block [#example-using-a-block] All blocks are defined as JSON schemas. Here's how simple it is: ```tsx import { SchemaRenderer } from '@object-ui/react'; // Copy this JSON from any block const loginBlockSchema = { type: "card", className: "max-w-md mx-auto", children: [ // ... block schema here ] }; // Render it in your app function App() { return ; } ``` ## Customization [#customization] Every block is fully customizable: * **Styling**: Add or modify `className` properties with Tailwind classes * **Content**: Change text, labels, and placeholders * **Layout**: Adjust spacing, sizing, and arrangement * **Behavior**: Add actions, data sources, and expressions * **Themes**: Works automatically with your theme configuration ## Block Architecture [#block-architecture] All blocks follow ObjectUI best practices: * Built with composable components * Use semantic HTML structure * Follow accessibility guidelines * Leverage Tailwind utility classes * Support responsive breakpoints * Include proper ARIA attributes ## Open Source & Free Forever [#open-source--free-forever] All blocks are open source under the MIT license. Use them in personal projects, commercial applications, or as inspiration for your own designs. ## Next Steps [#next-steps] * Browse [Authentication Blocks](/docs/blocks/authentication) * Explore [Dashboard Blocks](/docs/blocks/dashboard) * Check out [Form Blocks](/docs/blocks/forms) * View [Marketing Blocks](/docs/blocks/marketing) * Discover [E-commerce Blocks](/docs/blocks/ecommerce) ## Contributing [#contributing] Have a block idea? We'd love to see it! Check out our [contribution guidelines](https://github.com/objectstack-ai/objectui/blob/main/CONTRIBUTING.md) to submit your own blocks. # Marketing Blocks # Marketing Blocks [#marketing-blocks] Professional marketing components for landing pages and promotional content. ## Pricing Cards [#pricing-cards] Responsive pricing table with three tiers. ## Feature Grid [#feature-grid] Showcase product features in a grid layout. ## CTA Section [#cta-section] Call-to-action section for conversions. ## Testimonials [#testimonials] Customer testimonials and social proof. ## Usage [#usage] Customize marketing blocks for your campaigns: ### Update Colors [#update-colors] ```json { "type": "card", "className": "bg-gradient-to-r from-blue-600 to-purple-600" } ``` ### Add Analytics [#add-analytics] ```json { "type": "button", "label": "Get Started", "action": { "type": "analytics", "event": "cta_clicked" } } ``` ### Customize Content [#customize-content] ```json { "type": "text", "content": "Your custom headline here", "className": "text-4xl font-bold" } ``` ## Next Steps [#next-steps] * Browse [E-commerce Blocks](/docs/blocks/ecommerce) * Explore [Authentication Blocks](/docs/blocks/authentication) * Learn about [Theming](/docs/guide/theming) # Application Schema (AppSchema) # Application Schema [#application-schema] The `AppSchema` defines the top-level configuration for your entire ObjectUI application, including navigation menus, branding, layout strategies, and global actions. ## Overview [#overview] AppSchema provides a declarative way to configure: * **Navigation menus** - Hierarchical menu structures with icons and badges * **Branding** - Logo, title, favicon * **Layout strategies** - Sidebar, header, or empty layout * **Global actions** - User menu, global toolbar buttons ## Interactive Examples [#interactive-examples] ### Navigation Menu Preview [#navigation-menu-preview] ### Application Header Bar [#application-header-bar] ## Basic Usage [#basic-usage] ```plaintext import type { AppSchema } from '@object-ui/types'; const app: AppSchema = { type: 'app', name: 'my-crm', title: 'My CRM Application', logo: '/logo.svg', favicon: '/favicon.ico', layout: 'sidebar', menu: [ { type: 'item', label: 'Dashboard', icon: 'layout-dashboard', path: '/dashboard' } ], actions: [ { type: 'user', label: 'John Doe', avatar: '/avatar.jpg' } ] }; ``` ## Properties [#properties] ### Basic Configuration [#basic-configuration] | Property | Type | Description | | ------------- | -------- | ------------------------------------ | | `type` | `'app'` | Component type identifier (required) | | `name` | `string` | Application system identifier | | `title` | `string` | Display title shown in browser | | `description` | `string` | Application description | | `logo` | `string` | Logo URL or icon name | | `favicon` | `string` | Favicon URL | ### Layout Configuration [#layout-configuration] | Property | Type | Default | Description | | -------- | ---------------------------------- | ----------- | ---------------------- | | `layout` | `'sidebar' \| 'header' \| 'empty'` | `'sidebar'` | Global layout strategy | **Layout Options:** * **`sidebar`** - Standard admin layout with left sidebar navigation * **`header`** - Top navigation bar only * **`empty`** - No layout, pages handle their own structure ### Navigation Menu [#navigation-menu] The `menu` property accepts an array of `MenuItem` objects: ```plaintext interface MenuItem { type?: 'item' | 'group' | 'separator'; label?: string; icon?: string; // Lucide icon name path?: string; // Route path href?: string; // External link children?: MenuItem[]; // Submenu items badge?: string | number; hidden?: boolean | string; // Visibility condition } ``` #### Menu Types [#menu-types] **Item** - Single navigation link ```json { "type": "item", "label": "Dashboard", "icon": "layout-dashboard", "path": "/dashboard", "badge": "New" } ``` **Group** - Collapsible menu group ```json { "type": "group", "label": "Sales", "icon": "dollar-sign", "children": [ { "type": "item", "label": "Leads", "path": "/leads" }, { "type": "item", "label": "Deals", "path": "/deals" } ] } ``` **Separator** - Visual separator ```json { "type": "separator" } ``` ### Global Actions [#global-actions] The `actions` property defines global toolbar buttons: ```plaintext interface AppAction { type: 'button' | 'dropdown' | 'user'; label?: string; icon?: string; onClick?: string; avatar?: string; // For type='user' description?: string; // For type='user' items?: MenuItem[]; // For type='dropdown' or 'user' shortcut?: string; // Keyboard shortcut variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'; size?: 'default' | 'sm' | 'lg' | 'icon'; } ``` ## Complete Example [#complete-example] ```plaintext const crm: AppSchema = { type: 'app', name: 'acme-crm', title: 'Acme CRM', description: 'Customer Relationship Management System', logo: '/acme-logo.svg', favicon: '/favicon.ico', layout: 'sidebar', menu: [ { type: 'item', label: 'Dashboard', icon: 'layout-dashboard', path: '/dashboard' }, { type: 'separator' }, { type: 'group', label: 'Sales', icon: 'dollar-sign', children: [ { type: 'item', label: 'Leads', icon: 'users', path: '/leads', badge: 12 }, { type: 'item', label: 'Opportunities', icon: 'target', path: '/opportunities' }, { type: 'item', label: 'Quotes', icon: 'file-text', path: '/quotes' } ] }, { type: 'group', label: 'Marketing', icon: 'megaphone', children: [ { type: 'item', label: 'Campaigns', path: '/campaigns' }, { type: 'item', label: 'Email Templates', path: '/templates' } ] }, { type: 'separator' }, { type: 'item', label: 'Settings', icon: 'settings', path: '/settings', hidden: '${user.role !== "admin"}' } ], actions: [ { type: 'button', label: 'Quick Actions', icon: 'zap', variant: 'outline', onClick: 'openQuickActions' }, { type: 'user', label: 'John Doe', avatar: '/avatars/john.jpg', description: 'john@acme.com', items: [ { type: 'item', label: 'Profile', icon: 'user', path: '/profile' }, { type: 'item', label: 'Settings', icon: 'settings', path: '/settings' }, { type: 'separator' }, { type: 'item', label: 'Logout', icon: 'log-out', path: '/logout' } ] } ] }; ``` ## Runtime Validation [#runtime-validation] Use Zod validation to ensure your app configuration is correct: ```plaintext import { AppSchema } from '@object-ui/types/zod'; const result = AppSchema.safeParse(myAppConfig); if (result.success) { console.log('Valid app configuration'); } else { console.error('Validation errors:', result.error); } ``` ## Conditional Navigation [#conditional-navigation] Use expression syntax to show/hide menu items based on user permissions: ```json { "type": "item", "label": "Admin Panel", "path": "/admin", "hidden": "${user.role !== 'admin'}" } ``` ## Use Cases [#use-cases] AppSchema is ideal for: * **Multi-page applications** - Configure complex application structures with multiple routes * **Admin dashboards** - Create comprehensive admin panels with role-based navigation * **CRM systems** - Build customer relationship management interfaces * **Internal tools** - Develop enterprise internal tools with centralized navigation * **SaaS products** - Configure multi-tenant applications with customizable layouts ## Best Practices [#best-practices] 1. **Use semantic icons** - Choose Lucide icons that clearly represent the section 2. **Group related items** - Use menu groups to organize navigation 3. **Limit top-level items** - Keep the main menu concise (5-7 items) 4. **Add badges for notifications** - Show counts or status indicators 5. **Hide admin features** - Use conditional visibility for role-based access 6. **Provide keyboard shortcuts** - Add shortcuts for frequently used actions ## Related [#related] * [Layout Schema](/docs/guide/layout) - Page layout configuration * [Navigation Components](/docs/components/basic/navigation-menu) - Navigation UI components * [Theme Schema](/docs/core/theme-schema) - Application theming # Enhanced Actions # Enhanced Actions [#enhanced-actions] ObjectUI's action system has been significantly enhanced to support complex workflows, including AJAX API calls, confirmation dialogs, action chaining, conditional execution, and comprehensive tracking. ## Overview [#overview] The enhanced `ActionSchema` provides: * **New action types**: `ajax`, `confirm`, `dialog` * **Action chaining**: Execute multiple actions sequentially or in parallel * **Conditional logic**: If/then/else execution based on data * **Callbacks**: Success and failure handlers * **Tracking**: Event logging and analytics * **Retry logic**: Automatic retry with configurable backoff ## Interactive Examples [#interactive-examples] ### Action Buttons [#action-buttons] ### Confirmation Dialog Pattern [#confirmation-dialog-pattern] ### Action Toolbar [#action-toolbar] ## Action Types [#action-types] ### Ajax Actions [#ajax-actions] Execute API calls with full request configuration: ```plaintext const ajaxAction: ActionSchema = { type: 'action', label: 'Load Data', actionType: 'ajax', api: '/api/users', method: 'GET', headers: { 'Authorization': 'Bearer token' }, data: { filter: 'active' }, onSuccess: { type: 'toast', message: 'Data loaded successfully' }, onFailure: { type: 'message', message: 'Failed to load data' } }; ``` **Ajax Properties:** * `api` - API endpoint URL * `method` - HTTP method (GET, POST, PUT, DELETE, PATCH) * `data` - Request body/payload * `headers` - Custom HTTP headers * `timeout` - Request timeout in milliseconds * `retry` - Retry configuration ### Confirm Actions [#confirm-actions] Show confirmation dialog before executing: ```plaintext const confirmAction: ActionSchema = { type: 'action', label: 'Delete Record', actionType: 'confirm', confirm: { title: 'Confirm Deletion', message: 'Are you sure you want to delete this record?', confirmText: 'Yes, Delete', cancelText: 'Cancel', confirmVariant: 'destructive' }, api: '/api/records/123', method: 'DELETE' }; ``` **Confirm Properties:** * `title` - Dialog title * `message` - Confirmation message * `confirmText` - Confirm button text * `cancelText` - Cancel button text * `confirmVariant` - Button style ### Dialog Actions [#dialog-actions] Open a modal or dialog: ```plaintext const dialogAction: ActionSchema = { type: 'action', label: 'Edit Details', actionType: 'dialog', dialog: { title: 'Edit Record', size: 'lg', content: { type: 'form', fields: [ { name: 'name', type: 'input', label: 'Name' }, { name: 'email', type: 'input', label: 'Email' } ] }, actions: [ { type: 'action', label: 'Save', actionType: 'ajax', api: '/api/records/123', method: 'PUT' } ] } }; ``` ## Action Chaining [#action-chaining] Execute multiple actions in sequence or parallel: ```plaintext const chainedAction: ActionSchema = { type: 'action', label: 'Process Order', actionType: 'ajax', api: '/api/orders/process', method: 'POST', chain: [ { type: 'action', label: 'Send Email', actionType: 'ajax', api: '/api/emails/send', method: 'POST' }, { type: 'action', label: 'Update Inventory', actionType: 'ajax', api: '/api/inventory/update', method: 'PUT' }, { type: 'action', label: 'Log Event', actionType: 'ajax', api: '/api/events/log', method: 'POST' } ], chainMode: 'sequential' // or 'parallel' }; ``` **Chain Modes:** * `sequential` - Execute actions one after another (default) * `parallel` - Execute all actions simultaneously ## Conditional Execution [#conditional-execution] Execute different actions based on conditions: ```plaintext const conditionalAction: ActionSchema = { type: 'action', label: 'Approve', actionType: 'button', condition: { expression: '${data.amount > 1000}', then: { type: 'action', label: 'Require Manager Approval', actionType: 'confirm', confirm: { title: 'Manager Approval Required', message: 'Amount exceeds $1000. Manager approval needed.' } }, else: { type: 'action', label: 'Auto Approve', actionType: 'ajax', api: '/api/approve', method: 'POST' } } }; ``` **Condition Properties:** * `expression` - JavaScript expression to evaluate * `then` - Action(s) to execute if true * `else` - Action(s) to execute if false ## Callbacks [#callbacks] Handle success and failure scenarios: ```plaintext const actionWithCallbacks: ActionSchema = { type: 'action', label: 'Submit', actionType: 'ajax', api: '/api/submit', method: 'POST', onSuccess: { type: 'toast', message: 'Submitted successfully!' }, onFailure: { type: 'dialog', dialog: { title: 'Submission Failed', content: { type: 'text', value: 'Please try again or contact support.' } } } }; ``` **Callback Types:** * `toast` - Show toast notification * `message` - Show message dialog * `redirect` - Navigate to URL * `reload` - Reload data * `ajax` - Execute another API call * `dialog` - Open dialog * `custom` - Custom handler ## Action Tracking [#action-tracking] Track actions for analytics: ```plaintext const trackedAction: ActionSchema = { type: 'action', label: 'Download Report', actionType: 'ajax', api: '/api/reports/download', tracking: { enabled: true, event: 'report_downloaded', metadata: { reportType: 'sales', format: 'pdf', dateRange: '2024-01' } } }; ``` ## Retry Logic [#retry-logic] Automatically retry failed requests: ```plaintext const retryAction: ActionSchema = { type: 'action', label: 'Submit', actionType: 'ajax', api: '/api/submit', method: 'POST', timeout: 30000, // 30 seconds retry: { maxAttempts: 3, delay: 1000 // 1 second between retries } }; ``` ## Complete Example [#complete-example] A comprehensive action combining multiple features: ```plaintext const complexAction: ActionSchema = { type: 'action', label: 'Process Order', icon: 'shopping-cart', variant: 'default', // Confirm before processing actionType: 'confirm', confirm: { title: 'Confirm Order', message: 'Process this order for ${data.customerName}?', confirmText: 'Process Order', confirmVariant: 'default' }, // Conditional logic condition: { expression: '${data.totalAmount > 1000}', then: { type: 'action', actionType: 'ajax', api: '/api/orders/process-premium', method: 'POST' }, else: { type: 'action', actionType: 'ajax', api: '/api/orders/process-standard', method: 'POST' } }, // Action chain chain: [ { type: 'action', label: 'Send Confirmation Email', actionType: 'ajax', api: '/api/emails/order-confirmation', method: 'POST', data: { orderId: '${data.id}', customerEmail: '${data.customerEmail}' } }, { type: 'action', label: 'Update Inventory', actionType: 'ajax', api: '/api/inventory/update', method: 'PUT', data: { items: '${data.items}' } }, { type: 'action', label: 'Create Invoice', actionType: 'ajax', api: '/api/invoices/create', method: 'POST' } ], chainMode: 'sequential', // Callbacks onSuccess: { type: 'toast', message: 'Order processed successfully!' }, onFailure: { type: 'dialog', dialog: { title: 'Order Processing Failed', content: { type: 'text', value: 'Unable to process order. Please try again.' } } }, // Tracking tracking: { enabled: true, event: 'order_processed', metadata: { source: 'web_app', amount: '${data.totalAmount}' } }, // Retry timeout: 60000, retry: { maxAttempts: 3, delay: 2000 }, // Post-action behavior reload: true, close: true, redirect: '/orders/success' }; ``` ## Runtime Validation [#runtime-validation] ```plaintext import { ActionSchema } from '@object-ui/types/zod'; const result = ActionSchema.safeParse(myAction); if (result.success) { console.log('Valid action configuration'); } else { console.error('Validation errors:', result.error); } ``` ## Use Cases [#use-cases] Enhanced Actions are ideal for: * **API integration** - Connect to backend services and external APIs * **Multi-step processes** - Execute complex workflows with multiple stages * **Form submissions** - Handle form data with validation and callbacks * **Confirmation dialogs** - Add safety checks for critical operations * **Event tracking** - Monitor user interactions for analytics * **Batch operations** - Process multiple items in sequence or parallel ## Best Practices [#best-practices] 1. **Use confirm for destructive actions** - Always confirm delete, archive, etc. 2. **Provide clear feedback** - Use callbacks to inform users of success/failure 3. **Chain related operations** - Group logically related API calls 4. **Track important events** - Enable tracking for business-critical actions 5. **Set appropriate timeouts** - Don't let users wait indefinitely 6. **Retry transient failures** - Use retry for network-related errors 7. **Keep chains short** - Long chains can be hard to debug ## Related [#related] * [Building a CRUD App](/docs/guide/building-crud-app) - CRUD operations with actions * [Form](/docs/components/form) - Form submission actions * [Data Source](/docs/guide/data-source) - API integration # Report Schema (ReportSchema) # Report Schema [#report-schema] The `ReportSchema` enables creating comprehensive data reports with field aggregation, multiple export formats, and automated scheduling. ## Overview [#overview] ReportSchema provides: * **Field aggregation** - Sum, average, count, min, max, distinct * **Export formats** - PDF, Excel, CSV, JSON, HTML * **Scheduled reports** - Daily, weekly, monthly, quarterly, yearly * **Email distribution** - Automatic report delivery * **Interactive builder** - Report configuration UI * **Data filtering** - Complex filter criteria ## Interactive Examples [#interactive-examples] ### Sales Report Header [#sales-report-header] ### Report Data Table [#report-data-table] ### Schedule Configuration Preview [#schedule-configuration-preview] ## Basic Usage [#basic-usage] ```plaintext import type { ReportSchema } from '@object-ui/types'; const salesReport: ReportSchema = { type: 'report', title: 'Monthly Sales Report', description: 'Sales performance analysis', fields: [ { name: 'total_sales', label: 'Total Sales', type: 'number', aggregation: 'sum', format: 'currency' }, { name: 'order_count', label: 'Orders', type: 'number', aggregation: 'count' } ], filters: [ { field: 'date', operator: 'between', values: ['2024-01-01', '2024-01-31'] } ], showExportButtons: true }; ``` ## Properties [#properties] ### Basic Configuration [#basic-configuration] | Property | Type | Description | | ------------- | ------------ | ------------------------------------ | | `type` | `'report'` | Component type identifier (required) | | `title` | `string` | Report title | | `description` | `string` | Report description | | `dataSource` | `DataSource` | Data source configuration | ### Report Fields [#report-fields] ```plaintext interface ReportField { name: string; // Field name label?: string; // Display label // Drives type-aware cell rendering. Auto-hydrated from the bound // object's ObjectField when the report has an `objectName`. type?: | 'string' | 'text' | 'number' | 'date' | 'datetime' | 'time' | 'boolean' | 'select' | 'multi_select' | 'status' | 'lookup' | 'reference' | 'master_detail' | 'email' | 'url' | 'phone' | 'currency' | 'percent' | 'image' | 'file' | 'user' | 'owner' | 'richtext' | 'html' | 'markdown' | 'json' | 'tags'; // Used when type is select / multi_select / status. options?: Array<{ value: string | number; label: string; color?: string }>; // Used when type is lookup / reference / master_detail. Enables // deep-links to the related record's detail page. referenceTo?: string; aggregation?: 'sum' | 'avg' | 'min' | 'max' | 'count' | 'distinct'; format?: string; // Display format showInSummary?: boolean; // Show in summary section sortOrder?: number; // Sort order // Legacy opt-in: render a plain string cell as a Badge. // For most cases prefer `type: 'select'` + `options`. renderAs?: 'badge' | 'text'; colorMap?: Record; // value → CSS class } ``` #### Type-aware rendering [#type-aware-rendering] The runtime maps `field.type` to a cell renderer from `@object-ui/fields`'s `getCellRenderer` registry. Examples: `select` becomes a coloured `Badge`, `lookup` becomes a deep link to the related record, `boolean` becomes ✓/✗, `email`/`url`/`phone` become `mailto:`/external/`tel:` links, `image` becomes a thumbnail. Any unknown type falls back to plain text. When the report binds an `objectName`, columns inherit `type`, `options`, `referenceTo`, and `label` from the corresponding `ObjectField` automatically — author-provided values always win. ### Aggregation Types [#aggregation-types] * **`sum`** - Total of all values * **`avg`** - Average value * **`min`** - Minimum value * **`max`** - Maximum value * **`count`** - Count of records * **`distinct`** - Count of unique values ### Filters [#filters] ```plaintext interface ReportFilter { field: string; operator: 'equals' | 'not_equals' | 'contains' | 'greater_than' | 'less_than' | 'between' | 'in' | 'not_in'; value?: any; values?: any[]; // For 'between' and 'in' operators } ``` ### Group By [#group-by] ```plaintext interface ReportGroupBy { field: string; label?: string; sort?: 'asc' | 'desc'; } ``` ## Report Sections [#report-sections] Define report structure with sections: ```plaintext interface ReportSection { type: 'header' | 'summary' | 'chart' | 'table' | 'text' | 'page-break'; title?: string; content?: SchemaNode | SchemaNode[]; chart?: ChartSchema; // For type='chart' columns?: ReportField[]; // For type='table' text?: string; // For type='text' visible?: boolean | string; // Visibility condition } ``` ## Export Configuration [#export-configuration] ```plaintext interface ReportExportConfig { format: 'pdf' | 'excel' | 'csv' | 'json' | 'html'; filename?: string; includeHeaders?: boolean; orientation?: 'portrait' | 'landscape'; // PDF only pageSize?: 'A4' | 'A3' | 'Letter' | 'Legal'; // PDF only options?: Record; } ``` ## Scheduling [#scheduling] ```plaintext interface ReportSchedule { enabled?: boolean; frequency?: 'once' | 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'; dayOfWeek?: number; // For weekly (0-6) dayOfMonth?: number; // For monthly (1-31) time?: string; // HH:mm format timezone?: string; recipients?: string[]; // Email addresses subject?: string; body?: string; formats?: ReportExportFormat[]; // Formats to attach } ``` ## Complete Example [#complete-example] ```plaintext const comprehensiveReport: ReportSchema = { type: 'report', title: 'Quarterly Sales Analysis', description: 'Comprehensive sales performance analysis by region and product', // Data source dataSource: { provider: 'api', read: { url: '/api/sales', method: 'GET' } }, // Report fields fields: [ { name: 'region', label: 'Region', type: 'string' }, { name: 'product', label: 'Product', type: 'string' }, { name: 'revenue', label: 'Revenue', type: 'number', aggregation: 'sum', format: 'currency', showInSummary: true }, { name: 'units_sold', label: 'Units Sold', type: 'number', aggregation: 'sum', showInSummary: true }, { name: 'avg_price', label: 'Average Price', type: 'number', aggregation: 'avg', format: 'currency' } ], // Filters filters: [ { field: 'date', operator: 'between', values: ['2024-01-01', '2024-03-31'] }, { field: 'status', operator: 'equals', value: 'completed' } ], // Grouping groupBy: [ { field: 'region', label: 'Region', sort: 'asc' }, { field: 'product', label: 'Product', sort: 'desc' } ], // Report sections sections: [ { type: 'header', title: 'Executive Summary' }, { type: 'summary', title: 'Key Metrics' }, { type: 'chart', title: 'Revenue Trend', chart: { type: 'chart', chartType: 'line', data: [], series: [ { name: 'Revenue', type: 'line', dataKey: 'revenue' } ] } }, { type: 'table', title: 'Detailed Breakdown', columns: [ { name: 'region', label: 'Region' }, { name: 'product', label: 'Product' }, { name: 'revenue', label: 'Revenue', aggregation: 'sum' }, { name: 'units_sold', label: 'Units', aggregation: 'sum' } ] }, { type: 'page-break' }, { type: 'text', text: 'Report generated on ${new Date().toLocaleDateString()}' } ], // Schedule configuration schedule: { enabled: true, frequency: 'monthly', dayOfMonth: 1, time: '09:00', timezone: 'America/New_York', recipients: [ 'sales-team@company.com', 'management@company.com' ], subject: 'Monthly Sales Report - ${date}', body: 'Please find attached the monthly sales report.', formats: ['pdf', 'excel'] }, // Export configuration defaultExportFormat: 'pdf', exportConfigs: { pdf: { format: 'pdf', filename: 'sales-report-${date}.pdf', orientation: 'landscape', pageSize: 'A4', includeHeaders: true }, excel: { format: 'excel', filename: 'sales-report-${date}.xlsx', includeHeaders: true }, csv: { format: 'csv', filename: 'sales-data-${date}.csv', includeHeaders: true } }, // UI options showExportButtons: true, showPrintButton: true, showScheduleButton: true, refreshInterval: 300 // Auto-refresh every 5 minutes }; ``` ## Report Builder [#report-builder] Use `ReportBuilderSchema` for interactive report creation: ```plaintext import type { ReportBuilderSchema } from '@object-ui/types'; const builder: ReportBuilderSchema = { type: 'report-builder', report: { // Initial report configuration }, dataSources: [ { provider: 'api', read: { url: '/api/sales' } }, { provider: 'api', read: { url: '/api/customers' } } ], availableFields: [ { name: 'revenue', label: 'Revenue', type: 'number' }, { name: 'units', label: 'Units Sold', type: 'number' } ], showPreview: true, onSave: 'handleSaveReport', onCancel: 'handleCancel' }; ``` ## Report Viewer [#report-viewer] Use `ReportViewerSchema` to display generated reports: ```plaintext import type { ReportViewerSchema } from '@object-ui/types'; const viewer: ReportViewerSchema = { type: 'report-viewer', report: salesReport, data: reportData, showToolbar: true, allowExport: true, allowPrint: true, loading: false }; ``` ## Runtime Validation [#runtime-validation] ```plaintext import { ReportSchema } from '@object-ui/types/zod'; const result = ReportSchema.safeParse(myReport); if (result.success) { console.log('Valid report configuration'); } else { console.error('Validation errors:', result.error); } ``` ## Use Cases [#use-cases] ReportSchema is perfect for: * **Analytics dashboards** - Display key business metrics and KPIs * **Business intelligence** - Generate insights from operational data * **Automated reporting** - Schedule regular reports for stakeholders * **Data exports** - Provide data in multiple formats (PDF, Excel, CSV) * **Compliance reporting** - Generate audit trails and regulatory reports * **Executive summaries** - Create high-level overviews for management ## Best Practices [#best-practices] 1. **Use meaningful aggregations** - Choose aggregation types that make sense for the data 2. **Limit field count** - Too many fields make reports hard to read 3. **Group logically** - Group by dimensions that provide insights 4. **Test exports** - Verify all export formats render correctly 5. **Set reasonable schedules** - Don't over-email recipients 6. **Include filters** - Allow users to customize date ranges 7. **Add summary sections** - Provide key metrics at the top ## Related [#related] * [Charts Plugin](/docs/plugins/plugin-charts) - Data visualization * [Data Table](/docs/components/complex/data-table) - Tabular data display * [Data Source](/docs/guide/data-source) - Data integration # SchemaRenderer The SchemaRenderer is the heart of ObjectUI. It takes a JSON schema and dynamically renders the appropriate React components based on the schema's `type` field. ## How It Works [#how-it-works] The SchemaRenderer uses the Component Registry to look up the appropriate component for each schema type and renders it with the provided props. ```plaintext import { SchemaRenderer } from '@object-ui/react'; ``` ## Basic Example [#basic-example] ## Nested Schemas [#nested-schemas] The SchemaRenderer automatically handles nested schemas: ## Schema Structure [#schema-structure] ```plaintext interface SchemaNode { type: string; // Component type (e.g., 'card', 'button', 'text') id?: string; // Unique identifier className?: string; // Tailwind CSS classes children?: SchemaNode | SchemaNode[]; // Child components props?: Record; // Component-specific props [key: string]: any; // Any other component props } ``` ## Error Handling [#error-handling] When an unknown component type is encountered, SchemaRenderer displays a helpful error message: ## Component Registry [#component-registry] The SchemaRenderer uses the Component Registry to resolve component types: ```plaintext import { ComponentRegistry } from '@object-ui/core'; // Register a custom component ComponentRegistry.register('my-widget', MyWidgetComponent); // Now you can use it in schemas ``` ## Usage in Applications [#usage-in-applications] ### Simple Rendering [#simple-rendering] ```plaintext import { SchemaRenderer } from '@object-ui/react'; function App() { const schema = { type: 'page', title: 'Dashboard', body: [ { type: 'text', content: 'Welcome to your dashboard' } ] }; return ; } ``` ### Dynamic Schemas [#dynamic-schemas] ```plaintext import { SchemaRenderer } from '@object-ui/react'; import { useState, useEffect } from 'react'; function DynamicPage() { const [schema, setSchema] = useState(null); useEffect(() => { // Fetch schema from API fetch('/api/page-schema') .then(res => res.json()) .then(setSchema); }, []); if (!schema) return
Loading...
; return ; } ``` ### With Props [#with-props] You can pass additional props to the rendered component: ```plaintext console.log('Clicked!')} className="extra-classes" /> ``` ## Data Attributes [#data-attributes] SchemaRenderer automatically adds data attributes for debugging: * `data-obj-id`: The schema's id field * `data-obj-type`: The schema's type field These can be used for debugging, testing, or styling: ```css [data-obj-type="card"] { /* Target all card components */ } ``` ## Advanced Features [#advanced-features] ### String Rendering [#string-rendering] If a schema is just a string, it's rendered as text: ```plaintext // Renders: Hello World ``` ### Null/Undefined Handling [#nullundefined-handling] Null or undefined schemas render nothing: ```plaintext // Renders: null ``` ## Related [#related] * [Component Registry](/docs/guide/component-registry) - Register custom components * [Schema Rendering Guide](/docs/guide/schema-rendering) - Deep dive into schema rendering # Theme Schema (ThemeSchema) # Theme Schema [#theme-schema] The `ThemeSchema` provides a comprehensive theming system for ObjectUI applications, supporting light/dark modes, custom color palettes, typography, and CSS variable integration. ## Overview [#overview] ThemeSchema enables: * **Multiple theme definitions** - Create and switch between custom themes * **Light/Dark modes** - Automatic mode switching with persistence * **Color palettes** - 20+ semantic colors for consistent design * **Typography system** - Font families, sizes, weights, and line heights * **Tailwind integration** - Direct integration with Tailwind CSS * **CSS variables** - Custom CSS variable support ## Interactive Examples [#interactive-examples] ### Color Palette Preview [#color-palette-preview] ### Theme-Aware Components [#theme-aware-components] ## Basic Usage [#basic-usage] ```plaintext import type { ThemeSchema } from '@object-ui/types'; const theme: ThemeSchema = { type: 'theme', mode: 'dark', themes: [ { name: 'professional', label: 'Professional', light: { primary: '#3b82f6', background: '#ffffff', foreground: '#0f172a' }, dark: { primary: '#60a5fa', background: '#0f172a', foreground: '#f1f5f9' } } ], activeTheme: 'professional', allowSwitching: true, persistPreference: true }; ``` ## Properties [#properties] ### Theme Configuration [#theme-configuration] | Property | Type | Default | Description | | ------------------- | ------------------------------- | --------- | ------------------------------------- | | `type` | `'theme'` | - | Component type identifier (required) | | `mode` | `'light' \| 'dark' \| 'system'` | `'light'` | Current theme mode | | `themes` | `ThemeDefinition[]` | - | Available theme definitions | | `activeTheme` | `string` | - | Currently active theme name | | `allowSwitching` | `boolean` | `false` | Allow users to switch themes | | `persistPreference` | `boolean` | `false` | Save theme preference to localStorage | | `storageKey` | `string` | `'theme'` | Storage key for persisting theme | ## Theme Definition [#theme-definition] ```plaintext interface ThemeDefinition { name: string; // Theme identifier label?: string; // Display name light?: ColorPalette; // Light mode colors dark?: ColorPalette; // Dark mode colors typography?: Typography; spacing?: SpacingScale; radius?: BorderRadius; cssVariables?: Record; tailwind?: Record; } ``` ## Color Palette [#color-palette] Semantic color tokens for consistent design: ```plaintext interface ColorPalette { // Brand colors primary?: string; secondary?: string; accent?: string; // Base colors background?: string; foreground?: string; muted?: string; mutedForeground?: string; // Component colors card?: string; cardForeground?: string; popover?: string; popoverForeground?: string; // UI elements border?: string; input?: string; ring?: string; // Status colors success?: string; warning?: string; destructive?: string; info?: string; } ``` ### Example Color Palette [#example-color-palette] ```json { "light": { "primary": "#3b82f6", "secondary": "#64748b", "accent": "#8b5cf6", "background": "#ffffff", "foreground": "#0f172a", "muted": "#f1f5f9", "mutedForeground": "#64748b", "border": "#e2e8f0", "input": "#e2e8f0", "ring": "#3b82f6", "success": "#10b981", "warning": "#f59e0b", "destructive": "#ef4444", "info": "#3b82f6" }, "dark": { "primary": "#60a5fa", "secondary": "#94a3b8", "accent": "#a78bfa", "background": "#0f172a", "foreground": "#f1f5f9", "muted": "#1e293b", "mutedForeground": "#94a3b8", "border": "#334155", "input": "#334155", "ring": "#60a5fa", "success": "#34d399", "warning": "#fbbf24", "destructive": "#f87171", "info": "#60a5fa" } } ``` ## Typography System [#typography-system] ```plaintext interface Typography { fontSans?: string[]; // Sans-serif font stack fontSerif?: string[]; // Serif font stack fontMono?: string[]; // Monospace font stack fontSize?: number; // Base font size (rem) lineHeight?: number; // Base line height headingWeight?: number; // Font weight for headings bodyWeight?: number; // Font weight for body text } ``` ### Example Typography [#example-typography] ```json { "typography": { "fontSans": ["Inter", "system-ui", "sans-serif"], "fontSerif": ["Merriweather", "Georgia", "serif"], "fontMono": ["JetBrains Mono", "monospace"], "fontSize": 16, "lineHeight": 1.5, "headingWeight": 600, "bodyWeight": 400 } } ``` ## Spacing Scale [#spacing-scale] ```plaintext interface SpacingScale { base?: number; // Base spacing unit (rem) scale?: Record; // Custom spacing values } ``` ## Border Radius [#border-radius] ```plaintext interface BorderRadius { sm?: string; default?: string; md?: string; lg?: string; xl?: string; } ``` ## Complete Theme Example [#complete-theme-example] ```plaintext const professionalTheme: ThemeSchema = { type: 'theme', mode: 'system', themes: [ { name: 'professional', label: 'Professional', light: { primary: '#3b82f6', secondary: '#64748b', accent: '#8b5cf6', background: '#ffffff', foreground: '#0f172a', muted: '#f1f5f9', mutedForeground: '#64748b', card: '#ffffff', cardForeground: '#0f172a', popover: '#ffffff', popoverForeground: '#0f172a', border: '#e2e8f0', input: '#e2e8f0', ring: '#3b82f6', success: '#10b981', warning: '#f59e0b', destructive: '#ef4444', info: '#3b82f6' }, dark: { primary: '#60a5fa', secondary: '#94a3b8', accent: '#a78bfa', background: '#0f172a', foreground: '#f1f5f9', muted: '#1e293b', mutedForeground: '#94a3b8', card: '#1e293b', cardForeground: '#f1f5f9', popover: '#1e293b', popoverForeground: '#f1f5f9', border: '#334155', input: '#334155', ring: '#60a5fa', success: '#34d399', warning: '#fbbf24', destructive: '#f87171', info: '#60a5fa' }, typography: { fontSans: ['Inter', 'system-ui', 'sans-serif'], fontSize: 16, lineHeight: 1.5, headingWeight: 600, bodyWeight: 400 }, spacing: { base: 0.25, scale: { '0': '0', '1': '0.25rem', '2': '0.5rem', '4': '1rem', '8': '2rem' } }, radius: { sm: '0.25rem', default: '0.5rem', md: '0.75rem', lg: '1rem', xl: '1.5rem' }, cssVariables: { '--header-height': '4rem', '--sidebar-width': '16rem' } } ], activeTheme: 'professional', allowSwitching: true, persistPreference: true, storageKey: 'app-theme' }; ``` ## Theme Switcher [#theme-switcher] Use `ThemeSwitcherSchema` to add a theme switcher UI: ```plaintext import type { ThemeSwitcherSchema } from '@object-ui/types'; const switcher: ThemeSwitcherSchema = { type: 'theme-switcher', variant: 'dropdown', showMode: true, // Show light/dark mode toggle showThemes: true, // Show theme selector lightIcon: 'Sun', darkIcon: 'Moon' }; ``` ### Switcher Variants [#switcher-variants] * **`dropdown`** - Dropdown menu with theme options * **`toggle`** - Simple light/dark toggle button * **`buttons`** - Button group with all options ## Theme Preview [#theme-preview] Use `ThemePreviewSchema` to preview themes: ```plaintext import type { ThemePreviewSchema } from '@object-ui/types'; const preview: ThemePreviewSchema = { type: 'theme-preview', theme: professionalTheme.themes[0], mode: 'light', showColors: true, showTypography: true, showComponents: true }; ``` ## Runtime Validation [#runtime-validation] ```plaintext import { ThemeSchema } from '@object-ui/types/zod'; const result = ThemeSchema.safeParse(myTheme); if (result.success) { console.log('Valid theme configuration'); } else { console.error('Validation errors:', result.error); } ``` ## Use Cases [#use-cases] ThemeSchema is perfect for: * **Brand consistency** - Maintain consistent visual identity across applications * **White-labeling** - Enable multi-tenant applications with custom branding * **Accessibility** - Provide light/dark modes for user preference * **Design systems** - Implement comprehensive design tokens * **A/B testing** - Test different color schemes and typography ## Best Practices [#best-practices] 1. **Use semantic colors** - Stick to the semantic color tokens for consistency 2. **Test both modes** - Always test light and dark modes 3. **Maintain contrast** - Ensure sufficient color contrast for accessibility 4. **Limit custom themes** - Offer 2-3 well-designed themes max 5. **Respect system preferences** - Use `mode: 'system'` by default 6. **Persist preferences** - Enable `persistPreference` for better UX ## Related [#related] * [App Schema](/docs/core/app-schema) - Application configuration * [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) - MDN documentation * [Tailwind Theming](https://tailwindcss.com/docs/theme) - Tailwind CSS theming guide # AutoNumber Field The AutoNumber Field component displays auto-generated sequence numbers. This is a read-only field where the value is automatically generated by the backend when records are created. ## Basic Usage [#basic-usage] ## Custom Format [#custom-format] ## Date-Based Format [#date-based-format] ## Field Schema [#field-schema] ```plaintext interface AutoNumberFieldSchema { type: 'auto_number'; name: string; // Field name/ID label?: string; // Field label value?: string | number; // Generated value (read-only) readonly: true; // Always read-only className?: string; // Additional CSS classes // AutoNumber Configuration format?: string; // Number format template starting_number?: number; // Starting sequence number } ``` ## Format Templates [#format-templates] Common format patterns: ### Simple Sequential [#simple-sequential] ```plaintext format: '{0000}' // 0001, 0002, 0003... format: '{00000}' // 00001, 00002, 00003... ``` ### With Prefix [#with-prefix] ```plaintext format: 'ORD-{0000}' // ORD-0001, ORD-0002... format: 'INV-{00000}' // INV-00001, INV-00002... format: 'CUST-{000}' // CUST-001, CUST-002... ``` ### Date-Based [#date-based] ```plaintext format: '{YYYY}-{0000}' // 2024-0001, 2024-0002... format: '{YY}{MM}-{000}' // 2403-001, 2403-002... format: 'ORD-{YYYYMMDD}-{00}' // ORD-20240315-01... ``` ### Mixed Format [#mixed-format] ```plaintext format: 'PO-{YYYY}-{MM}-{0000}' // PO-2024-03-0001 format: '{YY}Q{Q}-{000}' // 24Q1-001, 24Q1-002... ``` ## Format Placeholders [#format-placeholders] * `{0}`, `{00}`, `{000}`, etc. - Sequential number with padding * `{YYYY}` - Four-digit year (2024) * `{YY}` - Two-digit year (24) * `{MM}` - Two-digit month (03) * `{DD}` - Two-digit day (15) * `{Q}` - Quarter (1-4) ## Backend Implementation [#backend-implementation] AutoNumber values are generated on record creation: ```plaintext const generateAutoNumber = (format: string, sequence: number) => { const now = new Date(); return format .replace('{YYYY}', now.getFullYear().toString()) .replace('{YY}', now.getFullYear().toString().slice(-2)) .replace('{MM}', (now.getMonth() + 1).toString().padStart(2, '0')) .replace('{DD}', now.getDate().toString().padStart(2, '0')) .replace('{Q}', Math.ceil((now.getMonth() + 1) / 3).toString()) .replace(/\{0+\}/, (match) => { const padding = match.length - 2; return sequence.toString().padStart(padding, '0'); }); }; // Example usage generateAutoNumber('ORD-{YYYY}-{0000}', 42); // Returns: "ORD-2024-0042" ``` ## Sequence Management [#sequence-management] The backend maintains sequence counters: ```plaintext interface SequenceCounter { object: string; // Object name field: string; // Field name current_value: number; // Current sequence number prefix?: string; // Optional prefix for partitioning } // Increment sequence atomically const getNextSequence = async (object: string, field: string) => { return await db.transaction(async (tx) => { const counter = await tx.findOne('sequences', { object, field }); const nextValue = (counter?.current_value || 0) + 1; await tx.upsert('sequences', { object, field }, { current_value: nextValue }); return nextValue; }); }; ``` ## Use Cases [#use-cases] * **Order Management**: Order numbers, PO numbers * **Invoicing**: Invoice IDs, receipt numbers * **Ticketing**: Support ticket IDs, case numbers * **Customer Management**: Customer IDs, account numbers * **Inventory**: SKU numbers, serial numbers * **Document Management**: Document IDs, revision numbers ## Best Practices [#best-practices] 1. **Choose appropriate padding**: Use enough digits for expected volume 2. **Include year for long-running systems**: Helps with archival and partitioning 3. **Use meaningful prefixes**: Makes numbers self-documenting 4. **Don't expose internal IDs**: Use auto-numbers for user-facing identifiers 5. **Consider reset policies**: Decide if/when sequences reset (yearly, monthly, etc.) # Boolean Field The Boolean Field component provides a switch or checkbox input for collecting true/false boolean values. ## Basic Usage [#basic-usage] ## With Description [#with-description] ## Default Value [#default-value] ## Field Schema [#field-schema] ```plaintext interface BooleanFieldSchema { type: 'boolean'; name: string; // Field name/ID label?: string; // Field label description?: string; // Helper text value?: boolean; // Default value required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes } ``` ## Use Cases [#use-cases] * **Feature Toggles**: Enable/disable features * **Preferences**: User settings and preferences * **Permissions**: Access control flags * **Status**: Active/inactive, published/draft states * **Agreements**: Terms acceptance, consent flags ## Cell Renderer [#cell-renderer] In tables/grids, boolean values are displayed as badges: ```plaintext import { BooleanCellRenderer } from '@object-ui/fields'; // Renders: // ✓ True (green badge) // ✗ False (gray badge) ``` ## Styling [#styling] The boolean field uses the Switch component from Shadcn UI, providing: * Smooth animations * Accessible keyboard navigation * Focus states * Disabled states # Currency Field The Currency Field component provides a formatted currency input with proper locale formatting and currency symbol display. ## Basic Usage [#basic-usage] ## Different Currencies [#different-currencies] ## Field Schema [#field-schema] ```plaintext interface CurrencyFieldSchema { type: 'currency'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: number; // Default value currency?: string; // Currency code (default: 'USD') required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Validation min?: number; // Minimum value max?: number; // Maximum value } ``` ## Supported Currencies [#supported-currencies] * **USD**: US Dollar ($) * **EUR**: Euro (€) * **GBP**: British Pound (£) * **JPY**: Japanese Yen (¥) * And all other ISO 4217 currency codes ## Cell Renderer [#cell-renderer] In tables/grids, currency values are formatted with the appropriate symbol: ```plaintext import { CurrencyCellRenderer } from '@object-ui/fields'; // Renders: $1,234.56 or €1.234,56 based on locale ``` ## Use Cases [#use-cases] * **Pricing**: Product prices, service costs * **Financial Data**: Revenue, expenses, budgets * **Transactions**: Payment amounts, invoice totals * **Salaries**: Compensation amounts # Date Field The Date Field component provides a date picker for selecting dates and optionally times. ## Basic Usage [#basic-usage] ## With Default Value [#with-default-value] ## Field Schema [#field-schema] ```plaintext interface DateFieldSchema { type: 'date' | 'datetime'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: string | Date; // Default value (ISO string or Date) required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Validation min?: string | Date; // Minimum date max?: string | Date; // Maximum date format?: string; // Display format } ``` ## Date Formats [#date-formats] The date field supports various display formats: * **Short**: 12/31/2024 * **Medium**: Dec 31, 2024 (default) * **Long**: December 31, 2024 * **Full**: Monday, December 31, 2024 ## Use Cases [#use-cases] * **Birthdates**: User birth dates * **Deadlines**: Task or project deadlines * **Events**: Event start/end dates * **Appointments**: Meeting or appointment scheduling * **Releases**: Product or content release dates ## Cell Renderer [#cell-renderer] In tables/grids, dates are formatted consistently: ```plaintext import { DateCellRenderer } from '@object-ui/fields'; // Renders: Dec 31, 2024 // Or: Dec 31, 2024 2:30 PM (for datetime) ``` ## DateTime Variant [#datetime-variant] For collecting both date and time: ```plaintext { type: 'datetime', name: 'appointment', label: 'Appointment Time' } ``` # DateTime Field The DateTime Field component provides a combined date and time input for collecting both date and time information in a single field. ## Basic Usage [#basic-usage] ## With Default Value [#with-default-value] ## Required Field [#required-field] ## Read-Only [#read-only] ## Field Schema [#field-schema] ```plaintext interface DateTimeFieldSchema { type: 'datetime'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: string; // Default value (ISO 8601 format) required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Validation format?: string; // Display format min_date?: string | Date; // Minimum date/time max_date?: string | Date; // Maximum date/time } ``` ## Date Format [#date-format] The datetime field stores values in ISO 8601 format: `YYYY-MM-DDTHH:mm` Example: `2024-03-15T14:30` represents March 15, 2024 at 2:30 PM ## Cell Renderer [#cell-renderer] When used in data tables or grids: ```plaintext import { DateTimeCellRenderer } from '@object-ui/fields'; // Renders: Mar 15, 2024, 02:30 PM ``` ## Use Cases [#use-cases] * **Event Scheduling**: Meeting times, appointments * **Timestamps**: Order placed, task deadline * **Booking Systems**: Reservation date and time * **Notifications**: Scheduled notification time # Email Field The Email Field component provides a text input with built-in email validation and formatting. ## Basic Usage [#basic-usage] ## Required Email [#required-email] ## Field Schema [#field-schema] ```plaintext interface EmailFieldSchema { type: 'email'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: string; // Default value required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes } ``` ## Validation [#validation] The email field automatically validates: * Proper email format ([user@domain.com](mailto:user@domain.com)) * Presence of @ symbol * Valid domain structure * No spaces or invalid characters ## Cell Renderer [#cell-renderer] In tables/grids, email addresses are clickable links: ```plaintext import { EmailCellRenderer } from '@object-ui/fields'; // Renders as: user@example.com // Clicking opens default email client ``` ## Use Cases [#use-cases] * **User Registration**: Account email addresses * **Contact Forms**: Customer contact information * **Support**: Support ticket emails * **Notifications**: Email notification addresses * **Team Members**: Team member email addresses ## Features [#features] * **Autocomplete**: Browser email autocomplete * **Validation**: Real-time email format validation * **Clickable Links**: Email links in read-only mode * **Accessible**: Proper ARIA labels and keyboard navigation # File Field The File Field component provides a file upload interface with support for multiple files, file type filtering, and file metadata display. ## Basic Usage [#basic-usage] ## Multiple Files [#multiple-files] ## File Type Restrictions [#file-type-restrictions] ## Field Schema [#field-schema] ```plaintext interface FileFieldSchema { type: 'file'; name: string; // Field name/ID label?: string; // Field label value?: FileMetadata | FileMetadata[]; // Current file(s) required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // File Options multiple?: boolean; // Allow multiple files accept?: string[]; // Accepted MIME types max_size?: number; // Max file size in bytes max_files?: number; // Max number of files } interface FileMetadata { name: string; // File name original_name?: string; // Original file name size?: number; // File size in bytes mime_type?: string; // MIME type url?: string; // File URL } ``` ## Accepted File Types [#accepted-file-types] Common MIME type examples: ```plaintext // Documents accept: ['application/pdf', 'application/msword'] // Images accept: ['image/png', 'image/jpeg', 'image/gif'] // Archives accept: ['application/zip', 'application/x-rar'] // Spreadsheets accept: ['application/vnd.ms-excel', 'text/csv'] // Wildcards accept: ['image/*'] // All images ``` ## Features [#features] * **File List Display**: Shows uploaded files with names and sizes * **Remove Files**: Individual file removal with X button * **Size Display**: File sizes shown in KB/MB * **Upload Button**: Clear upload interface * **Multiple Selection**: Support for batch file uploads ## Cell Renderer [#cell-renderer] In tables/grids, displays file count: ```plaintext import { FileCellRenderer } from '@object-ui/fields'; // Renders: "3 files" or "document.pdf" ``` ## Integration Notes [#integration-notes] This component creates object URLs for file preview. Actual file upload requires backend integration: ```plaintext // Frontend creates preview const fileData = { name: 'document.pdf', size: 1024000, mime_type: 'application/pdf', url: URL.createObjectURL(file) }; // Backend handles actual upload const uploadToServer = async (file) => { const formData = new FormData(); formData.append('file', file); const response = await fetch('/api/upload', { method: 'POST', body: formData }); return response.json(); }; ``` ## Use Cases [#use-cases] * **Document Management**: PDF uploads, contracts * **File Attachments**: Email attachments, support tickets * **Media Libraries**: Asset uploads, resources * **Data Import**: CSV files, batch uploads # Formula Field The Formula Field component displays computed values calculated from other fields. This is a read-only field where the value is automatically calculated by the backend. ## Basic Usage [#basic-usage] ## Text Formula [#text-formula] ## Date Formula [#date-formula] ## Field Schema [#field-schema] ```plaintext interface FormulaFieldSchema { type: 'formula'; name: string; // Field name/ID label?: string; // Field label value?: any; // Computed value (read-only) readonly: true; // Always read-only className?: string; // Additional CSS classes // Formula Configuration formula?: string; // Formula expression return_type?: string; // Return type (text, number, boolean, date, currency) } ``` ## Return Types [#return-types] The formula field formats values based on return type: * **number**: Displays with decimal precision * **currency**: Displays with currency symbol * **boolean**: Displays as Yes/No * **date**: Displays formatted date * **text**: Displays as string ## Formula Examples [#formula-examples] Common formula patterns: ```plaintext // Arithmetic formula: 'price * quantity' formula: '(subtotal - discount) * tax_rate' // Text concatenation formula: 'first_name + " " + last_name' formula: 'city + ", " + state + " " + zip' // Conditional formula: 'IF(age >= 18, "Adult", "Minor")' formula: 'IF(status == "closed", completed_at, null)' // Date calculations formula: 'created_at + 7 days' formula: 'end_date - start_date' ``` ## Cell Renderer [#cell-renderer] In tables/grids, displays with monospace font: ```plaintext import { FormulaCellRenderer } from '@object-ui/fields'; // Renders computed value in monospace font ``` ## Backend Implementation [#backend-implementation] Formula fields are computed on the backend: ```plaintext // Example backend calculation const calculateFormula = (formula: string, record: any) => { // Parse and evaluate formula if (formula === 'quantity * price') { return record.quantity * record.price; } // Use expression parser for complex formulas return evaluateExpression(formula, record); }; ``` ## Use Cases [#use-cases] * **Calculations**: Totals, subtotals, tax amounts * **Aggregations**: Sum of related records * **Concatenations**: Full names, addresses * **Derived Values**: Age from birthdate, days until deadline * **Conditional Logic**: Status based on other fields # Grid Field The Grid Field component provides an inline table for managing related records or tabular data within a parent record. It displays data in rows and columns with optional editing capabilities. ## Basic Usage [#basic-usage] ## With Data [#with-data] ## Read-Only [#read-only] ## Field Schema [#field-schema] ```plaintext interface GridFieldSchema { type: 'grid'; name: string; // Field name/ID label?: string; // Field label value?: any[]; // Array of row objects required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Grid Options columns?: ColumnDefinition[]; // Column definitions } interface ColumnDefinition { name: string; // Column field name label: string; // Column header label type: string; // Field type (text, number, etc.) width?: string; // Column width editable?: boolean; // Is column editable required?: boolean; // Is column required [key: string]: any; // Additional field-specific props } ``` ## Column Types [#column-types] Columns can use any field type: ```plaintext columns: [ { name: 'name', label: 'Name', type: 'text', required: true }, { name: 'quantity', label: 'Qty', type: 'number', min: 1 }, { name: 'price', label: 'Price', type: 'currency', currency: 'USD' }, { name: 'date', label: 'Date', type: 'date' }, { name: 'status', label: 'Status', type: 'select', options: [...] }, { name: 'active', label: 'Active', type: 'boolean' }, { name: 'receipt', label: 'Receipt', type: 'file', accept: ['image/*', '.pdf'] } ] ``` ### File / image columns [#file--image-columns] A `file` column renders a real upload control inside the cell — a compact "Upload" button that opens the native file picker, with uploaded files shown as removable chips (image files show a thumbnail). This covers the common "attach a receipt per expense line" pattern without opening the per-row form. * `accept?: string[]` — restrict the picker (e.g. `['image/*', '.pdf']`). * `multiple?: boolean` — allow several files per cell. * Uploads go through the configured `UploadProvider` adapter, exactly like the full-size file field. When grid columns are auto-derived from a child object's schema (master-detail subforms), `file` / `image` / `avatar` fields map to `file` columns automatically — image-flavoured fields default to `accept: ['image/*']`. Because file/image/avatar now render in-grid, a child object with a *single* such field keeps the inline **grid** form factor by default (the smart `inlineEdit` heuristic no longer forces a per-row form for one attachment column). Only a truly form-only field (textarea / rich text / JSON / location) or **several** rich fields tips the default to the per-row form; an explicit `inlineEdit: 'grid' | 'form'` always wins. ## Data Format [#data-format] Grid data is stored as an array of objects: ```plaintext const gridValue = [ { product: 'Item 1', quantity: 2, price: 29.99 }, { product: 'Item 2', quantity: 1, price: 49.99 }, { product: 'Item 3', quantity: 5, price: 9.99 } ]; ``` ## Common Patterns [#common-patterns] ### Invoice Line Items [#invoice-line-items] ```plaintext { type: 'grid', name: 'line_items', label: 'Line Items', columns: [ { name: 'description', label: 'Description', type: 'text', required: true }, { name: 'quantity', label: 'Quantity', type: 'number', min: 1, required: true }, { name: 'unit_price', label: 'Unit Price', type: 'currency', required: true }, { name: 'amount', label: 'Amount', type: 'currency', readonly: true } ] } ``` ### Order Details [#order-details] ```plaintext { type: 'grid', name: 'order_details', label: 'Order Details', columns: [ { name: 'sku', label: 'SKU', type: 'text' }, { name: 'product', label: 'Product', type: 'lookup', reference_to: 'products' }, { name: 'quantity', label: 'Qty', type: 'number' }, { name: 'price', label: 'Price', type: 'currency' }, { name: 'discount', label: 'Discount', type: 'percent' }, { name: 'total', label: 'Total', type: 'currency', readonly: true } ] } ``` ### Task Checklist [#task-checklist] ```plaintext { type: 'grid', name: 'tasks', label: 'Tasks', columns: [ { name: 'task', label: 'Task', type: 'text', required: true }, { name: 'assigned_to', label: 'Assigned To', type: 'user' }, { name: 'due_date', label: 'Due Date', type: 'date' }, { name: 'completed', label: 'Done', type: 'boolean' } ] } ``` ## Features [#features] * **Table Display**: Clean tabular layout * **Pagination Preview**: Shows first 5 rows with "Showing X of Y" indicator * **Type-Specific Rendering**: Each column renders according to its type * **Read-Only Mode**: Full table view without editing * **Responsive**: Scrollable for many columns ## Cell Renderer [#cell-renderer] In tables/grids, displays row count: ```plaintext import { GridCellRenderer } from '@object-ui/fields'; // Renders: "5 rows" ``` ## Full Grid Functionality [#full-grid-functionality] For advanced grid features, use the grid plugin (`@object-ui/plugin-grid`): ```plaintext // Basic inline grid (simple display) { type: 'grid', name: 'items', columns: [...] } // Advanced grid with full features (requires plugin) { type: 'plugin:grid', bind: 'items', props: { columns: [...], editable: true, // Plugin feature: inline editing sortable: true, // Plugin feature: column sorting filterable: true, // Plugin feature: filtering pagination: { pageSize: 20 } // Plugin feature: pagination } } ``` **Note**: Features like `editable`, `sortable`, `filterable`, and advanced `pagination` are provided by the `@object-ui/plugin-grid` package, not the basic grid field. ## Use Cases [#use-cases] * **Invoice/Order Line Items**: Product lines, services * **Expense Reports**: Expense entries, receipts * **Time Tracking**: Time entries, work logs * **Inventory**: Stock items, materials * **Checklists**: Task lists, requirements * **Schedules**: Appointments, bookings * **Configurations**: Settings lists, parameters ## Backend Storage [#backend-storage] Grid data is typically stored as JSON: ```plaintext // Database column type: JSONB (PostgreSQL) interface OrderRecord { id: string; customer_id: string; line_items: Array<{ product: string; quantity: number; price: number; }>; } // Store in database const order = { customer_id: 'CUST-123', line_items: [ { product: 'Widget A', quantity: 2, price: 29.99 }, { product: 'Widget B', quantity: 1, price: 49.99 } ] }; await db.insert('orders', order); ``` ## Validation [#validation] Example validation for grid data: ```plaintext const validateGridData = (data: any[], columns: ColumnDefinition[]) => { const errors: string[] = []; data.forEach((row, index) => { columns.forEach(col => { // Check required columns if (col.required && !row[col.name]) { errors.push(`Row ${index + 1}: ${col.label} is required`); } // Validate by type if (col.type === 'number' && isNaN(row[col.name])) { errors.push(`Row ${index + 1}: ${col.label} must be a number`); } // Check min/max if (col.min !== undefined && row[col.name] < col.min) { errors.push(`Row ${index + 1}: ${col.label} must be >= ${col.min}`); } }); }); return errors; }; ``` ## Integration with Advanced Grid [#integration-with-advanced-grid] For full-featured grids, use the `@object-ui/plugin-grid` package which provides: * Inline editing * Add/remove rows * Sorting and filtering * Drag-and-drop reordering * Export functionality * Formula columns * Aggregation rows # Image Field The Image Field component provides an image upload interface with thumbnail previews, multiple image support, and visual file management. ## Basic Usage [#basic-usage] ## Multiple Images [#multiple-images] ## Field Schema [#field-schema] ```plaintext interface ImageFieldSchema { type: 'image'; name: string; // Field name/ID label?: string; // Field label value?: FileMetadata | FileMetadata[]; // Current image(s) required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Image Options multiple?: boolean; // Allow multiple images accept?: string[]; // Accepted image types max_size?: number; // Max file size in bytes max_files?: number; // Max number of images max_width?: number; // Max image width max_height?: number; // Max image height } ``` ## Accepted Image Types [#accepted-image-types] By default, accepts all common image formats: ```plaintext // Default: 'image/*' // Specific formats: accept: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'] ``` ## Features [#features] * **Thumbnail Grid**: Images displayed in 4-column grid * **Preview**: Visual thumbnails for uploaded images * **Hover to Remove**: Delete button appears on hover * **Batch Upload**: Multiple image selection * **Size Restrictions**: Configurable file size limits ## Cell Renderer [#cell-renderer] In tables/grids, displays image thumbnails: ```plaintext import { ImageCellRenderer } from '@object-ui/fields'; // Shows up to 3 thumbnails plus count if more ``` ## Image Optimization [#image-optimization] For best results, consider: ```plaintext { type: 'image', name: 'product_images', label: 'Product Photos', multiple: true, max_size: 5242880, // 5MB max_files: 10, max_width: 2048, max_height: 2048, accept: ['image/jpeg', 'image/png', 'image/webp'] } ``` ## Integration Notes [#integration-notes] This component creates object URLs for preview. For production use, implement server-side upload: ```plaintext const uploadImage = async (file: File) => { // Validate dimensions const img = new Image(); img.src = URL.createObjectURL(file); await new Promise((resolve) => { img.onload = resolve; }); if (img.width > maxWidth || img.height > maxHeight) { throw new Error('Image dimensions too large'); } // Upload to server const formData = new FormData(); formData.append('image', file); const response = await fetch('/api/images', { method: 'POST', body: formData }); return response.json(); }; ``` ## Use Cases [#use-cases] * **User Profiles**: Avatar/profile pictures * **Product Catalogs**: Product images, SKU photos * **Photo Galleries**: Image collections, portfolios * **Content Management**: Blog post images, media assets * **Real Estate**: Property photos, listings # Location Field The Location Field component provides an input for geographic coordinates, storing latitude and longitude as a structured object. ## Basic Usage [#basic-usage] ## With Default Value [#with-default-value] ## Read-Only [#read-only] ## Field Schema [#field-schema] ```plaintext interface LocationFieldSchema { type: 'location'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: LocationValue; // Default coordinates required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Map Options default_zoom?: number; // Default map zoom level } interface LocationValue { latitude: number; // -90 to 90 longitude: number; // -180 to 180 } ``` ## Data Format [#data-format] The location field stores coordinates as an object: ```plaintext { latitude: 37.7749, longitude: -122.4194 } ``` Input format: `latitude, longitude` * Example: `37.7749, -122.4194` ## Coordinate Ranges [#coordinate-ranges] * **Latitude**: -90 to 90 (negative = South, positive = North) * **Longitude**: -180 to 180 (negative = West, positive = East) Examples: * New York: `40.7128, -74.0060` * Tokyo: `35.6762, 139.6503` * Sydney: `-33.8688, 151.2093` * London: `51.5074, -0.1278` ## Use Cases [#use-cases] * **Store Locator**: Retail locations, branch offices * **Delivery Zones**: Service areas, delivery points * **Event Venues**: Conference locations, meeting points * **Asset Tracking**: Equipment locations, vehicle tracking * **Real Estate**: Property coordinates, land parcels ## Integration with Maps [#integration-with-maps] For full map functionality, consider integrating with map services: ```plaintext import { LocationField } from '@object-ui/fields'; // Basic coordinates input // With map visualization (using map plugin) { type: 'plugin:map', bind: 'location', props: { center: location, zoom: 12, markers: [location] } } ``` ## Validation [#validation] The field validates coordinate ranges: ```plaintext // Valid coordinates { latitude: 37.7749, longitude: -122.4194 } ✓ // Invalid - latitude out of range { latitude: 95, longitude: -122.4194 } ✗ // Invalid - longitude out of range { latitude: 37.7749, longitude: 200 } ✗ ``` # Lookup Field The Lookup Field component provides a reference field for creating relationships between objects and records. It supports dynamic data loading from a DataSource with debounced search, loading/error/empty states, keyboard navigation, and optional quick-create entry. ## Basic Usage [#basic-usage] ## Multiple References [#multiple-references] ## Field Schema [#field-schema] ```plaintext interface LookupFieldSchema { type: 'lookup' | 'master_detail'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: string | string[]; // Default value(s) multiple?: boolean; // Allow multiple selections required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Reference Configuration reference_to: string; // Referenced object/table name reference_field?: string; // Field to display (default: 'name') description_field?: string; // Secondary field shown below label id_field?: string; // ID field on records (default: '_id') options?: LookupOption[]; // Available options (if static) // Data Source (automatic via SchemaRendererContext, or explicit) // When a DataSource is available, the popup dynamically loads // records from the referenced object on open, with debounced search. dataSource?: DataSource; // Quick-create callback (shown when no results found) onCreateNew?: (searchQuery: string) => void; // === Record Picker Configuration (Enterprise) === // Columns to show in the Record Picker dialog table. // Accepts field names or { field, label, width } objects. // When omitted, auto-infers from reference_field. lookup_columns?: Array; // Custom page size for the Record Picker dialog (default: 10) lookup_page_size?: number; } interface LookupOption { label: string; // Display label value: string; // Record ID description?: string; // Secondary text below label _id?: string; // Alternative ID field name?: string; // Alternative label field } ``` ## Dynamic Data Source [#dynamic-data-source] When a `DataSource` is available (via `SchemaRendererContext`, explicit prop, or field config), the Lookup popup **automatically** fetches records from the referenced object: ```plaintext // Automatic — DataSource from SchemaRendererContext // (works out-of-the-box in ObjectForm, DrawerForm, etc.) { type: 'lookup', name: 'customer', label: 'Customer', reference_to: 'customers', reference_field: 'name', // Display field (default: 'name') description_field: 'industry', // Optional secondary field } ``` The popup will: 1. Fetch records via `dataSource.find(reference_to, { $top: 50 })` on open 2. Send `$search` queries with 300ms debounce as the user types 3. Show loading spinner, error state with retry, and empty state 4. Display "Showing X of Y" when more records exist than the page size 5. Show a **"Show All Results"** button (inside the popover) to open the full Record Picker dialog when total exceeds page size ## Browse All Button [#browse-all-button] Every Lookup field with a `dataSource` always renders a **"Browse All"** button (table icon) next to the quick-select trigger. This button opens the full **RecordPickerDialog** directly, regardless of dataset size — ensuring enterprise features like multi-column tables, sort/filter bar, and cell renderers are always discoverable. * Always visible when `dataSource` is configured * Opens the Record Picker dialog without needing to open the popover first * Keyboard accessible and screen-reader friendly (`aria-label="Browse all records"`) ## Record Picker Dialog (Enterprise) [#record-picker-dialog-enterprise] The full **RecordPickerDialog** can be opened in two ways: 1. **"Browse All" button** (table icon) — always visible next to the quick-select trigger 2. **"Show All Results"** link inside the popover — shown when total records exceed the page size ```plaintext // Configure the Record Picker with lookup_columns { type: 'lookup', name: 'order', label: 'Order', reference_to: 'orders', reference_field: 'order_number', description_field: 'customer_name', lookup_columns: [ { field: 'order_number', label: 'Order #' }, { field: 'customer_name', label: 'Customer' }, { field: 'total_amount', label: 'Amount' }, { field: 'status', label: 'Status' }, ], lookup_page_size: 15, } ``` The Record Picker dialog provides: * **Multi-column table** with configurable columns via `lookup_columns` * **Search** with debounced server-side querying * **Column sorting** via clickable headers (sends `$orderby` to DataSource) * **Pagination** with page-by-page navigation * **Keyboard navigation** — Arrow keys to move between rows, Enter/Space to select * **Single/Multi-select** with visual check indicators and confirmation flow * **Responsive layout** — Mobile-friendly width (95vw on small screens) * **Loading, error, and empty states** * Auto-inferred columns from `reference_field` when `lookup_columns` is not set ## Lookup vs Master-Detail [#lookup-vs-master-detail] * **Lookup**: Standard reference field, can be deleted independently * **Master-Detail**: Parent-child relationship, deleting parent deletes children ## Cell Renderer [#cell-renderer] In tables/grids, lookup values display the referenced record name: ```plaintext import { LookupCellRenderer } from '@object-ui/fields'; // Single value: Display name/label // Multiple values: Multiple chips/badges ``` ## Use Cases [#use-cases] * **Assignments**: Assign tasks to users * **Relationships**: Link related records * **Categories**: Reference to category objects * **Parent Records**: Master-detail relationships * **Team Members**: Multi-user references ## Features [#features] * **Two-Level Interaction**: Popover typeahead (Level 1) + full Record Picker dialog (Level 2) * **Record Picker Dialog**: Enterprise-grade table with multi-column, pagination, search, sorting * **Inline Popover**: Level 1 opens as anchored dropdown (non-modal) for fast typeahead * **Column Sorting**: Clickable column headers with `$orderby` server-side sort * **Dynamic DataSource Loading**: Automatically fetches records from referenced objects * **Search**: Debounced type-ahead search with `$search` parameter * **Multi-Select**: Support for multiple references with confirmation flow * **Keyboard Navigation**: Arrow keys to navigate rows, Enter to select in both levels * **Responsive**: Mobile-friendly width, adapts to screen size * **Loading/Error/Empty States**: Friendly feedback for all states * **Secondary Field Display**: Show description/subtitle per option * **Quick-Create Entry**: Optional "Create new" button when no results * **Configurable Columns**: `lookup_columns` for multi-column picker display * **Base Filters**: `lookup_filters` to restrict selectable records * **Pagination**: Page-by-page navigation in Record Picker dialog * **Backward Compatible**: Falls back to static options when no DataSource # Number Field The Number Field component provides a numeric input for collecting integer or decimal numbers with optional precision control. ## Basic Usage [#basic-usage] ## With Precision [#with-precision] ## With Min/Max [#with-minmax] ## Field Schema [#field-schema] ```plaintext interface NumberFieldSchema { type: 'number'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: number; // Default value required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Validation min?: number; // Minimum value max?: number; // Maximum value precision?: number; // Decimal places (default: 0) step?: number; // Increment/decrement step } ``` ## Use Cases [#use-cases] * **Quantities**: Order quantities, stock levels * **Measurements**: Dimensions, weights, distances * **Ratings**: Numeric ratings or scores * **Ages**: User age or duration values ## Cell Renderer [#cell-renderer] In tables/grids, numbers are displayed with tabular formatting: ```plaintext import { NumberCellRenderer } from '@object-ui/fields'; // Renders with precision and tabular-nums font // Example: 1,234.56 ``` # Object Field The Object Field component provides a JSON editor for storing and editing structured data objects. It validates JSON syntax and provides a textarea interface for complex data. ## Basic Usage [#basic-usage] ## With Schema [#with-schema] ## Nested Data [#nested-data] ## Read-Only [#read-only] ## Field Schema [#field-schema] ```plaintext interface ObjectFieldSchema { type: 'object'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: Record; // JSON object value required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Object Options schema?: Record; // Optional schema definition } ``` ## JSON Validation [#json-validation] The field validates JSON syntax in real-time: * **Valid JSON**: Updates the value immediately * **Invalid JSON**: Maintains current valid value, doesn't update * **Empty Input**: Sets value to `null` ## Data Format [#data-format] The object field stores data as parsed JSON: ```plaintext // Input (string) '{"name": "John", "age": 30}' // Stored (object) { name: "John", age: 30 } // Display (formatted) { "name": "John", "age": 30 } ``` ## Common Patterns [#common-patterns] ### Configuration Objects [#configuration-objects] ```plaintext { type: 'object', name: 'api_config', label: 'API Configuration', value: { endpoint: 'https://api.example.com', timeout: 5000, retries: 3, headers: { 'Content-Type': 'application/json' } } } ``` ### Metadata [#metadata] ```plaintext { type: 'object', name: 'custom_metadata', label: 'Custom Metadata', value: { tags: ['important', 'urgent'], priority: 'high', department: 'engineering' } } ``` ### Preferences [#preferences] ```plaintext { type: 'object', name: 'user_preferences', label: 'Preferences', value: { theme: 'dark', language: 'en', notifications: { email: true, push: false, sms: false } } } ``` ## Cell Renderer [#cell-renderer] In tables/grids, displays formatted JSON preview: ```plaintext import { ObjectCellRenderer } from '@object-ui/fields'; // Shows: [Object] or truncated JSON in read-only mode ``` ## Schema Validation [#schema-validation] For typed object fields, you can define a schema in two formats: **Simplified Format** (for documentation): ```plaintext { type: 'object', name: 'product_specs', label: 'Product Specifications', schema: { weight: 'number', dimensions: { width: 'number', height: 'number', depth: 'number' }, materials: 'array' } } ``` **JSON Schema Format** (for validation): ```plaintext { type: 'object', name: 'product_specs', label: 'Product Specifications', schema: { type: 'object', properties: { weight: { type: 'number' }, dimensions: { type: 'object', properties: { width: { type: 'number' }, height: { type: 'number' }, depth: { type: 'number' } } }, materials: { type: 'array' } } } } ``` ## Use Cases [#use-cases] * **Configuration**: Application settings, API configurations * **Metadata**: Custom properties, tags, attributes * **API Responses**: Storing API response data * **Preferences**: User preferences, feature flags * **Structured Data**: Any complex structured information * **JSON Storage**: Raw JSON data storage ## Best Practices [#best-practices] 1. **Use for Flexible Data**: When schema changes frequently 2. **Validate on Backend**: Always validate structure server-side 3. **Consider Alternatives**: Use typed fields when structure is fixed 4. **Document Schema**: Provide clear documentation for expected structure 5. **Size Limits**: Set reasonable size limits for JSON data ## Backend Validation [#backend-validation] Example backend validation: ```plaintext import Ajv from 'ajv'; const ajv = new Ajv(); const validateObjectField = (value: any, schema: any) => { if (!schema) return { valid: true }; const validate = ajv.compile(schema); const valid = validate(value); return { valid, errors: validate.errors }; }; // Example schema const configSchema = { type: 'object', properties: { api_key: { type: 'string', minLength: 1 }, timeout: { type: 'number', minimum: 0 }, enabled: { type: 'boolean' } }, required: ['api_key'] }; ``` # Password Field The Password Field component provides a secure text input for passwords with a toggle button to show/hide the password text. ## Basic Usage [#basic-usage] ## With Validation [#with-validation] ## Confirm Password [#confirm-password] ## Field Schema [#field-schema] ```plaintext interface PasswordFieldSchema { type: 'password'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: string; // Default value required?: boolean; // Is field required readonly?: boolean; // Read-only mode (shows ••••••) disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Validation min_length?: number; // Minimum character length max_length?: number; // Maximum character length } ``` ## Features [#features] * **Masked Input**: Password is hidden by default with bullet points (•) * **Toggle Visibility**: Eye icon button to show/hide password * **Secure Display**: Read-only mode always shows •••••••• * **Validation**: Built-in support for length requirements ## Security Best Practices [#security-best-practices] When using password fields: 1. **Never log passwords**: Don't log form values containing passwords 2. **Use HTTPS**: Always transmit over secure connections 3. **Validation**: Enforce minimum length and complexity requirements 4. **Storage**: Never store passwords in plain text on the backend 5. **Auto-complete**: Consider disabling auto-complete for sensitive passwords ## Use Cases [#use-cases] * **User Registration**: New account password * **Login Forms**: User authentication * **Password Change**: Updating existing passwords * **Security Settings**: API keys, tokens (when masked input is appropriate) # Percent Field The Percent Field component provides a percentage input that automatically converts between stored decimal values (0-1) and displayed percentage values (0-100). ## Basic Usage [#basic-usage] ## With Precision [#with-precision] ## Required Field [#required-field] ## Read-Only [#read-only] ## Field Schema [#field-schema] ```plaintext interface PercentFieldSchema { type: 'percent'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: number; // Default value (0-1 decimal) required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Validation precision?: number; // Decimal places (default: 2) min?: number; // Minimum value (0-1) max?: number; // Maximum value (0-1) } ``` ## Value Conversion [#value-conversion] The percent field handles automatic conversion: * **Stored Value**: Decimal between 0 and 1 (e.g., `0.75`) * **Display Value**: Percentage between 0 and 100 (e.g., `75%`) Example: ```plaintext // Stored in database: 0.85 // Displayed to user: 85% // User enters: 90 // Stored as: 0.90 ``` ## Cell Renderer [#cell-renderer] In tables/grids, values are formatted with percentage symbol: ```plaintext import { PercentCellRenderer } from '@object-ui/fields'; // Renders: 85.50% ``` ## Use Cases [#use-cases] * **Progress Tracking**: Task completion, project progress * **Analytics**: Conversion rates, success metrics * **Financial Data**: Interest rates, discount percentages * **Statistics**: Performance scores, efficiency ratings # Phone Field The Phone Field component provides a text input optimized for phone number entry with proper formatting. ## Basic Usage [#basic-usage] ## Required Phone [#required-phone] ## Field Schema [#field-schema] ```plaintext interface PhoneFieldSchema { type: 'phone'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: string; // Default value required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes } ``` ## Cell Renderer [#cell-renderer] In tables/grids, phone numbers are clickable tel: links: ```plaintext import { PhoneCellRenderer } from '@object-ui/fields'; // Renders as: +1 (555) 123-4567 // Clicking initiates a phone call on mobile devices ``` ## Use Cases [#use-cases] * **Contact Information**: Customer phone numbers * **Support**: Support line numbers * **Emergency Contacts**: Emergency contact numbers * **Delivery**: Delivery contact numbers * **Appointments**: Callback numbers ## Features [#features] * **Mobile Optimized**: Opens phone dialer on mobile * **Clickable Links**: tel: links in read-only mode * **International Support**: Supports international formats * **Accessible**: Proper input type for mobile keyboards # Rich Text Field The Rich Text Field component provides a WYSIWYG editor for creating formatted text content with markdown or HTML support. ## Basic Usage [#basic-usage] ## HTML Editor [#html-editor] ## Field Schema [#field-schema] ```plaintext interface RichTextFieldSchema { type: 'markdown' | 'html'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: string; // Default value rows?: number; // Editor height in rows required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Editor Configuration toolbar?: boolean; // Show formatting toolbar preview?: boolean; // Show preview panel minHeight?: number; // Minimum height in pixels maxHeight?: number; // Maximum height in pixels } ``` ## Supported Formats [#supported-formats] ### Markdown [#markdown] * **Headings**: `# H1`, `## H2`, `### H3` * **Bold/Italic**: `**bold**`, `*italic*` * **Lists**: Ordered and unordered * **Links**: `[text](url)` * **Images**: `![alt](url)` * **Code**: Inline and blocks * **Tables**: Markdown tables ### HTML [#html] * Full HTML editing support * Sanitization for security * Style preservation * Embedded media support ## Cell Renderer [#cell-renderer] In tables/grids, rich text is shown as plain text: ```plaintext import { TextCellRenderer } from '@object-ui/fields'; // Strips formatting, shows plain text only // Long content is truncated ``` ## Use Cases [#use-cases] * **Blog Posts**: Article content, blog posts * **Documentation**: Technical documentation * **Descriptions**: Product or feature descriptions * **Comments**: Rich formatted comments * **Emails**: Email templates, rich email content * **Pages**: CMS page content ## Features [#features] * **WYSIWYG**: What you see is what you get editing * **Markdown Support**: For lightweight formatting * **HTML Support**: For advanced formatting * **Preview Mode**: See formatted output * **Syntax Highlighting**: For code blocks * **Image Upload**: Embed images (when configured) * **Auto-save**: Prevent data loss ## Editor Modes [#editor-modes] The rich text field can operate in different modes: 1. **Markdown Mode** (`type: 'markdown'`) * Lightweight markup * Easy to write and read * Great for documentation 2. **HTML Mode** (`type: 'html'`) * Full HTML editing * Advanced formatting * Embedded content support # Select Field The Select Field component provides a dropdown for selecting one or more options from a predefined list. ## Basic Usage [#basic-usage] ## With Colors [#with-colors] ## Multiple Selection [#multiple-selection] ## Cascading & Role-Gated Options [#cascading--role-gated-options] An option can carry a `visibleWhen` CEL predicate — it is offered only when the predicate is TRUE. The predicate is evaluated against the **live record** plus **`current_user`**, the same engine and binding environment as a field-level `visibleWhen`. This single mechanism covers two needs: * **Cascading / dependent options** — narrow a child list by a parent field (country → province → city). * **Role / context gating** — offer an option only to certain users. Declare the sibling field(s) a select reacts to with `dependsOn`. While any is empty the control is **gated** (a "Select country first" hint) instead of showing an unfiltered list; once the parent changes, the list re-evaluates and any now-invalid selection is **cleared automatically** (no stale "China + California" pair). ### Cascading (country → province) [#cascading-country--province] ```json { "type": "form", "fields": [ { "name": "country", "label": "Country", "type": "select", "options": [ { "label": "China", "value": "cn" }, { "label": "United States", "value": "us" } ]}, { "name": "province", "label": "Province", "type": "select", "dependsOn": "country", "options": [ { "label": "Zhejiang", "value": "zj", "visibleWhen": "record.country == 'cn'" }, { "label": "Guangdong", "value": "gd", "visibleWhen": "record.country == 'cn'" }, { "label": "California", "value": "ca", "visibleWhen": "record.country == 'us'" }, { "label": "Texas", "value": "tx", "visibleWhen": "record.country == 'us'" } ]} ] } ``` Chain a third level (`city`, `dependsOn: "province"`) the same way — the gate and cascade-clear propagate down the chain. ### Role-gated option [#role-gated-option] ```json { "name": "visibility", "type": "select", "options": [ { "label": "Private (only me)", "value": "private" }, { "label": "My team", "value": "team" }, { "label": "Whole organization", "value": "org" }, { "label": "Public — external", "value": "public", "visibleWhen": "'admin' in current_user.positions" } ]} ``` > **Security — hiding is UX, not authorization.** A `visibleWhen` on an option > only removes it from the dropdown on the client; a determined caller can still > submit the value. When an option is gated for **access-control** reasons the > **server must also reject** writes of that value (the rule-validator evaluates > the picked value's `visibleWhen`). Use option `visibleWhen` for convenience and > cascades freely; for real authorization, pair it with server-side enforcement. ### When to use options vs. a lookup [#when-to-use-options-vs-a-lookup] `visibleWhen` options are for **small, static dictionaries** (category → subcategory, a handful of provinces). When the data is large, changes over time, or is shared across forms (real country/province/city tables, org units, product catalogs), model each level as a **`lookup`** with `depends_on` instead — the candidate query is filtered server-side and paginated. See [Lookup Field](/docs/fields/lookup). ## Field Schema [#field-schema] ```plaintext interface SelectFieldSchema { type: 'select'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: string | string[]; // Default value(s) multiple?: boolean; // Allow multiple selections required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Options options: SelectOption[]; // Available options // Cascading: sibling field(s) whose value drives this option list. While any // is empty the field is gated ("Select first"); it re-evaluates as // they change. Same knob dependent lookups use. dependsOn?: string | string[]; } interface SelectOption { label: string; // Display label value: string; // Option value color?: string; // Badge color (gray, red, blue, etc.) disabled?: boolean; // Disable specific option // Per-option visibility predicate (CEL). The option is offered only when TRUE, // evaluated against the live record + current_user (same engine/env as a // field-level visibleWhen). Omit = always available. visibleWhen?: string; } ``` ## Available Colors [#available-colors] * `gray` - Default neutral color * `red` - For errors, urgent items * `orange` - For warnings, high priority * `yellow` - For pending, attention needed * `green` - For success, completed * `blue` - For info, in progress * `indigo` - For special items * `purple` - For creative, design * `pink` - For featured items ## Cell Renderer [#cell-renderer] In tables/grids, select values are displayed as colored badges: ```plaintext import { SelectCellRenderer } from '@object-ui/fields'; // Single value: Colored badge // Multiple values: Multiple badges in a row ``` ## Use Cases [#use-cases] * **Status Fields**: Order status, task status * **Categories**: Product categories, content types * **Tags**: Multi-tag selection * **Priorities**: Task or ticket priorities * **Roles**: User roles or permissions # Summary Field The Summary Field component displays aggregated values from related records. This is a read-only field where the value is automatically calculated by the backend through rollup aggregations. ## Count Summary [#count-summary] ## Sum Summary [#sum-summary] ## Average Summary [#average-summary] ## Field Schema [#field-schema] ```plaintext interface SummaryFieldSchema { type: 'summary'; name: string; // Field name/ID label?: string; // Field label value?: number; // Computed value (read-only) readonly: true; // Always read-only className?: string; // Additional CSS classes // Summary Configuration summary_object?: string; // Related object name summary_field?: string; // Field to aggregate summary_type?: 'count' | 'sum' | 'avg' | 'min' | 'max'; } ``` ## Summary Types [#summary-types] * **count**: Count of related records * **sum**: Sum of numeric field values * **avg**: Average of numeric field values * **min**: Minimum value in the field * **max**: Maximum value in the field ## Examples [#examples] ### Count Related Records [#count-related-records] ```plaintext { type: 'summary', name: 'task_count', label: 'Open Tasks', summary_object: 'tasks', summary_field: 'id', summary_type: 'count' } ``` ### Sum Amounts [#sum-amounts] ```plaintext { type: 'summary', name: 'total_sales', label: 'Total Sales', summary_object: 'invoices', summary_field: 'amount', summary_type: 'sum' } ``` ### Average Value [#average-value] ```plaintext { type: 'summary', name: 'avg_response_time', label: 'Avg Response Time', summary_object: 'support_tickets', summary_field: 'response_time_hours', summary_type: 'avg' } ``` ### Min/Max Values [#minmax-values] ```plaintext { type: 'summary', name: 'highest_bid', label: 'Highest Bid', summary_object: 'bids', summary_field: 'amount', summary_type: 'max' } ``` ## Cell Renderer [#cell-renderer] In tables/grids, displays with tabular numbers: ```plaintext import { SummaryCellRenderer } from '@object-ui/fields'; // Renders: 15,750.50 (formatted with proper precision) ``` ## Backend Implementation [#backend-implementation] Summary fields are calculated through database aggregations: ```plaintext // Example backend aggregation const calculateSummary = async (config: SummaryConfig, parentId: string) => { const { summary_object, summary_field, summary_type } = config; switch (summary_type) { case 'count': return db.count(summary_object, { parent_id: parentId }); case 'sum': return db.sum(summary_object, summary_field, { parent_id: parentId }); case 'avg': return db.avg(summary_object, summary_field, { parent_id: parentId }); case 'min': return db.min(summary_object, summary_field, { parent_id: parentId }); case 'max': return db.max(summary_object, summary_field, { parent_id: parentId }); } }; ``` ## Use Cases [#use-cases] * **Order Management**: Total order value, order count * **CRM**: Number of contacts, total deal value * **Project Management**: Task count, total hours * **Analytics**: Average ratings, min/max values * **Financial**: Sum of expenses, average transaction size # Text Field The Text Field component provides a single-line text input for collecting basic text data from users. It's the most commonly used field type for names, titles, and short text entries. ## Basic Usage [#basic-usage] ## With Placeholder [#with-placeholder] ## Required Field [#required-field] ## Read-Only [#read-only] ## Field Schema [#field-schema] ```plaintext interface TextFieldSchema { type: 'text'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: string; // Default value required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Validation min_length?: number; // Minimum character length max_length?: number; // Maximum character length pattern?: string | RegExp; // Validation pattern } ``` ## Use Cases [#use-cases] * **User Names**: Collecting first names, last names, or usernames * **Titles**: Post titles, document names, or product names * **Short Text**: Any short text input that doesn't require multiple lines * **IDs**: Custom identifiers or reference numbers ## Cell Renderer [#cell-renderer] When used in data tables or grids, the text field is rendered as simple truncated text: ```plaintext import { TextCellRenderer } from '@object-ui/fields'; // Automatically used for type: 'text' in grids/tables // Displays value or '-' if empty ``` # TextArea Field The TextArea Field component provides a multi-line text input for collecting longer text content from users, such as descriptions, comments, or notes. ## Basic Usage [#basic-usage] ## Custom Rows [#custom-rows] ## Required Field [#required-field] ## Field Schema [#field-schema] ```plaintext interface TextAreaFieldSchema { type: 'textarea'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text rows?: number; // Number of visible rows (default: 3) value?: string; // Default value required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Validation min_length?: number; // Minimum character length max_length?: number; // Maximum character length } ``` ## Use Cases [#use-cases] * **Descriptions**: Product descriptions, bio information * **Comments**: User feedback, review comments * **Notes**: Meeting notes, task descriptions * **Messages**: Contact form messages, support tickets ## Cell Renderer [#cell-renderer] When displayed in tables or grids, long text is truncated: ```plaintext import { TextCellRenderer } from '@object-ui/fields'; // Same renderer as text field - truncates long content ``` # Time Field The Time Field component provides a time-only input for collecting hour and minute information without a date component. ## Basic Usage [#basic-usage] ## With Default Value [#with-default-value] ## Required Field [#required-field] ## Read-Only [#read-only] ## Field Schema [#field-schema] ```plaintext interface TimeFieldSchema { type: 'time'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: string; // Default value (HH:mm format) required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // Validation format?: string; // Display format } ``` ## Time Format [#time-format] The time field uses 24-hour format: `HH:mm` Examples: * `09:00` - 9:00 AM * `14:30` - 2:30 PM * `23:45` - 11:45 PM ## Use Cases [#use-cases] * **Operating Hours**: Business hours, shift times * **Scheduling**: Daily task times, recurring events * **Alarms**: Reminder times, notification schedules * **Time Tracking**: Work start/end times # URL Field The URL Field component provides a text input with URL validation and clickable link rendering. ## Basic Usage [#basic-usage] ## Required URL [#required-url] ## Field Schema [#field-schema] ```plaintext interface UrlFieldSchema { type: 'url'; name: string; // Field name/ID label?: string; // Field label placeholder?: string; // Placeholder text value?: string; // Default value required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes } ``` ## Validation [#validation] The URL field automatically validates: * Proper URL protocol (http\:// or https\://) * Valid domain structure * No invalid characters ## Cell Renderer [#cell-renderer] In tables/grids, URLs are clickable external links: ```plaintext import { UrlCellRenderer } from '@object-ui/fields'; // Renders as: https://example.com // Opens in new tab with security attributes ``` ## Use Cases [#use-cases] * **Websites**: Company or personal websites * **Social Media**: Social media profile links * **Documentation**: External documentation links * **Resources**: Reference materials, articles * **Portfolios**: Portfolio or project links ## Features [#features] * **Link Preview**: Clickable links in read-only mode * **New Tab**: External links open in new tab * **Security**: Proper rel="noopener noreferrer" attributes * **Validation**: Real-time URL format validation * **Accessible**: Proper ARIA labels for screen readers # User Field The User Field component provides a user selector for assigning users or owners to records. It displays user avatars and supports both single and multiple user selection. ## Basic Usage [#basic-usage] ## Multiple Users [#multiple-users] ## Owner Field [#owner-field] ## Field Schema [#field-schema] ```plaintext interface UserFieldSchema { type: 'user' | 'owner'; name: string; // Field name/ID label?: string; // Field label value?: User | User[]; // Selected user(s) required?: boolean; // Is field required readonly?: boolean; // Read-only mode disabled?: boolean; // Disabled state className?: string; // Additional CSS classes // User Options multiple?: boolean; // Allow multiple users } interface User { id: string; // User ID name?: string; // Display name username?: string; // Username email?: string; // Email address avatar?: string; // Avatar URL } ``` ## User vs Owner [#user-vs-owner] * **User Field**: Selectable user field for assignments, team members, etc. * **Owner Field**: Typically read-only, automatically set to the record creator ## Display Features [#display-features] * **Avatar Badges**: User avatars with initials if no image * **Color Coding**: Consistent colors for user avatars * **Multiple Display**: Shows up to 3 users, then "+N more" * **Hover Info**: User details on avatar hover ## Cell Renderer [#cell-renderer] In tables/grids, displays avatar with name: ```plaintext import { UserCellRenderer } from '@object-ui/fields'; // Single user: Avatar + name // Multiple users: Overlapping avatars + count ``` ## How it works [#how-it-works] `user` / `owner` fields are a **lookup specialized to the framework's `sys_user` object** — there is no custom user API to wire up. The `UserField` widget delegates to the shared lookup picker with the reference fixed to `sys_user`, reusing the same debounced search, record-picker dialog and id resolution as any lookup field: * The field stores the selected user's **id** (a foreign key to `sys_user`); `multiple: true` stores an array of ids. * A `dataSource` (provided by `SchemaRenderer` / the app shell) supplies the candidate search — the picker queries `sys_user` by name/email as you type. * On read, `$expand` resolves the id(s) to the full user record so cells and read-only views show the name/avatar (via `UserCellRenderer`). No custom user-management integration is required when a `dataSource` is present. ## Permission Patterns [#permission-patterns] Common permission configurations: ```plaintext // Record owner only { type: 'owner', name: 'owner', label: 'Owner', readonly: true, defaultValue: 'current_user' } // Assignable user { type: 'user', name: 'assigned_to', label: 'Assigned To', required: true } // Team members { type: 'user', name: 'collaborators', label: 'Collaborators', multiple: true } ``` ## Use Cases [#use-cases] * **Task Management**: Task assignee, reviewer * **CRM**: Account owner, opportunity owner * **Support**: Ticket assignee, support agent * **Project Management**: Project members, task owners * **Approval Workflows**: Approvers, reviewers * **Collaboration**: Document collaborators, editors ## Backend behavior [#backend-behavior] The platform handles the common cases natively — you generally don't write backend code for user fields: * **Owner stamping** — declare `defaultValue: 'current_user'` and the acting user's id is filled in at insert time (no create hook required). * **Display resolution** — request `expand=` and the stored id(s) resolve to the full `sys_user` record (name / avatar) for rendering. * **Ownership & permissions** — record ownership and row-level security continue to use the platform's `owner_id` convention and security rules. # Vector Field The Vector Field component displays vector embeddings used in AI/ML applications. This is a read-only field that shows a preview of the embedding values and dimensionality. ## Basic Usage [#basic-usage] ## Different Dimensions [#different-dimensions] ## Field Schema [#field-schema] ```plaintext interface VectorFieldSchema { type: 'vector'; name: string; // Field name/ID label?: string; // Field label value?: number[]; // Vector array readonly: true; // Always read-only className?: string; // Additional CSS classes // Vector Options dimensions?: number; // Vector dimensionality } ``` ## Display Format [#display-format] Vectors are displayed with a preview: ``` [0.1234, -0.5678, 0.9012...] (768D) ``` Shows: * First 3 values (formatted to 4 decimal places) * Total dimensionality in parentheses ## Common Embedding Dimensions [#common-embedding-dimensions] Different AI models use different embedding sizes: * **OpenAI text-embedding-3-small**: 512, 1536, or 3072 dimensions (configurable) * **OpenAI text-embedding-3-large**: 256, 1024, or 3072 dimensions (configurable) * **OpenAI text-embedding-ada-002**: 1536 dimensions (fixed) * **Sentence Transformers (BERT)**: 768 dimensions * **OpenAI CLIP**: 512 dimensions * **Word2Vec**: 100-300 dimensions * **Custom Models**: Variable dimensions ## Data Format [#data-format] Vectors are stored as arrays of floating-point numbers: ```plaintext // Example embedding const embedding: number[] = [ 0.1234, -0.5678, 0.9012, 0.3456, -0.7890, // ... 763 more values for 768D embedding ]; ``` ## Cell Renderer [#cell-renderer] In tables/grids, displays compact preview: ```plaintext import { VectorCellRenderer } from '@object-ui/fields'; // Renders: [0.1234, -0.5678, 0.9012...] (768D) ``` ## Vector Operations [#vector-operations] Common operations with vector fields (performed on backend): ### Similarity Search [#similarity-search] ```plaintext // Find similar vectors using cosine similarity const findSimilar = async (queryVector: number[], limit: number = 10) => { return await db.raw(` SELECT id, 1 - (embedding <=> $1) as similarity FROM documents ORDER BY embedding <=> $1 LIMIT $2 `, [queryVector, limit]); }; ``` ### Distance Metrics [#distance-metrics] * **Cosine Similarity**: Measures angle between vectors * **Euclidean Distance**: Straight-line distance * **Dot Product**: Inner product of vectors ## Use Cases [#use-cases] * **Semantic Search**: Find similar documents, products, or content * **Recommendation Systems**: Product recommendations, content suggestions * **Text Analysis**: Document clustering, topic modeling * **Image Search**: Similar image finding, visual search * **Anomaly Detection**: Identify outliers in data * **AI Applications**: Any machine learning feature vectors ## Backend Implementation [#backend-implementation] Example vector storage and search: ```plaintext // PostgreSQL with pgvector extension import { Client } from 'pg'; // Create table with vector column const createTable = async () => { await db.raw(` CREATE TABLE IF NOT EXISTS documents ( id SERIAL PRIMARY KEY, content TEXT, embedding vector(768) ); CREATE INDEX ON documents USING ivfflat (embedding vector_cosine_ops); `); }; // Generate embedding (using OpenAI) const generateEmbedding = async (text: string): Promise => { const response = await openai.embeddings.create({ model: 'text-embedding-3-small', input: text }); return response.data[0].embedding; }; // Store document with embedding const storeDocument = async (content: string) => { const embedding = await generateEmbedding(content); return db.insert('documents', { content, embedding }); }; // Search similar documents const searchSimilar = async (query: string, limit = 10) => { const queryEmbedding = await generateEmbedding(query); return db.raw(` SELECT id, content, 1 - (embedding <=> $1::vector) as similarity FROM documents ORDER BY embedding <=> $1::vector LIMIT $2 `, [queryEmbedding, limit]); }; ``` ## Database Support [#database-support] Vector storage is supported by: * **PostgreSQL**: pgvector extension * **Pinecone**: Specialized vector database * **Weaviate**: Vector search engine * **Milvus**: Open-source vector database * **Qdrant**: Vector similarity search engine * **Chroma**: AI-native embedding database ## Performance Considerations [#performance-considerations] 1. **Indexing**: Use approximate nearest neighbor (ANN) indices 2. **Quantization**: Consider dimension reduction for storage 3. **Batch Operations**: Generate embeddings in batches 4. **Caching**: Cache frequently used embeddings 5. **Async Generation**: Generate embeddings asynchronously ## Example: Semantic Search [#example-semantic-search] ```plaintext // 1. Generate embedding for user query const query = "Find documentation about authentication"; const queryEmbedding = await generateEmbedding(query); // 2. Search for similar documents const results = await db.raw(` SELECT id, title, content, 1 - (embedding <=> $1::vector) as similarity FROM documents WHERE 1 - (embedding <=> $1::vector) > 0.7 ORDER BY embedding <=> $1::vector LIMIT 10 `, [queryEmbedding]); // 3. Display results with similarity scores results.forEach(doc => { console.log(`${doc.title} (${(doc.similarity * 100).toFixed(1)}% match)`); }); ``` # Agent Skills # Agent Skills [#agent-skills] ObjectUI ships an official **Agent Skill** — a structured bundle of rules, guides, and evals that teaches AI coding agents how to build pages, plugins, and integrations the *right* way (schema-first, no-touch zones respected, English-only, expression-aware). The skill is published on [skills.sh](https://skills.sh/objectstack-ai/objectui) and lives in [`skills/objectui/`](https://github.com/objectstack-ai/objectui/tree/main/skills/objectui) inside this repository. [![skills.sh](https://skills.sh/b/objectstack-ai/objectui)](https://skills.sh/objectstack-ai/objectui) ## Install [#install] One command, in your project root: ```bash npx skills add objectstack-ai/objectui ``` The `skills` CLI auto-detects which AI agent you're using and writes the skill to the location that agent reads from — for example: | Agent | Install location | | ---------------------------------------------------- | ---------------------------------- | | Claude Code | `.claude/skills/objectui/` | | GitHub Copilot | `.github/copilot/skills/objectui/` | | Cursor | `.cursor/skills/objectui/` | | Codex | `.codex/skills/objectui/` | | Windsurf, Gemini, Cline, Goose, Kilo, Droid, Trae, … | per-agent path | See [skills.sh/docs](https://skills.sh/docs) for the full, up-to-date list of supported agents. ## What you get [#what-you-get] Once installed the skill activates automatically whenever you describe ObjectUI work in chat. Nothing to import, nothing to configure. The skill is structured as a single entry point plus deep-dive guides: ``` skills/objectui/ ├── SKILL.md # Entry point — core principles, tech stack, scope ├── rules/ # Non-negotiable global constraints │ ├── protocol.md │ ├── styling.md │ ├── composition.md │ └── no-touch-zones.md ├── guides/ # Domain-specific deep dives, loaded on demand │ ├── architecture.md │ ├── page-builder.md │ ├── plugin-development.md │ ├── schema-expressions.md │ ├── data-integration.md │ ├── project-setup.md │ ├── testing.md │ ├── i18n.md │ ├── mobile.md │ ├── auth-permissions.md │ └── console-development.md └── evals/ # Machine-checkable prompts (one per guide) ``` When the agent picks up a task it: 1. Reads `SKILL.md` for core principles, scope boundaries, and the package map. 2. Loads the relevant `rules/*.md` so non-negotiables (Shadcn purity, expression syntax, layout composition) are respected. 3. Pulls the matching `guides/*.md` for the task at hand. ## What the skill enforces [#what-the-skill-enforces] The skill bakes the ObjectUI worldview into every answer your agent gives you: * **Schema-first**: page output is JSON for ``, not bespoke React. * **Shadcn-native aesthetics**: components stay in Tailwind + `cn()` + `cva`; no inline styles, no CSS-in-JS. * **Protocol agnostic**: data goes through the `DataSource` interface, not raw `fetch`/`axios`. * **Expression-aware**: `visible`, `hidden`, `disabled` use `${data.*}` / `${props.*}` templates, never raw JS. * **No-touch zones**: upstream Shadcn primitives in `packages/components/src/ui/**` are never edited — extensions live in `custom/` wrappers. * **English-only**: all generated component text, labels, comments, and docs are English. * **Scope discipline**: the skill explicitly defers backend/data-modelling questions to the `objectstack-*` skills, so it stays focused on the UI engine. Each rule has a machine-checkable eval under `evals/`, so regressions in agent behaviour are caught the same way regressions in code are. ## When *not* to use it [#when-not-to-use-it] If your question is purely about data modelling, kernel plugins, ObjectQL queries, CEL formulas, or server-side automation, prefer the matching `objectstack-*` skill. The ObjectUI skill's frontmatter explicitly defers those topics so multiple skills can coexist cleanly in your agent. ## Updating [#updating] To pull the latest version of the skill after we ship improvements: ```bash npx skills add objectstack-ai/objectui ``` The CLI is idempotent — running it again refreshes the local copy in place. ## Contributing [#contributing] The skill source lives in [`skills/objectui/`](https://github.com/objectstack-ai/objectui/tree/main/skills/objectui). PRs are welcome — see [`skills/objectui/README.md`](https://github.com/objectstack-ai/objectui/blob/main/skills/objectui/README.md) for the maintenance rules (guide ↔ eval naming, English-only, eval assertion format). # Architecture Overview # Architecture Overview [#architecture-overview] ObjectUI is a **Server-Driven UI (SDUI) engine** that transforms JSON schemas into fully interactive React interfaces built on Shadcn/Tailwind. This document covers the internal architecture, data flow, and extension points. ## The 3-Layer Architecture [#the-3-layer-architecture] ObjectUI enforces a strict separation across three layers. Each layer has clear constraints on what it may import and what it may contain. ``` ┌─────────────────────────────────────────────────────────────────────┐ │ Layer 1: @objectstack/spec ^4.0.4 (The Protocol) │ │ Pure TypeScript type definitions — 12 export modules │ │ ❌ No runtime code. No React. No dependencies. │ └──────────────────────────────┬──────────────────────────────────────┘ │ imports (never redefines) ┌──────────────────────────────▼──────────────────────────────────────┐ │ Layer 2: @object-ui/types (The Bridge) │ │ Re-exports spec types + ObjectUI-specific schemas │ │ ❌ No runtime code. Zero runtime dependencies. │ └──────────────────────────────┬──────────────────────────────────────┘ │ consumed by ┌──────────────────────────────▼──────────────────────────────────────┐ │ Layer 3: Implementations (The Runtime) │ │ core, react, components (91+), fields (35+), plugins, etc. │ └─────────────────────────────────────────────────────────────────────┘ ``` ### Layer 1 — `@objectstack/spec` (The Protocol) [#layer-1--objectstackspec-the-protocol] The upstream JSON specification for all ObjectStack products. ObjectUI **imports** these types but never redefines them. Located externally; consumed as `@objectstack/spec ^4.0.0`. ### Layer 2 — `@object-ui/types` (The Bridge) [#layer-2--object-uitypes-the-bridge] Lives in `packages/types/`. Re-exports spec types and adds ObjectUI-specific schemas (component schemas, widget props, field props). Has **zero** runtime dependencies — only type-level imports of `@objectstack/spec` and `zod` for validation schemas. Marked `sideEffects: false`. ### Layer 3 — Implementations (The Runtime) [#layer-3--implementations-the-runtime] All packages that ship runnable code: `core`, `react`, `components`, `fields`, `layout`, `i18n`, `auth`, `permissions`, and every `plugin-*` package. ## Package Dependency Graph [#package-dependency-graph] ``` @objectstack/spec │ ▼ @object-ui/types ◄─────────────────────────────────────┐ │ │ ▼ │ @object-ui/core │ │ (registry, evaluator, actions, validation) │ ▼ │ @object-ui/react ──────► @object-ui/i18n │ │ (SchemaRenderer, hooks, contexts) │ ├──────────────────────────────────────┐ │ ▼ ▼ │ @object-ui/components @object-ui/fields │ │ (91+ Shadcn atoms) (35+ inputs) │ ▼ ▼ │ @object-ui/layout @object-ui/plugin-* │ (AppShell, Header, (grid, kanban, charts, │ Sidebar, routing) dashboard, form, etc.) │ │ │ └─────────────────┘ (all packages import types) ``` **Strict rules:** | Package | May Import | Must NOT Import | | ------------ | -------------------------------- | ------------------------ | | `types` | `@objectstack/spec` | Any runtime package | | `core` | `types`, `lodash`, `zod` | React, any UI library | | `react` | `core`, `types`, `i18n` | Plugin packages directly | | `components` | `types` (for props) | `core`, `react` | | `fields` | `types` (for `FieldWidgetProps`) | `core` business logic | | `plugin-*` | Any package | Other plugins | ## Data Flow: Schema → Screen [#data-flow-schema--screen] The rendering pipeline transforms a JSON schema into a live React component tree: ``` JSON Schema (from API or static file) │ ▼ ┌─────────────┐ ┌──────────────────┐ │ SchemaRenderer │───►│ ExpressionEvaluator │ │ (packages/ │ │ Resolves ${...} │ │ react/src/) │ │ expressions │ └──────┬────────┘ └──────────────────┘ │ ▼ ┌─────────────────┐ │ ComponentRegistry │ ← registry.resolve(schema.type) │ (packages/core/ │ │ src/registry/) │ └──────┬──────────┘ │ ▼ ┌──────────────────┐ │ React Component │ ← Wrapped in ErrorBoundary │ (Button, Grid, │ with ARIA attributes │ Kanban, etc.) │ └──────────────────┘ ``` 1. **SchemaRenderer** (`packages/react/src/SchemaRenderer.tsx`) receives a JSON schema object. 2. It evaluates dynamic expressions via `ExpressionEvaluator` (`packages/core/src/evaluator/`). 3. It looks up the component type in the `ComponentRegistry` (`packages/core/src/registry/Registry.ts`). 4. The matched component renders with schema props, wrapped in a per-component `ErrorBoundary` with ARIA accessibility attributes (`aria-label`, `aria-describedby`, `role`). ## The Plugin System [#the-plugin-system] Plugins are self-contained packages that register heavy or complex views (grids, kanbans, charts). They follow a consistent pattern defined in `packages/core/src/registry/PluginSystem.ts`. ### Plugin Lifecycle [#plugin-lifecycle] ``` import 'plugin-kanban' │ ▼ PluginSystem.load(plugin) │ ├─ Check dependencies │ ├─ Prevent duplicate loading │ └─ Call plugin.register(scope) ▼ PluginScope.registerComponent(type, Component, meta) │ └─ Auto-prefixes with plugin namespace ▼ ComponentRegistry.register('kanban-ui', KanbanRenderer, { namespace: 'plugin-kanban', category: 'plugin' }) ``` ### Lazy Loading [#lazy-loading] Plugins use `React.lazy()` wrapped by `LazyPluginLoader` (`packages/react/src/LazyPluginLoader.tsx`): * **Retry logic**: 2 retries with 1-second delay by default * **Custom fallbacks**: Loading skeleton + error boundary * **Tree-shaking**: Plugins are code-split from the initial bundle ### Plugin Registration Pattern [#plugin-registration-pattern] Every plugin follows the same structure (see `packages/plugin-kanban/`, `packages/plugin-grid/`): ```typescript // 1. Lazy-load the heavy implementation const LazyKanban = React.lazy(() => import('./KanbanImpl')); // 2. Define a thin wrapper with Suspense const KanbanRenderer: React.FC = ({ schema }) => ( }> ); // 3. Register in the global registry ComponentRegistry.register('kanban-ui', KanbanRenderer, { namespace: 'plugin-kanban', label: 'Kanban Board', category: 'plugin', inputs: [/* schema config */] }); ``` Scaffold a new plugin with `npx @object-ui/create-plugin`. ## State Management [#state-management] ObjectUI uses **React Context** for all state management, with contexts scoped to specific concerns: ### Core Contexts (`packages/react/src/context/`) [#core-contexts-packagesreactsrccontext] | Context | Purpose | | ----------------------- | ----------------------------------------------------- | | `SchemaRendererContext` | Data scope + debug mode for the rendering tree | | `ActionContext` | Action runner instance for handling user interactions | | `ThemeContext` | Theme tokens and dark/light mode | | `NotificationContext` | Toast and notification system | | `DndContext` | Drag-and-drop state for sortable views | ### Domain Contexts (separate packages) [#domain-contexts-separate-packages] | Context | Package | Purpose | | ------------------- | ----------------------- | ----------------------- | | `AuthContext` | `packages/auth/` | Authentication state | | `PermissionContext` | `packages/permissions/` | RBAC/ABAC permissions | | `I18nProvider` | `packages/i18n/` | Locale and translations | Contexts are composed at the application root and consumed by plugins and components via hooks (e.g., `useActionRunner`, `useExpression`, `useViewData`). ## Expression Evaluation [#expression-evaluation] The expression engine lives in `packages/core/src/evaluator/` and powers dynamic schemas: ### Components [#components] * **`ExpressionEvaluator.ts`** — Main engine: parses and evaluates `${...}` template expressions. * **`ExpressionContext.ts`** — Variable scope: provides `data`, `user`, `params` to expressions. * **`ExpressionCache.ts`** — Caches parsed expressions for repeated evaluations. * **`FormulaFunctions.ts`** — Built-in functions available inside expressions. ### Expression Types [#expression-types] ```json { "visible": "${data.role === 'admin'}", "label": "Hello, ${data.user.name}!", "disabled": "${data.status !== 'draft'}", "className": "${data.priority === 'high' ? 'text-red-500' : 'text-gray-500'}" } ``` Expressions support: * **String interpolation**: `"Welcome, ${data.name}"` * **Conditionals**: `"${data.age > 18}"` * **Ternary operators**: `"${data.active ? 'Yes' : 'No'}"` * **Variable references**: `data.*`, `user.*`, `params.*` ## Action System [#action-system] The action system (`packages/core/src/actions/`) handles user interactions defined in schemas. ### ActionRunner (`ActionRunner.ts`) [#actionrunner-actionrunnerts] Executes action schemas and returns directives: ``` User Click → Schema Action │ ▼ ActionRunner.execute(action, context) │ ├─ Check confirmation dialog │ ├─ Evaluate conditions │ └─ Execute by action type ▼ ActionResult ├─ reload: boolean ├─ redirect: string ├─ modal: SchemaObject └─ toast: { message, type } ``` ### Supported Action Types [#supported-action-types] | Type | Description | | -------- | ------------------------------------ | | `script` | Execute inline JavaScript | | `url` | Navigate to a URL | | `api` | Make an API request (AJAX) | | `modal` | Open a modal with a nested schema | | `flow` | Execute a multi-step action sequence | ### TransactionManager (`TransactionManager.ts`) [#transactionmanager-transactionmanagerts] Wraps multi-step actions in transactions for consistency, supporting rollback on failure. ## Key Directories Reference [#key-directories-reference] ``` packages/ ├── types/src/ # Layer 2 — Pure type definitions ├── core/src/ │ ├── evaluator/ # Expression engine │ ├── registry/ # Component & plugin registries │ ├── actions/ # ActionRunner, TransactionManager │ ├── validation/ # Schema validation engine │ ├── adapters/ # Data source adapters (API, Value) │ ├── data-scope/ # DataScopeManager │ ├── query/ # Query AST (filtering/sorting) │ ├── theme/ # ThemeEngine │ └── builder/ # Schema builder utilities ├── react/src/ │ ├── SchemaRenderer.tsx │ ├── LazyPluginLoader.tsx │ ├── context/ # All React contexts │ └── hooks/ # 20+ hooks (useExpression, useActionRunner, etc.) ├── components/ # 91+ Shadcn UI atoms ├── fields/ # 35+ field renderers ├── layout/ # AppShell, Header, Sidebar ├── i18n/ # Internationalization ├── auth/ # AuthContext + providers ├── permissions/ # RBAC/ABAC └── plugin-*/ # 20 plugin packages ├── plugin-grid/ ├── plugin-kanban/ ├── plugin-charts/ ├── plugin-dashboard/ ├── plugin-form/ └── ... (15 more) ``` ## Further Reading [#further-reading] * [Schema Overview](/docs/guide/schema-overview) — JSON schema structure * [Schema Rendering](/docs/guide/schema-rendering) — How schemas become UI * [Component Registry](/docs/guide/component-registry) — Registering components * [Plugin Development](/docs/guide/plugin-development) — Building plugins * [Expressions](/docs/guide/expressions) — Expression syntax reference * [Theming](/docs/guide/theming) — Theme configuration # Architecture Overview # Architecture Overview [#architecture-overview] ObjectUI is a universal, server-driven UI (SDUI) engine built on React, Tailwind CSS, and Shadcn UI. This guide explains the core architecture and how all the pieces work together. ## Core Philosophy [#core-philosophy] ObjectUI follows three fundamental principles: 1. **JSON-First**: Every UI element is described as JSON metadata, not hardcoded React components 2. **Backend Agnostic**: Works with any backend system (ObjectStack, custom APIs, etc.) 3. **Component Library Quality**: Combines low-code speed with Shadcn/Tailwind design quality ## Architecture Layers [#architecture-layers] ``` ┌─────────────────────────────────────────────┐ │ JSON Schema (Protocol) │ ← Backend sends this ├─────────────────────────────────────────────┤ │ @object-ui/react (Renderer) │ ← Interprets schema ├─────────────────────────────────────────────┤ │ Component Registry + Field Registry │ ← Lookup system ├─────────────────────────────────────────────┤ │ @object-ui/components (UI Primitives) │ ← Buttons, Cards, etc. │ @object-ui/fields (Form Inputs) │ ← Text, Date, Select │ @object-ui/layout (Page Structure) │ ← AppShell, Sidebar │ @object-ui/plugin-* (Advanced Widgets) │ ← Grid, Charts, Kanban ├─────────────────────────────────────────────┤ │ Shadcn UI + Radix UI (Primitives) │ ← Accessible components │ Tailwind CSS (Styling) │ ← Utility-first CSS └─────────────────────────────────────────────┘ ``` ## Package Structure [#package-structure] ObjectUI is organized as a PNPM monorepo with clear separation of concerns: ### Core Packages [#core-packages] #### `@object-ui/types` [#object-uitypes] * **Role**: The Protocol * **Contains**: Pure TypeScript interfaces for JSON schemas * **Constraint**: ZERO dependencies, no React code * **Example**: `ComponentSchema`, `ActionSchema`, `FieldSchema` #### `@object-ui/core` [#object-uicore] * **Role**: The Engine * **Contains**: Schema validation, expression evaluation, registries * **Constraint**: No UI library dependencies, logic only * **Features**: * Expression engine (`visible: "${data.age > 18}"`) * Schema registry and validation * Event system #### `@object-ui/react` [#object-uireact] * **Role**: The Runtime * **Contains**: `SchemaRenderer` and React integration * **Purpose**: Transforms JSON schemas into live React components ### UI Packages [#ui-packages] #### `@object-ui/components` [#object-uicomponents] * **Role**: The Atoms * **Contains**: Shadcn primitives (Button, Badge, Card, Dialog, etc.) * **Constraint**: Pure UI, no business logic * **Style**: Tailwind CSS with `class-variance-authority` #### `@object-ui/fields` [#object-uifields] * **Role**: The Inputs * **Contains**: Standard field renderers (Text, Number, Select, Date, etc.) * **Implements**: `FieldWidgetProps` interface * **Purpose**: Reusable form inputs with consistent API #### `@object-ui/layout` [#object-uilayout] * **Role**: The Shell * **Contains**: Page structure components (AppShell, Page, Sidebar, Header) * **Purpose**: Routing-aware composition and app scaffolding ### Plugin Packages [#plugin-packages] Each plugin provides specialized, complex widgets: * `@object-ui/plugin-grid` - Data tables with ObjectStack integration * `@object-ui/plugin-kanban` - Kanban board view * `@object-ui/plugin-charts` - Recharts-based visualizations * `@object-ui/plugin-calendar` - Calendar and event views * `@object-ui/plugin-map` - Map visualization * `@object-ui/plugin-form` - Advanced forms * `@object-ui/plugin-editor` - Code editor (Monaco) * `@object-ui/plugin-markdown` - Markdown renderer * `@object-ui/plugin-gantt` - Gantt chart timeline * `@object-ui/plugin-timeline` - Event timeline * `@object-ui/plugin-dashboard` - Dashboard layouts * `@object-ui/plugin-chatbot` - Chat interface **Important**: Heavy dependencies (like Monaco, Recharts) are only allowed in plugin packages to keep the core bundle small. ### Utility Packages [#utility-packages] Development tools and integration utilities: * `@object-ui/cli` - Command-line tool for building apps from schemas * `@object-ui/create-plugin` - Interactive plugin scaffolder * `@object-ui/runner` - Universal runtime for testing and demos * `@object-ui/data-objectstack` - ObjectStack data backend adapter * `vscode-extension` - VS Code extension for schema development [Learn more about utilities →](/docs/utilities) ## How Schema Rendering Works [#how-schema-rendering-works] ### 1. JSON Schema Input [#1-json-schema-input] A backend system sends a JSON schema: ```json { "type": "card", "title": "Welcome", "body": { "type": "text", "value": "Hello, ${user.name}!" } } ``` ### 2. SchemaRenderer Processing [#2-schemarenderer-processing] The `SchemaRenderer` component: 1. Receives the schema + data context 2. Evaluates expressions (`${user.name}`) 3. Looks up the component type in the registry 4. Recursively renders child schemas 5. Handles events and state updates ```tsx import { SchemaRenderer } from '@object-ui/react' function App() { const data = { user: { name: "Alice" } } return } ``` ### 3. Component Registry Lookup [#3-component-registry-lookup] The registry maps type strings to React components: ```typescript // During app initialization ComponentRegistry.register('card', CardComponent) ComponentRegistry.register('text', TextComponent) // At runtime const Component = ComponentRegistry.get('card') // → CardComponent ``` ### 4. React Component Rendering [#4-react-component-rendering] The registered component renders with evaluated props: ```tsx ``` ## The Registry Pattern [#the-registry-pattern] ObjectUI uses two registry systems for extensibility: ### Component Registry [#component-registry] Maps schema types to React components: ```tsx import { ComponentRegistry } from '@object-ui/core' // Register a component ComponentRegistry.register('my-widget', MyWidgetComponent, { label: 'My Widget', category: 'Custom', icon: 'box', inputs: [ { name: 'title', type: 'string', label: 'Title' } ] }) ``` ### Field Registry [#field-registry] Maps field types to input components: ```tsx import { registerFieldRenderer } from '@object-ui/fields' // Register a field renderer registerFieldRenderer('rating', RatingFieldComponent) ``` This allows: * ✅ Overriding standard components * ✅ Adding custom field types * ✅ Plugin system for complex widgets * ✅ Keeping bundles small (lazy loading) ## Expression System [#expression-system] ObjectUI includes a powerful expression engine for dynamic UIs: ### String Interpolation [#string-interpolation] ```json { "type": "text", "value": "Welcome, ${user.firstName} ${user.lastName}!" } ``` ### Conditional Rendering [#conditional-rendering] ```json { "type": "button", "text": "Submit", "visible": "${form.isValid && !form.isSubmitting}", "disabled": "${form.isSubmitting}" } ``` ### Data Transformations [#data-transformations] ```json { "type": "badge", "text": "${orders.length} Orders", "variant": "${orders.length > 10 ? 'success' : 'warning'}" } ``` See the [Expressions Guide](/docs/guide/expressions) for complete details. ## Data Flow [#data-flow] ``` Backend API ↓ JSON Schema + Data ↓ SchemaRenderer (evaluates expressions) ↓ Component Registry (maps types) ↓ React Components (render UI) ↓ User Interactions (events) ↓ Event Handlers (update data) ↓ Re-render (React state updates) ``` ## Styling System [#styling-system] ObjectUI uses **Tailwind CSS** exclusively for styling: ### Class-Variance-Authority (CVA) [#class-variance-authority-cva] All component variants use `cva` for type-safe variants: ```tsx import { cva } from 'class-variance-authority' const buttonVariants = cva( 'inline-flex items-center justify-center rounded-md', { variants: { variant: { default: 'bg-primary text-primary-foreground', destructive: 'bg-destructive text-destructive-foreground', }, size: { default: 'h-10 px-4 py-2', sm: 'h-9 px-3', lg: 'h-11 px-8', } } } ) ``` ### Class Merging [#class-merging] Use `cn()` helper (tailwind-merge + clsx) for class overrides: ```tsx import { cn } from '@/lib/utils' setSearchQuery(e.target.value)} /> // Grid responds to view and search changes ``` The `filter` and `sort` arrays defined in the `active` list view are applied automatically when that view is selected. The `$search` query param is passed through to your `DataSource.find()` method. ## Step 8: Add a Detail View [#step-8-add-a-detail-view] Create a detail page that renders a single record with all its fields: ```tsx function TaskDetail({ taskId, onBack }: { taskId: string; onBack: () => void }) { return (
); } ``` Use this component in your main app with simple routing state, or integrate with a router like React Router or TanStack Router for URL-based navigation. ## Deployment Considerations [#deployment-considerations] **Environment config** — Keep your API URL configurable: ```ts const dataSource = new RestDataSource( import.meta.env.VITE_API_URL || 'http://localhost:3000/api' ); ``` **Performance** — Use server-side pagination via `$top`/`$skip` query params. ObjectUI plugins support lazy loading via `LazyPluginLoader` from `@object-ui/react`. Set `cache: { enabled: true, ttl: 300 }` on your schema for client-side caching. **Production build** — Run `pnpm build` and deploy the `dist/` folder to any static host (Vercel, Netlify, Cloudflare Pages). **Authentication** — Extend `RestDataSource` to inject auth headers: ```ts class AuthenticatedDataSource extends RestDataSource { constructor(baseUrl: string, private getToken: () => string) { super(baseUrl); } // Override fetch calls to include: Authorization: `Bearer ${this.getToken()}` } ``` ## Next Steps [#next-steps] * Explore the [Schema Overview](/docs/guide/schema-overview) for advanced schema features * Add a Kanban board view using `@object-ui/plugin-kanban` (see the [Todo example](https://github.com/objectstack-ai/objectui/tree/main/examples/todo)) * Connect to a production backend with the [Data Connectivity](/docs/guide/data-source) guide * Build multi-object apps with relationships (see the [CRM example](https://github.com/objectstack-ai/objectui/tree/main/examples/crm)) # CI/CD Pipeline # CI/CD Pipeline [#cicd-pipeline] ObjectUI uses **11 GitHub Actions workflows** to automate testing, quality checks, security scanning, releases, and repository maintenance. All workflow files live in `.github/workflows/`. ## Workflow Overview [#workflow-overview] ``` ┌─────────────────────────────────────────────────────────────────┐ │ Push / PR to main/develop │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ ci.yml │ │ size-check │ │ performance- │ │ │ │ (test, │ │ .yml │ │ budget.yml │ │ │ │ lint, │ │ │ │ │ │ │ │ build) │ │ │ │ │ │ │ └──────────┘ └──────────────┘ └──────────────┘ │ │ │ │ ┌──────────────┐ │ │ │ labeler.yml │ │ │ │ │ │ │ └──────────────┘ │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ Push to main │ │ ┌───────────────────┐ │ │ │ changeset-release │ → npm publish via changesets │ │ │ .yml │ │ │ └───────────────────┘ │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ Tag push (v*) │ │ ┌──────────┐ ┌───────────────┐ │ │ │ release │ │ changelog.yml │ │ │ │ .yml │ │ (git-cliff) │ │ │ └──────────┘ └───────────────┘ │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ Scheduled │ │ ┌──────────┐ ┌───────────────────┐ ┌──────────────────┐ │ │ │ stale │ │ shadcn-check.yml │ │ dependabot- │ │ │ │ .yml │ │ (weekly Mon 9AM) │ │ auto-merge.yml │ │ │ └──────────┘ └───────────────────┘ └──────────────────┘ │ │ │ ├─────────────────────────────────────────────────────────────────┤ │ Manual dispatch │ │ ┌──────────────┐ │ │ │ check-links │ → Lychee link validation │ │ │ .yml │ │ │ └──────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ## Core CI Workflow (`ci.yml`) [#core-ci-workflow-ciyml] **Triggers:** Push and PR to `main` and `develop` branches. Runs five parallel jobs: | Job | Description | | -------------- | ---------------------------------------------------------------------------------- | | **Test** | Runs `vitest` across all packages with Turbo caching. Uploads coverage to Codecov. | | **Lint** | Runs ESLint via `eslint.config.js` (flat config) and TypeScript type-checking. | | **Build Core** | Builds all packages using `turbo run build`. | | **E2E Tests** | Runs Playwright end-to-end tests from the `e2e/` directory. | | **Build Docs** | Builds the documentation site (`apps/site`). | Uses: Node 20, pnpm (via `corepack`), Turbo remote caching. ## Performance Budget (`performance-budget.yml`) [#performance-budget-performance-budgetyml] **Triggers:** Push and PR when changes touch `packages/`, `apps/console/`, or `pnpm-lock.yaml`. **Enforced limits:** | Bundle | Max gzip size | | ------------------ | ------------- | | Console main entry | 60 KB | * Builds the console app and measures bundle sizes. * Posts a PR comment with the budget report and pass/fail status. ## Size Check (`size-check.yml`) [#size-check-size-checkyml] **Triggers:** Push to `main`/`develop` with changes in `packages/`. **Enforced limits:** | Package Category | Max Size | | ------------------------------------ | ----------- | | Core (`@object-ui/core`) | 50 KB | | Components (`@object-ui/components`) | 100 KB | | Plugins (`@object-ui/plugin-*`) | 150 KB each | * Generates a markdown table with raw and gzipped sizes for each package. * Posts the size report as a PR comment. ## Link Checking (`check-links.yml`) [#link-checking-check-linksyml] **Trigger:** Manual workflow dispatch (`workflow_dispatch`). Uses [Lychee](https://github.com/lycheeverse/lychee) with configuration from `lychee.toml`: * Scans markdown files in `docs/` and `README.md` * Max concurrency: 10, timeout: 20s, retries: 3 * Excludes: localhost, example.com, Twitter/X, GitHub compare/commit URLs * Remaps internal `/docs/*` paths to `file://./docs/*` for local resolution ## Release Workflows [#release-workflows] ### Tag Release (`release.yml`) [#tag-release-releaseyml] **Trigger:** Push of version tags matching `v*`. 1. Runs the full test suite. 2. Builds all packages. 3. Creates a GitHub Release with auto-generated release notes. > Note: npm publish is currently handled by `changeset-release.yml` instead. ### Changeset Release (`changeset-release.yml`) [#changeset-release-changeset-releaseyml] **Trigger:** Push to `main`. Uses [Changesets](https://github.com/changesets/changesets) for automated versioning and npm publishing: 1. Detects pending changesets. 2. Bumps package versions. 3. Publishes to npm. 4. Configures a pnpm-lock.yaml merge driver to prevent lock file conflicts. ### Changelog Generation (`changelog.yml`) [#changelog-generation-changelogyml] **Trigger:** `release` event (when a GitHub Release is published), or manual dispatch. Uses [git-cliff](https://git-cliff.org/) with `cliff.toml` configuration to auto-generate `CHANGELOG.md` and commit it to the repository. ## Repository Maintenance [#repository-maintenance] ### Auto-Labeler (`labeler.yml`) [#auto-labeler-labeleryml] **Trigger:** PR opened, synchronized, or reopened. Automatically labels PRs based on file path patterns defined in `.github/labeler.yml`. Syncs labels on each push to the PR. ### Stale Issues (`stale.yml`) [#stale-issues-staleyml] **Trigger:** Daily at 00:00 UTC (cron), or manual dispatch. | Resource | Stale after | Close after | | ------------- | ----------- | ----------- | | Issues | 60 days | 7 days | | Pull Requests | 45 days | 14 days | Exempt labels: `pinned`, `security`, `critical`, `in-progress`. ### Dependabot Auto-Merge (`dependabot-auto-merge.yml`) [#dependabot-auto-merge-dependabot-auto-mergeyml] **Trigger:** PRs on `main`/`develop` authored by `dependabot[bot]`. * **Patch/minor updates**: Auto-approved and squash-merged. * **Major updates**: Approved with a comment for manual review. * Configures a pnpm-lock.yaml merge driver for conflict resolution. ### Shadcn Component Check (`shadcn-check.yml`) [#shadcn-component-check-shadcn-checkyml] **Trigger:** Weekly on Monday at 9:00 AM UTC, or manual dispatch. * Runs offline and online analysis of shadcn/ui components. * Creates or updates a GitHub issue if components need review or updating. * Uploads analysis artifacts for reference. ## Adding a New Workflow [#adding-a-new-workflow] 1. Create a new `.yml` file in `.github/workflows/`. 2. Follow the existing pattern for pnpm + Turbo setup: ```yaml - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - run: pnpm install --frozen-lockfile ``` 3. Use Turbo for any build/test/lint steps to leverage caching: ```yaml - run: pnpm turbo run build --filter=@object-ui/core ``` 4. For PR workflows, consider adding path filters to avoid unnecessary runs: ```yaml on: pull_request: paths: - 'packages/**' - 'pnpm-lock.yaml' ``` ## Environment Variables and Secrets [#environment-variables-and-secrets] | Secret / Variable | Used By | Purpose | | ----------------- | ----------------------- | ---------------------------------- | | `GITHUB_TOKEN` | All workflows | GitHub API access (automatic) | | `NPM_TOKEN` | `changeset-release.yml` | npm package publishing | | `CODECOV_TOKEN` | `ci.yml` | Coverage upload to Codecov | | `TURBO_TOKEN` | Build workflows | Turbo remote cache authentication | | `TURBO_TEAM` | Build workflows | Turbo remote cache team identifier | Secrets are configured in the repository settings under **Settings → Secrets and variables → Actions**. # Component Registry The Component Registry is Object UI's system for mapping schema types to React components. Understanding the registry is key to extending Object UI with custom components. ## Overview [#overview] The registry acts as a lookup table that the `SchemaRenderer` uses to determine which React component to render for each schema type: ``` Schema Type → Component Registry → React Component ``` ## Getting the Registry [#getting-the-registry] ```tsx import { getComponentRegistry } from '@object-ui/react' const registry = getComponentRegistry() ``` ## Registering Components [#registering-components] ### Using Default Components [#using-default-components] The easiest way to get started is to register all default components: ```tsx import { registerDefaultRenderers } from '@object-ui/components' // Call once at app initialization registerDefaultRenderers() ``` This registers all built-in components like: * Forms: `input`, `textarea`, `select`, `checkbox`, etc. * Data: `table`, `list`, `card`, `tree`, etc. * Layout: `page`, `grid`, `flex`, `container`, etc. * Feedback: `alert`, `dialog`, `toast`, etc. ### Registering Individual Components [#registering-individual-components] Register specific components one at a time: ```tsx import { getComponentRegistry } from '@object-ui/react' import { InputRenderer } from '@object-ui/components' const registry = getComponentRegistry() registry.register('input', InputRenderer) ``` ### Registering Custom Components [#registering-custom-components] Create and register your own components: ```tsx import { getComponentRegistry } from '@object-ui/react' import type { BaseSchema } from '@object-ui/core' interface MyComponentSchema extends BaseSchema { type: 'my-component' title: string content: string } function MyComponent(props: MyComponentSchema) { return (

{props.title}

{props.content}

) } const registry = getComponentRegistry() registry.register('my-component', MyComponent) ``` Now you can use it in schemas: ```json { "type": "my-component", "title": "Hello", "content": "This is my custom component!" } ``` ## Component Interface [#component-interface] All registered components receive the schema as props: ```tsx interface ComponentProps { // The complete schema object schema: T // Data context (optional) data?: Record // Event handlers (optional) onAction?: (action: any, context: any) => void onChange?: (value: any) => void onSubmit?: (data: any) => void } function MyRenderer(props: ComponentProps) { const { schema, data, onChange } = props return (
{/* Your component implementation */}
) } ``` ## Advanced Registration [#advanced-registration] ### With Metadata [#with-metadata] Register components with additional metadata: ```tsx registry.register('my-component', MyComponent, { displayName: 'My Custom Component', category: 'Custom', icon: 'component-icon', description: 'A custom component for special use cases', schema: { type: 'object', properties: { title: { type: 'string' }, content: { type: 'string' } } } }) ``` This metadata is used by the Visual Designer to provide better editing experience. ### Lazy Loading [#lazy-loading] Register components that load on demand: ```tsx import { lazy } from 'react' const HeavyComponent = lazy(() => import('./HeavyComponent')) registry.register('heavy-component', HeavyComponent, { lazy: true }) ``` ### Overriding Built-in Components [#overriding-built-in-components] Override default components with your own: ```tsx import { registerDefaultRenderers } from '@object-ui/components' // Register defaults first registerDefaultRenderers() // Override specific component registry.register('button', MyCustomButton) ``` ## Component Categories [#component-categories] Default components are organized by category: ### Form Components [#form-components] ```tsx - input - textarea - select - checkbox - radio - switch - slider - date-picker - time-picker - file-upload - color-picker ``` ### Data Display [#data-display] ```tsx - table - list - card - tree - timeline - calendar - kanban ``` ### Layout [#layout] ```tsx - page - container - grid - flex - tabs - accordion - divider - spacer ``` ### Feedback [#feedback] ```tsx - alert - toast - dialog - drawer - popover - tooltip - progress - skeleton - spinner ``` ### Navigation [#navigation] ```tsx - menu - breadcrumb - pagination - steps ``` ### Other [#other] ```tsx - button - link - text - icon - image - video - badge - avatar ``` ## Checking Registered Components [#checking-registered-components] ### Get All Registered Types [#get-all-registered-types] ```tsx const types = registry.getRegisteredTypes() console.log(types) // ['input', 'button', 'form', ...] ``` ### Check if Type is Registered [#check-if-type-is-registered] ```tsx if (registry.has('my-component')) { console.log('Component is registered') } ``` ### Get Component Metadata [#get-component-metadata] ```tsx const metadata = registry.getMetadata('input') console.log(metadata) // { // displayName: 'Input', // category: 'Form', // icon: 'input-icon', // ... // } ``` ## Best Practices [#best-practices] ### 1. Register Once at App Initialization [#1-register-once-at-app-initialization] ```tsx // main.tsx or App.tsx import { registerDefaultRenderers } from '@object-ui/components' registerDefaultRenderers() function App() { // Your app code } ``` ### 2. Use TypeScript for Custom Components [#2-use-typescript-for-custom-components] ```tsx import type { BaseSchema } from '@object-ui/core' interface CustomSchema extends BaseSchema { type: 'custom' customProp: string } function CustomComponent(props: { schema: CustomSchema }) { // TypeScript ensures type safety } ``` ### 3. Follow Naming Conventions [#3-follow-naming-conventions] Use kebab-case for component types: * ✅ `my-component`, `custom-button`, `data-table` * ❌ `MyComponent`, `customButton`, `DataTable` ### 4. Provide Meaningful Metadata [#4-provide-meaningful-metadata] ```tsx registry.register('rating', RatingComponent, { displayName: 'Star Rating', category: 'Form', icon: 'star', description: 'A 5-star rating input component', tags: ['form', 'input', 'rating'] }) ``` ### 5. Handle Missing Props Gracefully [#5-handle-missing-props-gracefully] ```tsx function MyComponent(props: ComponentProps) { const { schema } = props const title = schema.title || 'Default Title' const content = schema.content || '' return (

{title}

{content}

) } ``` ## Creating Plugin Packages [#creating-plugin-packages] Group related components into plugin packages: ```tsx // @my-org/objectui-plugin-charts import { getComponentRegistry } from '@object-ui/react' import { BarChart } from './BarChart' import { LineChart } from './LineChart' import { PieChart } from './PieChart' export function registerChartComponents() { const registry = getComponentRegistry() registry.register('bar-chart', BarChart) registry.register('line-chart', LineChart) registry.register('pie-chart', PieChart) } ``` Usage: ```tsx import { registerDefaultRenderers } from '@object-ui/components' import { registerChartComponents } from '@my-org/objectui-plugin-charts' registerDefaultRenderers() registerChartComponents() ``` ## Example: Custom Form Component [#example-custom-form-component] Here's a complete example of a custom form component: ```tsx import { forwardRef } from 'react' import { getComponentRegistry } from '@object-ui/react' import type { BaseSchema } from '@object-ui/core' import { cn } from '@/lib/utils' interface RatingSchema extends BaseSchema { type: 'rating' name: string label?: string maxStars?: number required?: boolean disabled?: boolean onChange?: (value: number) => void } const RatingComponent = forwardRef( ({ schema }, ref) => { const [value, setValue] = useState(0) const maxStars = schema.maxStars || 5 const handleClick = (rating: number) => { if (schema.disabled) return setValue(rating) schema.onChange?.(rating) } return (
{schema.label && ( )}
{Array.from({ length: maxStars }).map((_, index) => ( ))}
) } ) RatingComponent.displayName = 'Rating' // Register the component const registry = getComponentRegistry() registry.register('rating', RatingComponent, { displayName: 'Star Rating', category: 'Form', description: 'A star rating input component', schema: { type: 'object', properties: { name: { type: 'string' }, label: { type: 'string' }, maxStars: { type: 'number', default: 5 }, required: { type: 'boolean' }, disabled: { type: 'boolean' } }, required: ['name'] } }) export { RatingComponent } ``` ## Next Steps [#next-steps] * [Expression System](./expressions.md) - Learn about dynamic expressions * [Schema Rendering](./schema-rendering.md) - Understand the rendering engine * [Creating Custom Components](/spec/component-package.md) - Deep dive into component creation ## Related Documentation [#related-documentation] * [Core API](/api/core) - Component registry API * [React API](/api/react) - React integration * [Component Specification](/spec/component.md) - Component metadata spec # Console Architecture # Console Architecture [#console-architecture] This document describes the internal architecture of the Console app (`apps/console`). ## Data Flow [#data-flow] ``` ┌─────────────────────────────────────────────────────────┐ │ objectstack.config.ts │ │ (defineStack → apps, objects, views) │ └────────────────────┬────────────────────────────────────┘ │ MSW / Real Server ▼ ┌─────────────────────────────────────────────────────────┐ │ ObjectStackAdapter (@object-ui/data-objectstack) │ │ • discovery() → apps[], objects[] │ │ • find / findOne / create / update / delete │ │ • getView / getApp (optional metadata cache) │ └────────────────────┬────────────────────────────────────┘ │ DataSource interface ▼ ┌─────────────────────────────────────────────────────────┐ │ SchemaRendererProvider (@object-ui/react) │ │ • provides dataSource + registry to all children │ └────────────────────┬────────────────────────────────────┘ │ React Context ▼ ┌─────────────────────────────────────────────────────────┐ │ App.tsx │ │ ├── ExpressionProvider (user, app, evaluator) │ │ ├── ConsoleLayout │ │ │ ├── AppShell (@object-ui/layout) │ │ │ │ └── useAppShellBranding (CSS vars) │ │ │ ├── AppSidebar (navigation tree) │ │ │ └── AppHeader (breadcrumbs, status) │ │ └── Routes │ │ ├── /apps/:appName/:objectName → ObjectView │ │ ├── /apps/:appName/:objectName/record/:id → Detail │ │ └── /apps/:appName → Home Page │ └─────────────────────────────────────────────────────────┘ ``` ## Routing [#routing] The console uses React Router DOM v7 with a simple flat route structure: | Route Pattern | Component | Purpose | | --------------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------- | | `/apps/:appName` | Home redirect | Redirects to the first object in navigation | | `/apps/:appName/:objectName` | `ObjectView` | Object list with view switcher | | `/apps/:appName/:objectName/view/:viewId` | `ObjectView` | Specific view for an object | | `/apps/:appName/:objectName/data` | `ObjectDataPage` | Bare data surface — URL `filter[]=` conditions, not bound to any saved view (ADR-0055) | | `/apps/:appName/:objectName/record/:recordId` | `RecordDetailView` | Single-record detail | | `/apps/:appName/create-app` | `CreateAppPage` | App creation wizard (4-step) | | `/apps/:appName/edit-app/:editAppName` | `EditAppPage` | Edit existing app configuration | ## Key Patterns [#key-patterns] ### 1. Expression-Based Visibility [#1-expression-based-visibility] Navigation items can be conditionally hidden using expressions: ```json { "type": "object", "objectName": "admin_settings", "visible": "${user.role === 'admin'}" } ``` The `ExpressionProvider` wraps the layout and provides an `ExpressionEvaluator` that resolves `${}` templates against context variables (`user`, `app`, `data`). ### 2. Action System [#2-action-system] Actions are typed with `ActionDef` from `@object-ui/core`: ```ts const { execute } = useActionRunner({ context: { objectName: 'contacts' }, }); await execute({ type: 'delete', confirmText: 'Are you sure?', params: { recordId: '123' }, }); ``` The `ActionRunner` supports: * **Confirmation** — async `ConfirmationHandler` (default: `window.confirm`, override with Shadcn AlertDialog) * **Toast notifications** — `ToastHandler` for success/error messages * **Custom handlers** — register domain-specific action types (e.g., `'create'`, `'delete'`, `'refresh'`) ### 3. Plugin ObjectView Delegation [#3-plugin-objectview-delegation] The console's `ObjectView` is a **thin wrapper** around `@object-ui/plugin-view`'s `ObjectView`: * Resolves views from the object definition's `list_views` * Passes a `renderListView` callback for multi-view rendering (kanban, calendar, chart) * Handles console-specific concerns: URL routing, MetadataInspector, record detail Sheet ### 4. App Creation & Editing [#4-app-creation--editing] The console integrates the `AppCreationWizard` from `@object-ui/plugin-designer` for creating and editing apps: * **Create App** — `CreateAppPage` at `/apps/:appName/create-app`. Passes metadata objects as `availableObjects`, handles `onComplete` (converts draft via `wizardDraftToAppSchema()`, navigates to new app), `onCancel` (navigate back), and `onSaveDraft` (localStorage persistence). * **Edit App** — `EditAppPage` at `/apps/:appName/edit-app/:editAppName`. Loads existing app config as `initialDraft` and updates on completion. **Entry Points:** * AppSidebar app switcher → "Add App" / "Edit App" buttons * CommandPalette (⌘+K) → "Create New App" command in Actions group * Empty state CTA → "Create Your First App" button when no apps are configured ### 5. Branding [#5-branding] Per-app branding is applied via `AppShell`'s `branding` prop: ```tsx ``` This sets CSS custom properties (`--brand-primary`, `--brand-primary-hsl`, etc.) on the document root. ## MSW Mock Mode [#msw-mock-mode] In development, the console uses MSW (Mock Service Worker) to simulate an ObjectStack backend: 1. `objectstack.config.ts` defines apps and objects via `@objectstack/spec` 2. `@objectstack/runtime` boots an `ObjectKernel` with `MSWPlugin` 3. MSW intercepts `/api/v1/*` requests and serves in-memory data 4. The `ObjectStackAdapter` connects to this mock server transparently This allows full offline development without a real backend. # Console App # ObjectStack Console [#objectstack-console] The **Console** is the reference application for [ObjectUI](/docs/guide). It renders a full-featured admin interface from JSON metadata — objects, views, dashboards, and actions — with zero custom pages required. ## Quick Start [#quick-start] ```bash # From the repository root pnpm install pnpm console # starts the dev server (Vite) ``` The console opens at **[http://localhost:5175](http://localhost:5175)** with MSW (Mock Service Worker) providing a simulated backend. ## Key Features [#key-features] | Feature | Description | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Multi-App Switcher** | Switch between apps defined in `objectstack.config.ts`. | | **Dynamic Navigation** | Sidebar renders from the app's `navigation` tree (objects, groups, URLs, pages). | | **Object Views** | List / Grid / Kanban / Calendar — backed by `@object-ui/plugin-view`. | | **CRUD Dialogs** | Create & edit records via schema-driven forms. | | **Expression Visibility** | Show/hide navigation items using `visible: "${data.role === 'admin'}"`. | | **Branding** | Per-app colors, favicons, and logos via `AppShell` branding. | | **Command Palette** | `⌘+K` opens a searchable command bar for quick navigation. | | **Studio Package Scope** | Studio home, metadata counts, quick-create links, and diagnostics follow the selected package. | | **Design in Studio** | Workspace admins get a top-bar entry inside a running app that opens its owning package on the Studio design surface. On an interface route — a dashboard, page, or report — it deep-links straight to that surface's design page in the Interfaces pillar (`/studio/:packageId/interfaces?surface=:`, e.g. `surface=page:showcase_crm_workbench`); elsewhere (objects, the app root) it opens the package's Data tab (`/studio/:packageId/data`). These interfaces are authored in Studio — there is no in-page edit panel. | | **App Creation Wizard** | 4-step wizard (Basic Info → Objects → Navigation → Branding) to create or edit apps. | | **Error Boundary** | Graceful error handling with a retry button. | ### Object design (Studio Data tab) [#object-design-studio-data-tab] Selecting an object in Studio's **Data** pillar (`/studio/:packageId/data`) opens a tab strip over that object — **Records · Form · Validations · Hooks · Actions · API · Settings**. Each of Validations, Hooks and Actions is a no-code **config panel driven by the corresponding metadata**, and each supports **adding** new entries — no code round-trip required: | Tab | Edits | Panel | | --------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Validations** | the object's inline `validations[]` (spec `ValidationRuleSchema`) | Master-detail covering **every** rule type — `script`, `cross_field`, `state_machine`, `format`, `json_schema`, `conditional`. The **New** menu adds any type (seeded with a valid, never-firing skeleton); a rule's type can be switched in place. CEL predicates reuse the shared `ConditionBuilder`, fed the object's draft fields. | | **Hooks** | the separate `hook` metadata type targeting this object | Master-detail whose editor is the platform `SchemaForm` **driven by the live `hook` JSONSchema from `/meta/types`**, so its fields and enums always match the running server's contract. | | **Actions** | the object's inline `actions[]` (spec `ActionSchema`) | Master-detail using the type-aware `ActionDefaultInspector`; anything not curated falls through to a **"More fields"** form fed the live `action` JSONSchema, so no spec property is un-editable. | Validations and Actions persist with the object's own **Save draft**; Hooks (a distinct metadata type) save per-hook. Nothing goes live until the package is published from the top-bar **Publish** flow. ## Configuration [#configuration] The console reads its configuration from `objectstack.config.ts`: ```ts import { defineStack } from '@objectstack/spec'; import { ObjectSchema, App, Field } from '@objectstack/spec'; export default defineStack({ apps: [ App.create({ name: 'crm', label: 'CRM', icon: 'briefcase', navigation: [ { type: 'object', objectName: 'contacts', label: 'Contacts', icon: 'users' }, { type: 'object', objectName: 'deals', label: 'Deals', icon: 'dollar-sign' }, ], branding: { primaryColor: '#3B82F6' }, }), ], objects: [ ObjectSchema.create({ name: 'contacts', label: 'Contacts', fields: [ Field.text('name', { label: 'Name', required: true }), Field.email('email', { label: 'Email' }), ], }), ], }); ``` ## Running with a Real Backend [#running-with-a-real-backend] To connect to a real ObjectStack server instead of MSW: 1. Set the `VITE_API_URL` environment variable: ```bash VITE_API_URL=http://localhost:3000 pnpm console ``` 2. The console will use the ObjectStack client to discover metadata and perform CRUD operations against the server. ## Folder Structure [#folder-structure] ``` apps/console/ src/ App.tsx # Root component + routing dataSource.ts # ObjectStackAdapter wrapper components/ AppHeader.tsx # Top navbar (breadcrumbs, connection status) AppSidebar.tsx # Left sidebar (app switcher, navigation tree) CommandPalette.tsx # ⌘+K command bar ConsoleLayout.tsx # AppShell wrapper ObjectView.tsx # Object list view (wraps plugin-view) RecordDetailView.tsx # Single-record detail view pages/ CreateAppPage.tsx # App creation wizard page EditAppPage.tsx # Edit existing app page context/ ExpressionProvider.tsx # Expression evaluation context hooks/ useBranding.ts # Delegates to @object-ui/layout useObjectActions.ts # CRUD action handlers mocks/ browser.ts # MSW browser worker ``` ## See Also [#see-also] * [Console Architecture](/docs/guide/console-architecture) — data flow, routing, and plugin integration * [Schema Overview](/docs/guide/schema-overview) — the JSON protocol that drives the console * [Data Source](/docs/guide/data-source) — how the adapter fetches and caches data # Dashboard-Level Filters A dashboard often needs one top-level filter — a date range, a region select — that drives **several charts at once**. ObjectUI models this as a **dashboard-level parameter**, not a shared dataset: * The **filter control and its value live on the dashboard** — hosted as dashboard-level variables (the page/dashboard variables primitive). * Each widget declares which of **its own** fields a filter binds to via `filterBindings` — a small mapping, not a copied query. * At render time the dashboard **broadcasts** the active values into every bound widget's inline query, `AND`-combined with the widget's own `filter`. Charts stay inline and self-contained; one place owns the filter; each chart edit stays local. > Working examples: the schema catalog ships a `plugin-dashboard/filtered-dashboard` > example plus variants for dynamic options, text/number/lookup filter types, > dataset widgets, the `targetWidgets` allow-list, and date presets with a > custom range. ## Tutorial: from zero to a filtered dashboard [#tutorial-from-zero-to-a-filtered-dashboard] ### Step 1 — a plain dashboard [#step-1--a-plain-dashboard] Start from two charts over **different** objects. Without filters they always show everything: ```json { "type": "dashboard", "columns": 2, "widgets": [ { "id": "invoices_by_status", "title": "Invoices by Status", "type": "bar", "object": "invoices", "categoryField": "status", "aggregate": "count" }, { "id": "accounts_signed", "title": "Accounts Signed", "type": "line", "object": "accounts", "categoryField": "signed_at", "categoryGranularity": "month", "aggregate": "count" } ] } ``` ### Step 2 — add the built-in date range [#step-2--add-the-built-in-date-range] Declare `dateRange` at the dashboard level. A preset/custom date-range control appears in the filter bar above the widgets: ```json { "dateRange": { "field": "created_at", "defaultRange": "last_30_days", "allowCustomRange": true } } ``` * `field` — the **default** field the range applies to on every bound widget (falls back to `created_at` when omitted). * `defaultRange` — the initially selected preset: `today`, `yesterday`, `this_week`, `last_week`, `this_month`, `last_month`, `this_quarter`, `last_quarter`, `this_year`, `last_year`, `last_7_days`, `last_30_days`, `last_90_days`, or `custom` (starts empty and lets the user pick). * `allowCustomRange` — offer a "Custom…" item that opens a from/to calendar (default `true`). Presets stay **symbolic** until query time: they compile to date-macro tokens (`{30_days_ago}`, `{current_month_start}`, …) that each widget resolves exactly like hand-authored widget filters — so a dashboard saved today still means "last 30 days" tomorrow. ### Step 3 — add a global filter [#step-3--add-a-global-filter] Add a `globalFilters` entry. Each entry renders one control in the filter bar: ```json { "globalFilters": [ { "name": "region", "field": "region", "label": "Region", "type": "select", "options": ["EMEA", "APAC", "AMER"] } ] } ``` * `name` — the **stable filter name**: the variable key the value is published under, and the key widgets reference in `filterBindings`. Defaults to `field`. (`"dateRange"` is reserved for the built-in date range.) * `field` — the default field the filter applies to on bound widgets. * `type` — the control type: `text`, `number`, `select`, `lookup`, or `date`. | Type | Control | Generated condition | | ------------------- | ------------------- | ----------------------------------------- | | `text` | input | `{ field: { "$contains": value } }` | | `number` | numeric input | `{ field: value }` (equality) | | `select` / `lookup` | dropdown | `{ field: value }` (or `$in` for arrays) | | `date` | preset/custom range | `{ field: { "$gte": from, "$lte": to } }` | Static `options` accept the `@objectstack/spec` object form (`{ "value": "amer", "label": "AMER" }` — canonical, and what the spec validates) or a bare-string shorthand (`["EMEA", "APAC"]`); the runtime normalizes both to value/label pairs. Options can also be fetched from an object at runtime: ```json { "name": "industry", "field": "industry", "label": "Industry", "type": "select", "optionsFrom": { "object": "accounts", "valueField": "industry", "labelField": "industry" } } ``` With a dataset-capable data source, `optionsFrom` resolves distinct values **server-side** (a GROUP BY over the source object), so the option list is complete regardless of row count. Data sources without dataset queries fall back to a best-effort client-side dedupe over the first 200 records. ### Step 4 — bind each widget's own fields [#step-4--bind-each-widgets-own-fields] By default every filter applies to its own `field` on every widget. When a widget stores the concept under a different field — or should ignore a filter — declare `filterBindings` on the widget: ```json { "widgets": [ { "id": "invoices_by_status", "type": "bar", "object": "invoices", "categoryField": "status", "aggregate": "count" }, { "id": "accounts_signed", "type": "line", "object": "accounts", "categoryField": "signed_at", "categoryGranularity": "month", "aggregate": "count", "filterBindings": { "dateRange": "signed_at", "region": "sales_region" } }, { "id": "total_invoices", "title": "Total Invoices (all regions)", "type": "metric", "object": "invoices", "aggregate": "count", "filterBindings": { "region": false } } ] } ``` Binding rules, in precedence order: 1. `filterBindings[name]` as a **string** — apply the filter to that field. 2. `filterBindings[name]: false` — opt this widget out of that filter. 3. Legacy `targetWidgets` on the filter — when set, only listed widget ids get the default binding (an explicit `filterBindings` entry still wins). 4. Otherwise the filter applies to its own `field` (the built-in date range defaults to `dateRange.field ?? 'created_at'`). That's the whole feature: changing any filter live re-scopes every bound widget, each against **its own** field. Here it is running in the showcase app's *Revenue Pulse* dashboard — the date range's default field is the invoice `issued_on`, account widgets re-map it to `signed_on`, and the "Accounts (all time)" KPI opts out of both filters: Revenue Pulse — dashboard-level date + region filters over two objects Selecting **EMEA** re-scopes every bound widget live (invoices via their own `region`, accounts via `sales_region`), while the opted-out KPI holds steady — and a Reset button appears once any filter deviates from its default: Revenue Pulse re-scoped to EMEA — bound widgets update, the opted-out KPI holds Bindings can also be edited visually: the Studio dashboard widget inspector shows a **Dashboard filter bindings** section (one row per declared filter) with an Apply toggle (opt-out) and a field picker for the override — no JSON editing required. The widget inspector's Dashboard filter bindings section ## Reading filter values in expressions [#reading-filter-values-in-expressions] Filter values are hosted as dashboard variables, so any widget expression can read them under the `page.` scope, keyed by the filter's `name`: ```json { "type": "text", "value": "Region: ${page.region || 'All'}" } ``` ```json { "id": "emea_playbook", "component": { "type": "card", "title": "EMEA Playbook", "hidden": "${page.region !== 'EMEA'}" } } ``` The built-in date range is an object under `page.dateRange` — a preset selection is `{ "preset": "last_30_days" }`, a custom range is `{ "from": "2026-01-01", "to": "2026-03-31" }` (either bound may be absent). ## Dataset widgets [#dataset-widgets] Widgets bound to a semantic-layer `dataset` participate the same way: the dashboard merges the scoped filter into the widget's `filter`, which the dataset widget forwards to the dataset query as `runtimeFilter`. Inline (`object`-based) and dataset-bound widgets can mix freely on one filtered dashboard. ## Nested variable scopes [#nested-variable-scopes] When a filtered dashboard is embedded inside a Page that declares its own `variables`, the two scopes **merge**: inside the dashboard subtree, `page.*` resolves the outer Page's variables plus the dashboard's filter values, and a dashboard filter only shadows an outer variable that has the **same name**. Writes route to the scope that defines the variable — setting an outer-page variable from inside the dashboard updates the outer scope, so both subtrees stay in sync. ## Known limitations [#known-limitations] * **Static-data widgets are not filtered** — a widget with an inline `data` array has no query to scope, so dashboard filters do not apply to it. Bind the widget to an `object` (or a `dataset`) if it should respond to filters. * **Default bindings are metadata-checked for `object` widgets only** — when a filter's default `field` does not exist on an inline widget's object, the binding is skipped with a console warning instead of issuing a query that matches nothing. Dataset-bound widgets can't be checked this way (the dashboard doesn't know the dataset's base-object fields), so map their filters explicitly with `filterBindings: { "": "" }` or opt out with `false`. Explicit string bindings are always honoured as written — a typo shows up as a visibly empty widget rather than a silently dropped filter. ## i18n [#i18n] The filter bar's strings resolve from the `dashboard.filters.*` keys (`@object-ui/i18n` ships `en` and `zh` entries — control labels come from each filter's `label`, so translate those in your schema metadata). ## Spec alignment [#spec-alignment] `DashboardSchema.dateRange`, `GlobalFilterSchema` (including `name`) and `DashboardWidgetSchema.filterBindings` are part of `@objectstack/spec` (framework#2501). Author dashboards against the spec shapes; ObjectUI renders them. # Data Connectivity ObjectUI follows the **Universal Adapter Pattern**. UI components do not hardcode transport details. They receive a `DataSource` implementation from `SchemaRendererProvider` and call a stable CRUD/query contract. This keeps the renderer backend-agnostic: ObjectStack, REST, GraphQL, and proprietary backends can all be adapted behind the same interface. ## The Interface [#the-interface] The canonical interface lives in `@object-ui/types`: ```typescript import type { QueryParams, QueryResult } from '@object-ui/types'; export interface DataSource { find(resource: string, params?: QueryParams): Promise>; findOne(resource: string, id: string | number, params?: QueryParams): Promise; create(resource: string, data: Partial): Promise; update( resource: string, id: string | number, data: Partial, opts?: { ifMatch?: string }, ): Promise; delete( resource: string, id: string | number, opts?: { ifMatch?: string }, ): Promise; getObjectSchema(objectName: string): Promise; } ``` `find()` returns a `QueryResult` so components can receive both rows and pagination metadata: ```typescript interface QueryResult { data: T[]; total?: number; page?: number; pageSize?: number; hasMore?: boolean; cursor?: string; metadata?: Record; } ``` ## Available Adapters [#available-adapters] ### ObjectStack Adapter (Official) [#objectstack-adapter-official] Use `@object-ui/data-objectstack` for ObjectStack-compatible backends. ```bash pnpm add @object-ui/data-objectstack ``` ```typescript import { createObjectStackAdapter } from '@object-ui/data-objectstack'; const dataSource = createObjectStackAdapter({ baseUrl: 'https://api.your-instance.com' }); ``` ## Usage [#usage] Inject the data source at the renderer boundary: ```tsx import '@object-ui/components'; import '@object-ui/fields'; import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; import { createObjectStackAdapter } from '@object-ui/data-objectstack'; const dataSource = createObjectStackAdapter({ baseUrl: 'https://api.example.com' }); function App() { return ( ); } ``` ## Creating a Custom Adapter [#creating-a-custom-adapter] If you have a proprietary backend, wrap its SDK or client in a `DataSource` implementation. Keep transport details in the adapter, not in renderers. ```typescript import type { DataSource, QueryParams, QueryResult } from '@object-ui/types'; type User = { id: string; name: string; email: string; }; type BackendClient = { listUsers(params?: QueryParams): Promise<{ rows: User[]; total?: number }>; getUser(id: string | number): Promise; createUser(data: Partial): Promise; updateUser(id: string | number, data: Partial): Promise; deleteUser(id: string | number): Promise; describeObject(name: string): Promise; }; class UserDataSource implements DataSource { constructor(private readonly client: BackendClient) {} async find(resource: string, params?: QueryParams): Promise> { if (resource !== 'users') { return { data: [], total: 0 }; } const result = await this.client.listUsers(params); return { data: result.rows, total: result.total, }; } findOne(_resource: string, id: string | number): Promise { return this.client.getUser(id); } create(_resource: string, data: Partial): Promise { return this.client.createUser(data); } update(_resource: string, id: string | number, data: Partial): Promise { return this.client.updateUser(id, data); } delete(_resource: string, id: string | number): Promise { return this.client.deleteUser(id); } getObjectSchema(objectName: string): Promise { return this.client.describeObject(objectName); } } ``` ## Query Parameters [#query-parameters] ObjectUI uses OData-style query keys for broad compatibility: ```typescript await dataSource.find('users', { $select: ['id', 'name', 'email'], $filter: { status: 'active' }, $orderby: { name: 'asc' }, $skip: 0, $top: 25, $count: true, }); ``` Data-aware plugins may also use optional methods such as `bulkUpdate`, `bulkDelete`, `getView`, or `listViewOverrides` when an adapter supports them. Keep the required CRUD methods implemented first, then add optional capabilities as your UI needs them. # Deployment # Deployment [#deployment] ObjectUI apps are standard Vite + React applications, so they can be deployed anywhere that serves static files or runs Node.js containers. This guide provides copy-paste-ready configurations for the most popular platforms. ## Prerequisites [#prerequisites] * A production build: `pnpm build` (runs `turbo run build` across all packages) * The build output lives in `apps/console/dist/` (or your app's `dist/` folder) * Environment variables configured for your target environment ## Docker [#docker] Create a multi-stage `Dockerfile` at the project root: ```dockerfile # Stage 1: Build FROM node:20-alpine AS builder RUN corepack enable && corepack prepare pnpm@9 --activate WORKDIR /app COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./ COPY packages/ packages/ COPY apps/ apps/ RUN pnpm install --frozen-lockfile RUN pnpm build # Stage 2: Serve FROM nginx:alpine AS runner COPY --from=builder /app/apps/console/dist /usr/share/nginx/html COPY <<'EOF' /etc/nginx/conf.d/default.conf server { listen 80; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ { expires 1y; add_header Cache-Control "public, immutable"; } gzip on; gzip_types text/plain text/css application/json application/javascript text/xml; } EOF EXPOSE 80 CMD ["nginx", "-g", "daemon off;"] ``` Build and run: ```bash docker build -t objectui-app . docker run -p 3000:80 objectui-app ``` ## Vercel [#vercel] Create `vercel.json` in the project root: ```json { "buildCommand": "pnpm build", "outputDirectory": "apps/console/dist", "installCommand": "pnpm install --frozen-lockfile", "framework": "vite", "rewrites": [ { "source": "/(.*)", "destination": "/index.html" } ], "headers": [ { "source": "/assets/(.*)", "headers": [ { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" } ] } ] } ``` Deploy with the Vercel CLI: ```bash npx vercel --prod ``` > **Note:** Set the **Root Directory** to the repository root so the monorepo workspace resolves correctly. ## Railway [#railway] Create `railway.json` in the project root: ```json { "$schema": "https://railway.com/railway.schema.json", "build": { "builder": "NIXPACKS", "buildCommand": "corepack enable && pnpm install --frozen-lockfile && pnpm build" }, "deploy": { "startCommand": "npx serve apps/console/dist -s -l tcp://0.0.0.0:$PORT", "healthcheckPath": "/", "restartPolicyType": "ON_FAILURE", "restartPolicyMaxRetries": 3 } } ``` Push to your linked Railway project: ```bash railway up ``` ## Netlify [#netlify] Create `netlify.toml` in the project root: ```toml [build] command = "pnpm install --frozen-lockfile && pnpm build" publish = "apps/console/dist" [build.environment] NODE_VERSION = "20" PNPM_VERSION = "9" # SPA fallback — redirect all routes to index.html [[redirects]] from = "/*" to = "/index.html" status = 200 [[headers]] for = "/assets/*" [headers.values] Cache-Control = "public, max-age=31536000, immutable" ``` Deploy with the Netlify CLI: ```bash npx netlify deploy --prod ``` ## Environment Variables [#environment-variables] ObjectUI uses Vite's `import.meta.env` for build-time configuration. Prefix all custom variables with `VITE_`. | Variable | Default | Description | | ---------------------- | --------------- | --------------------------------------------------------------------------------------------------------- | | `VITE_USE_MOCK_SERVER` | `"true"` | Enable MSW mock server for local development. Set to `"false"` for production builds that hit a real API. | | `VITE_API_URL` | — | Base URL for the ObjectStack API (e.g. `https://api.example.com`). | | `NODE_ENV` | `"development"` | Set automatically to `"production"` by `vite build`. | Create a `.env.production` file for production defaults: ```bash VITE_USE_MOCK_SERVER=false VITE_API_URL=https://api.example.com ``` For platform-specific configuration, set environment variables in each platform's dashboard or CLI: ```bash # Vercel vercel env add VITE_API_URL production # Railway railway variables set VITE_API_URL=https://api.example.com # Netlify netlify env:set VITE_API_URL https://api.example.com ``` > **Tip:** The console app ships with MSW enabled by default. Always set `VITE_USE_MOCK_SERVER=false` in production to disable the mock service worker. ## Build Optimization [#build-optimization] ### Gzip and Brotli Compression [#gzip-and-brotli-compression] Add the `vite-plugin-compression` plugin for pre-compressed assets: ```bash pnpm add -D vite-plugin-compression ``` ```ts // vite.config.ts import compression from 'vite-plugin-compression'; export default defineConfig({ plugins: [ react(), tailwindcss(), compression({ algorithm: 'gzip' }), // .gz files compression({ algorithm: 'brotliCompress', ext: '.br' }), // .br files ], }); ``` ### Code Splitting [#code-splitting] Vite splits chunks automatically. For ObjectUI plugins, use dynamic imports to keep the initial bundle small: ```tsx import { createLazyPlugin } from '@object-ui/react'; const ObjectGrid = createLazyPlugin( () => import('@object-ui/plugin-grid'), { fallback:
Loading grid...
} ); ``` ### Bundle Analysis [#bundle-analysis] Visualize your bundle to find optimization opportunities: ```bash pnpm add -D rollup-plugin-visualizer ``` ```ts // vite.config.ts import { visualizer } from 'rollup-plugin-visualizer'; export default defineConfig({ plugins: [ react(), tailwindcss(), visualizer({ open: true, gzipSize: true }), ], }); ``` ### Production Build Command [#production-build-command] Build without the mock server and verify the output: ```bash VITE_USE_MOCK_SERVER=false pnpm build ``` This is equivalent to the `build:server` script defined in the console app. ## Health Checks [#health-checks] For containerized deployments, add a lightweight health check endpoint. Create `public/health.json` in your app: ```json { "status": "ok" } ``` This file is copied to the build output as-is by Vite. Point your health check to `/health.json`: ```dockerfile # Docker HEALTHCHECK HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD wget -qO- http://localhost:80/health.json || exit 1 ``` ```json // railway.json (excerpt) { "deploy": { "healthcheckPath": "/health.json" } } ``` For platforms that expect an HTTP 200 on `/`, the SPA `index.html` fallback already handles this. ## Next Steps [#next-steps] * [CI/CD Pipeline](/docs/guide/ci-cd-pipeline) — Understand the automated build and release workflows * [Architecture Overview](/docs/guide/architecture) — How ObjectUI packages fit together * [Quick Start](/docs/guide/quick-start) — Set up a new ObjectUI project from scratch * [Theming](/docs/guide/theming) — Customize the look-and-feel before deploying # Designing App Navigation # Designing App Navigation [#designing-app-navigation] When you compose an app, the same requirement can often be expressed three ways: a plain object menu entry, an object entry pinned to a named view, or a custom page that embeds views. They are **not** interchangeable — this guide gives you the decision rules. (The canonical, agent-facing version lives in `skills/objectui/guides/app-composition.md`; keep the two in sync.) ## The Three Ways to Reach a List [#the-three-ways-to-reach-a-list] Say your app has a `project` object: | You write | User lands on | You get for free | | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `{ "type": "object", "objectName": "project" }` | `/apps/my_app/project` — the object's **default view** | The full object shell: view switcher, object actions, a create button, record detail routing, search and recent-items integration | | `{ "type": "object", "objectName": "project", "viewName": "project.by_status" }` | `/apps/my_app/project/view/project.by_status` | The same shell, with the entry **anchored** to a named view — users can still switch | | `{ "type": "object", "objectName": "project", "filters": { "status": "open" } }` | `/apps/my_app/project/data?filter[status]=open` — the **bare data surface** | URL-defined conditions over everything permissions allow, bound to **no saved view**. Conditions show as removable chips; the full filter/sort/group toolbar is available; "Save as view" turns the slice into a named view | | `{ "type": "page", "pageName": "project_overview" }` | `/apps/my_app/page/project_overview` | Nothing but your page schema. View switching, actions, and record links must be assembled by hand | The key asymmetry: a page can imitate the other two, but it **loses the object shell** — and every future improvement to that shell (new actions, better view switching, permission trimming) will skip your page. ## The Rule of Least Power [#the-rule-of-least-power] Use the least powerful construct that expresses the requirement: 1. **Default: one plain `object` entry per core business object.** No `viewName`. The default view is defined by view metadata, so the navigation layer stays decoupled from presentation. 2. **Add `viewName` when the menu item is a named slice.** If the label is a *perspective* — "By Status", "Due This Week", "My Tasks" — create a named view (convention: `.`, e.g. `project.by_status`) and anchor the entry to it. `viewName` sets the entry point; it does not lock the user in. 3. **Use `filters` for one-off or parameterized slices.** A dashboard drill-through, a shared link, "records assigned to me" — put the condition in the URL (`filters: { "owner_id": "{current_user_id}" }`) and let it land on the bare data surface instead of authoring a view. Promote the slice to a named view only when it's curated and reused. Note the surface is not a security feature: it shows exactly what row-level permissions already allow. 4. **Create a page only for composition a single object view cannot express.** Multiple objects side by side or in tabs, KPI cards mixed with lists, onboarding or static content, parameterized pages. A page that wraps a single object's single view is an anti-pattern. 5. **Use `dashboard` for metric/chart aggregation and `report` for tabular analysis** — don't rebuild them as pages of chart blocks. 6. **One entry per target.** Don't offer the same object through both a plain entry and a page that wraps its default view. If one object needs several menu entries, make all of them named-view entries — mixing styles breaks active-state highlighting in the sidebar. As a rule of thumb, in a typical business app about 80% of navigation entries should be `object` entries (with or without `viewName`); pages are for the home screen, onboarding, and cross-object workbenches. ## Write Spec-Shaped Items [#write-spec-shaped-items] Navigation is a discriminated union on `type`. Each type has its own target field — there is no generic `path` and no `kind`: ```json { "navigation": [ { "id": "nav_projects", "type": "object", "objectName": "project", "label": "Projects" }, { "id": "nav_by_status", "type": "object", "objectName": "project", "viewName": "project.by_status", "label": "By Status" }, { "id": "nav_my_open", "type": "object", "objectName": "project", "filters": { "owner_id": "{current_user_id}", "status": "open" }, "label": "My Open Projects" }, { "id": "nav_kpis", "type": "dashboard", "dashboardName": "company_kpis", "label": "KPIs" }, { "id": "nav_workbench", "type": "page", "pageName": "cross_object_workbench", "label": "Workbench" }, { "id": "nav_docs", "type": "url", "url": "https://docs.example.com", "target": "_blank", "label": "Docs" } ] } ``` Requirements: * `id` (snake\_case), `type`, and `label` are mandatory on every item. * The target field must match the type: `objectName`, `pageName`, `dashboardName`, `reportName`, or `url`. Keys like `path` or `kind` are ignored at runtime and rejected at save. * Put items under the `navigation` key. `menu` is deprecated legacy and only kept for backward compatibility. Object entries also support record deep-links — `recordId` (with template variables like `{current_user_id}`) opens a specific record, which is how "My Profile"-style entries are built. ## Quick Checklist [#quick-checklist] Before publishing an app, scan the navigation for: * Pages that merely wrap a single object view → replace with an object entry. * Items carrying `path`/`kind` → rewrite as typed items. * The same object reachable twice (plain entry + wrapper page) → keep one. * View names not following `.`, ids not snake\_case → rename. # Expression System Object UI includes a powerful expression system that enables dynamic, data-driven UIs. Expressions allow you to reference data, compute values, and create conditional logic directly in your JSON schemas. ## Overview [#overview] Expressions are JavaScript-like code snippets embedded in schemas using the `${}` syntax: ```json { "type": "text", "value": "Hello, ${user.name}!" } ``` With data: ```tsx const data = { user: { name: "Alice" } } ``` This renders: **"Hello, Alice!"** ## Basic Syntax [#basic-syntax] ### Simple Property Access [#simple-property-access] Access data properties using dot notation: ```json { "type": "text", "value": "${user.firstName}" } ``` ### Nested Properties [#nested-properties] Access nested objects: ```json { "type": "text", "value": "${user.address.city}" } ``` ### Array Access [#array-access] Access array elements: ```json { "type": "text", "value": "${users[0].name}" } ``` ### String Interpolation [#string-interpolation] Mix expressions with static text: ```json { "type": "text", "value": "Welcome, ${user.firstName} ${user.lastName}!" } ``` ## Operators [#operators] ### Arithmetic Operators [#arithmetic-operators] ```json { "type": "text", "value": "Total: ${price * quantity}" } ``` Supported: `+`, `-`, `*`, `/`, `%` ### Comparison Operators [#comparison-operators] ```json { "type": "badge", "variant": "${score >= 90 ? 'success' : 'default'}" } ``` Supported: `>`, `<`, `>=`, `<=`, `==`, `===`, `!=`, `!==` ### Logical Operators [#logical-operators] ```json { "type": "button", "visibleOn": "${user.isAdmin && user.isActive}" } ``` Supported: `&&`, `||`, `!` ### Ternary Operator [#ternary-operator] ```json { "type": "text", "value": "${count > 0 ? count + ' items' : 'No items'}" } ``` ## Conditional Properties [#conditional-properties] ### visibleOn [#visibleon] Show component when expression is true: ```json { "type": "button", "label": "Admin Panel", "visibleOn": "${user.role === 'admin'}" } ``` ### hiddenOn [#hiddenon] Hide component when expression is true: ```json { "type": "section", "hiddenOn": "${user.settings.hideSection}" } ``` ### disabledOn [#disabledon] Disable component when expression is true: ```json { "type": "button", "label": "Submit", "disabledOn": "${form.submitting || !form.isValid}" } ``` ## Data Context [#data-context] ### Accessing Root Data [#accessing-root-data] The root data object is available directly: ```tsx const data = { user: { name: "Alice" }, settings: { theme: "dark" } } ``` ```json { "type": "text", "value": "Theme: ${settings.theme}" } ``` ### Scoped Data [#scoped-data] Some components provide scoped data: ```json { "type": "list", "items": "${users}", "itemTemplate": { "type": "card", "title": "${item.name}", // 'item' is scoped data "body": "${item.email}" } } ``` ### Index in Loops [#index-in-loops] Access the current index in loops: ```json { "type": "list", "items": "${users}", "itemTemplate": { "type": "text", "value": "#${index + 1}: ${item.name}" } } ``` ## Built-in Functions [#built-in-functions] ### String Functions [#string-functions] ```json { "type": "text", "value": "${user.name.toUpperCase()}" } ``` Available: * `toUpperCase()`, `toLowerCase()` * `trim()`, `trimStart()`, `trimEnd()` * `substring(start, end)` * `replace(search, replace)` * `split(separator)` * `includes(substring)` * `startsWith(prefix)`, `endsWith(suffix)` ### Array Functions [#array-functions] ```json { "type": "text", "value": "Total users: ${users.length}" } ``` ```json { "type": "text", "value": "${users.map(u => u.name).join(', ')}" } ``` Available: * `length` * `map(fn)`, `filter(fn)`, `reduce(fn, initial)` * `join(separator)` * `slice(start, end)` * `includes(item)` * `find(fn)`, `findIndex(fn)` * `some(fn)`, `every(fn)` ### Number Functions [#number-functions] ```json { "type": "text", "value": "Price: ${price.toFixed(2)}" } ``` Available: * `toFixed(decimals)` * `toPrecision(digits)` * `toString()` ### Math Functions [#math-functions] ```json { "type": "text", "value": "${Math.round(average)}" } ``` Available: All standard `Math` functions * `Math.round()`, `Math.floor()`, `Math.ceil()` * `Math.min()`, `Math.max()` * `Math.abs()` * `Math.random()` ### Date Functions [#date-functions] ```json { "type": "text", "value": "${new Date().toLocaleDateString()}" } ``` ## Complex Expressions [#complex-expressions] ### Nested Ternary [#nested-ternary] ```json { "type": "badge", "variant": "${ status === 'active' ? 'success' : status === 'pending' ? 'warning' : status === 'error' ? 'destructive' : 'default' }" } ``` ### Combining Operators [#combining-operators] ```json { "type": "alert", "visibleOn": "${ (user.role === 'admin' || user.role === 'moderator') && user.isActive && !user.isSuspended }" } ``` ### Array Methods [#array-methods] ```json { "type": "text", "value": "${ users .filter(u => u.isActive) .map(u => u.name) .join(', ') }" } ``` ## Practical Examples [#practical-examples] ### User Greeting [#user-greeting] ```json { "type": "text", "value": "${ new Date().getHours() < 12 ? 'Good morning' : new Date().getHours() < 18 ? 'Good afternoon' : 'Good evening' }, ${user.firstName}!" } ``` ### Status Badge [#status-badge] ```json { "type": "badge", "text": "${status}", "variant": "${ status === 'completed' ? 'success' : status === 'in_progress' ? 'info' : status === 'pending' ? 'warning' : 'default' }" } ``` ### Price Formatting [#price-formatting] ```json { "type": "text", "value": "$${(price * quantity).toFixed(2)}" } ``` ### Empty State [#empty-state] ```json { "type": "empty-state", "visibleOn": "${items.length === 0}", "message": "No items to display", "description": "Start by adding your first item" } ``` ### Percentage Bar [#percentage-bar] ```json { "type": "progress", "value": "${(completed / total) * 100}", "label": "${completed} of ${total} completed" } ``` ### Conditional Styling [#conditional-styling] ```json { "type": "card", "className": "${ isPriority ? 'border-red-500 border-2' : isCompleted ? 'opacity-50' : 'border-gray-200' }" } ``` ## Form Expressions [#form-expressions] ### Dependent Fields [#dependent-fields] ```json { "type": "form", "body": [ { "type": "select", "name": "country", "label": "Country", "options": ["USA", "Canada", "Mexico"] }, { "type": "select", "name": "state", "label": "State/Province", "visibleOn": "${form.country === 'USA'}", "options": ["CA", "NY", "TX"] } ] } ``` ### Dynamic Validation [#dynamic-validation] ```json { "type": "input", "name": "email", "label": "Email", "required": true, "validations": { "isEmail": true, "errorMessage": "Please enter a valid email" } } ``` ### Computed Fields [#computed-fields] ```json { "type": "input", "name": "total", "label": "Total", "value": "${form.price * form.quantity}", "disabled": true } ``` ## Performance Considerations [#performance-considerations] ### Expensive Computations [#expensive-computations] Expressions are re-evaluated when data changes. Avoid expensive operations: ```json // ❌ Bad: Complex computation in expression { "type": "text", "value": "${users.map(u => expensiveOperation(u)).join(', ')}" } // ✅ Good: Pre-compute in data ``` ```tsx const data = { processedUsers: users.map(u => expensiveOperation(u)) } ``` ### Caching [#caching] The expression engine automatically caches results when data doesn't change. ## Security [#security] ### Sandboxed Execution [#sandboxed-execution] Expressions run in a sandboxed environment and can only access: * The data context you provide * Built-in JavaScript functions (Math, Date, String, Array methods) They **cannot** access: * Browser APIs (window, document, localStorage) * Node.js APIs (fs, path, etc.) * Global variables * Function constructors ### Sanitization [#sanitization] All expression outputs are automatically sanitized to prevent XSS attacks. ## Debugging Expressions [#debugging-expressions] ### Expression Errors [#expression-errors] Invalid expressions show helpful error messages: ```json { "type": "text", "value": "${user.invalidProperty}" } ``` Error: "Cannot read property 'invalidProperty' of undefined" ### Debug Mode [#debug-mode] Enable debug mode to see expression evaluation: ```tsx ``` This logs all expression evaluations to the console. ## Advanced Usage [#advanced-usage] ### Custom Functions [#custom-functions] Extend the expression context with custom functions: ```tsx import { getExpressionEvaluator } from '@object-ui/core' const evaluator = getExpressionEvaluator() evaluator.registerFunction('formatCurrency', (value: number) => { return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(value) }) ``` Use in schemas: ```json { "type": "text", "value": "${formatCurrency(price)}" } ``` ### Custom Operators [#custom-operators] Register custom operators for domain-specific logic: ```tsx evaluator.registerOperator('contains', (array, item) => { return array.includes(item) }) ``` ```json { "type": "button", "visibleOn": "${user.permissions contains 'admin'}" } ``` ## Best Practices [#best-practices] ### 1. Keep Expressions Simple [#1-keep-expressions-simple] ```json // ❌ Bad: Too complex { "value": "${users.filter(u => u.age > 18).map(u => ({...u, isAdult: true})).reduce((acc, u) => acc + u.score, 0)}" } // ✅ Good: Pre-compute complex logic { "value": "${adultUsersScore}" } ``` ### 2. Use Meaningful Variable Names [#2-use-meaningful-variable-names] ```json // ❌ Bad { "visibleOn": "${x && y || z}" } // ✅ Good { "visibleOn": "${isAdmin && isActive || isSuperUser}" } ``` ### 3. Handle Null/Undefined [#3-handle-nullundefined] ```json // ❌ Bad: Might throw error { "value": "${user.address.city}" } // ✅ Good: Safe access { "value": "${user.address?.city || 'N/A'}" } ``` ### 4. Use TypeScript [#4-use-typescript] Define your data types: ```tsx interface UserData { user: { name: string role: 'admin' | 'user' isActive: boolean } } const data: UserData = { /* ... */ } ``` ## Next Steps [#next-steps] * [Schema Rendering](./schema-rendering.md) - Learn the rendering engine * [Component Registry](./component-registry.md) - Understand components * [Protocol Overview](/protocol/overview) - Explore schema specifications ## Related Documentation [#related-documentation] * [Core API](/api/core) - Expression evaluator API * [Form Protocol](/protocol/form) - Form-specific expressions * [View Protocol](/protocol/view) - Data view expressions # Field Registry Object UI uses a **Field Registry** system to decouple the core engine from specific UI implementations of fields. This allows for rich extensibility and plugin support. ## Concept [#concept] The `@object-ui/fields` package serves as the "Universal Language" for rendering values. When a component like `` needs to render a `date` field, it doesn't import a DatePicker directly. Instead, it asks the registry: > *"Hey, give me the component responsible for rendering type 'date'."* This architecture allows you to: 1. **Override standard fields** (e.g. replace the native date picker with a fancy one). 2. **Add new field types** (e.g. add a `rating` or `signature` field). 3. **Keep bundles small** (heavy components like Code Editors are loaded only if their plugin is registered). ## Usage [#usage] ### 1. Registering a Custom Field [#1-registering-a-custom-field] You can register a custom renderer globally, typically at your app's entry point. ```tsx // src/setup.tsx import { registerFieldRenderer, type CellRendererProps } from '@object-ui/fields'; const MyRatingField = ({ value, onChange }: CellRendererProps) => { return (
{[1, 2, 3, 4, 5].map(star => ( onChange(star)} style={{ color: star <= value ? 'gold' : 'grey' }} > ★ ))}
); }; // Register it registerFieldRenderer('rating', MyRatingField); ``` ### 2. Using in Schema [#2-using-in-schema] Once registered, you can simply use the new type in your JSON schema. ```json { "type": "form", "fields": [ { "name": "customer_satisfaction", "type": "rating", "label": "Satisfaction" } ] } ``` ## Standard Fields [#standard-fields] Object UI comes with built-in support for the standard [ObjectStack Protocol](/protocol) types: | Type | Description | | --------------- | ------------------------------------------------------------------------------------ | | `text` | Single line text | | `textarea` | Multi-line text | | `number` | Numeric input | | `currency` | Currency formatting | | `percent` | Percentage values | | `date` | Date picker | | `datetime` | Date & Time picker | | `boolean` | Checkbox / Switch | | `select` | Dropdown | | `lookup` | Reference to another object | | `master_detail` | Parent-child relationship | | `user` | Person picker — searches the `sys_user` object (a lookup specialized to users) | | `owner` | Record owner — a `user` field, typically read-only and stamped with the current user | ## Using Renderers in Custom Components [#using-renderers-in-custom-components] If you are building your own custom component (like a Kanban board card), you can leverage the registry to render fields without reinventing the wheel. ```tsx import { getCellRenderer } from '@object-ui/fields'; export const KanbanCard = ({ task }) => { // Get the standard renderer for a 'user' type field const UserRenderer = getCellRenderer('user'); return (

{task.name}

); }; ``` # Flow Designer # Flow Designer [#flow-designer] The **Flow Designer** is the visual editor for `flow` metadata in the console's metadata admin. It renders a flow's nodes and edges on a pan/zoom canvas so you can assemble automation — create/update records, branch on conditions, wait for events, call APIs, route for approval — without hand-editing JSON. The designer is a thin, **spec-driven** view over `flow` metadata: everything you drop on the canvas is a node in the flow's schema, so the same document runs unchanged on the automation engine. Open it from a `flow` record's **Design** tab. The **Form** tab holds the same flow as an editable tree; the two stay in sync. ## The canvas [#the-canvas] Each node is a card showing its icon, label, `type`, and a one-line summary of its config. Edges are the arrows between them. | Gesture | Result | | ------------------------------------------ | ------------------------------------ | | Click a card | Select it (opens the node inspector) | | Drag a card | Reposition it | | Hover a card → click the bottom **+** | Append a connected child node | | Zoom controls / **Fit** | Scale and re-center the graph | Structural problems (an undeclared cycle, an unreachable node) surface three ways at once: a red ring on the offending card, a badge in its corner, and an inline banner at the top-left of the canvas. Clicking a banner row selects and pans to the element it refers to. ## Adding nodes — the palette [#adding-nodes--the-palette] The **Add node** button (top-right, edit mode only) opens the node palette: a searchable, grouped list of every node type the flow can use. ### Search and keyboard [#search-and-keyboard] Type in the box at the top to filter across **all** categories at once — the match is a case-insensitive substring test over each node's **label**, **hint**, and **type**, so `scr` finds *Screen*, `http` finds *HTTP request*, and a word that only appears in a hint (`concurrently` → *Parallel*) still surfaces the node. Clearing the box restores the full grouped list. The palette is keyboard-navigable end to end: | Key | Action | | --------- | --------------------------------- | | `↑` / `↓` | Move the highlight (wraps around) | | `Enter` | Insert the highlighted node | | `Esc` | Close the palette | The search box autofocuses when the palette opens, so you can open, type a few letters, and press `Enter` without touching the mouse. ### Categories [#categories] Nodes are grouped into five sections, in this order: | Category | Node types | | --------------- | ---------------------------------------------------- | | **Data** | Create / Update / Get / Delete record | | **Logic** | Decision, Loop, Set variables, Parallel, Try / Catch | | **Human** | Approval, Screen | | **Integration** | HTTP request, Connector, Script | | **Flow** | Subflow, Wait, End | Section headings are localized to the active console language (e.g. *数据 / 逻辑 / 人工 / 集成 / 流程* in Chinese); the underlying node types are unchanged. ### Recently used [#recently-used] When the search box is empty, a **Recently used** group tops the list with the node types you inserted most recently (up to five, most-recent first) — so the nodes you reach for repeatedly stop needing a scroll or a search. The list is per-user and, when the console is connected to a backend, syncs across devices (see [User-Scoped State Persistence](/docs/guide/user-state-persistence)); it falls back to browser-local storage when offline. Types from a plugin that was since uninstalled drop out of the list automatically. ### Server-merged, plugin-extensible [#server-merged-plugin-extensible] The palette is **server-driven**. Beyond the built-in node types, the running engine publishes its registered actions at `GET /api/v1/automation/actions`, and plugins contribute their own node types there (for example an `approval` node from an approvals plugin, or third-party `connector_action` providers). The designer overlays those descriptors onto the built-in list — adopting the engine's labels and descriptions and appending engine-only types — so the palette always matches what the connected backend actually supports. Plugin nodes are searchable exactly like built-ins, including by their registered `type`. When the endpoint is unreachable the designer falls back to its built-in defaults, so authoring still works offline. ## The node inspector [#the-node-inspector] Selecting a node opens its inspector: **ID**, **Label**, **Node Type**, an optional **Description**, and a **Configuration** section. New nodes start with spec-valid defaults (a *Wait* node already carries a timer config, an *HTTP* node defaults to `GET`) so a freshly dropped block is never in a broken intermediate state. For node types whose engine executor publishes a `configSchema` (ADR-0018), the inspector renders a **server-driven property form** from that schema — so a plugin's node gets a real config UI without the designer hardcoding its fields. A **Decision** node's Branches editor defines each branch's label, CEL expression, **and target node** in one table: the **Target** column picks the downstream node, wiring (creating, retargeting, or detaching) the branch's outgoing edge with its condition, label, and default flag. The same binding can also be edited from the edge side — select a connector and use its **Branch** picker — and the two stay in sync, because the routing always lives on the edges. ## Validate, simulate, inspect runs [#validate-simulate-inspect-runs] The toolbar toggles four side panels: | Panel | What it shows | | ------------- | ------------------------------------------------------------------------------------------- | | **Variables** | The flow's declared variables | | **Problems** | Structural + server validation issues, each clickable to reveal on canvas | | **Debug** | A step-through **simulator** that walks the graph and highlights the active / visited nodes | | **Runs** | Execution history for the published flow, fetched from the engine | ## See also [#see-also] * [User-Scoped State Persistence](/docs/guide/user-state-persistence) — how the "Recently used" list is stored and synced. * [Console App](/docs/guide/console) — the reference app that hosts the metadata admin and its designers. # Guide # ObjectUI Guide [#objectui-guide] Welcome to the ObjectUI Guide! This comprehensive guide covers everything you need to know to build powerful server-driven UIs with ObjectUI. ## Examples [#examples] * [examples/server](examples/server) - Run a metadata app using @objectstack/cli. ### Run the ObjectStack Server Example [#run-the-objectstack-server-example] ```bash pnpm --filter @object-ui/example-objectstack-server serve ``` # Layout System # Layout System [#layout-system] ObjectUI provides a comprehensive layout system through the `@object-ui/layout` package. This guide explains how to use layout components to build professional application structures. ## Overview [#overview] The layout system provides: * **AppShell** - Full application container with header, sidebar, and content areas * **Page** - Individual page wrapper with header and body * **PageHeader** - Consistent page headers with title, breadcrumbs, and actions * **SidebarNav** - Navigation sidebar with menu items ## Installation [#installation] The layout package is included in the core ObjectUI installation: ```bash npm install @object-ui/react ``` Layout components are automatically registered when you import ObjectUI. ## AppShell Component [#appshell-component] The `AppShell` provides a complete application structure with header, sidebar, and main content area. ### Basic Usage [#basic-usage] ```json { "type": "app-shell", "header": { "type": "header-bar", "title": "My Application" }, "sidebar": { "type": "sidebar-nav", "items": [ { "label": "Dashboard", "href": "/dashboard", "icon": "layout-dashboard" }, { "label": "Users", "href": "/users", "icon": "users" }, { "label": "Settings", "href": "/settings", "icon": "settings" } ] }, "body": { "type": "page", "title": "Dashboard", "body": { "type": "text", "value": "Welcome to the dashboard!" } } } ``` ### Schema API [#schema-api] ```typescript { type: 'app-shell', // Header configuration header?: ComponentSchema, // Sidebar configuration sidebar?: ComponentSchema, sidebarCollapsible?: boolean, // Allow sidebar collapse sidebarDefaultOpen?: boolean, // Initial sidebar state // Main content body: ComponentSchema, // Styling className?: string, headerClassName?: string, sidebarClassName?: string, contentClassName?: string } ``` ### Features [#features] * Responsive layout that adapts to mobile/tablet/desktop * Collapsible sidebar with state management * Sticky header * Scroll management for content area * Consistent spacing and structure ## Page Component [#page-component] The `Page` component provides a consistent wrapper for individual pages with optional headers. ### Basic Usage [#basic-usage-1] ```json { "type": "page", "title": "User Management", "description": "Manage users and permissions", "body": { "type": "container", "children": [ { "type": "text", "value": "User list goes here" } ] } } ``` ### With Action Buttons [#with-action-buttons] ```json { "type": "page", "title": "Products", "actions": [ { "type": "button", "text": "Add Product", "variant": "default", "icon": "plus" }, { "type": "button", "text": "Export", "variant": "outline", "icon": "download" } ], "body": { "type": "object-grid", "object": "products" } } ``` ### Schema API [#schema-api-1] ```typescript { type: 'page', // Header title?: string, // Page title description?: string, // Page description/subtitle icon?: string, // Optional icon breadcrumbs?: Array<{ // Breadcrumb navigation label: string, href?: string }>, actions?: ComponentSchema[], // Action buttons // Content body: ComponentSchema, // Main page content // Layout options maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full', padding?: boolean, // Add padding (default: true) // Styling className?: string, headerClassName?: string, bodyClassName?: string } ``` ### Max Width Options [#max-width-options] Control page content width: ```json { "type": "page", "title": "Settings", "maxWidth": "lg", // Centered content with max width "body": { "type": "form", "fields": [...] } } ``` Available values: * `sm` - 640px * `md` - 768px * `lg` - 1024px * `xl` - 1280px * `2xl` - 1536px * `full` - No maximum width (default) ## PageHeader Component [#pageheader-component] The `PageHeader` provides consistent page headers with title, breadcrumbs, and actions. ### Usage [#usage] ```json { "type": "page-header", "title": "Customer Details", "description": "View and edit customer information", "breadcrumbs": [ { "label": "Home", "href": "/" }, { "label": "Customers", "href": "/customers" }, { "label": "John Doe" } ], "actions": [ { "type": "button", "text": "Edit", "variant": "default", "icon": "pencil" }, { "type": "button", "text": "Delete", "variant": "destructive", "icon": "trash" } ] } ``` ### Schema API [#schema-api-2] ```typescript { type: 'page-header', title: string, description?: string, icon?: string, breadcrumbs?: Array<{ label: string, href?: string, icon?: string }>, actions?: ComponentSchema[], className?: string } ``` ## SidebarNav Component [#sidebarnav-component] The `SidebarNav` provides a collapsible navigation sidebar with menu items. ### Basic Usage [#basic-usage-2] ```json { "type": "sidebar-nav", "items": [ { "label": "Dashboard", "href": "/dashboard", "icon": "layout-dashboard" }, { "label": "Users", "href": "/users", "icon": "users", "badge": "12" }, { "label": "Reports", "icon": "bar-chart", "items": [ { "label": "Sales", "href": "/reports/sales" }, { "label": "Analytics", "href": "/reports/analytics" } ] } ] } ``` ### Schema API [#schema-api-3] ```typescript { type: 'sidebar-nav', items: Array<{ label: string, href?: string, icon?: string, badge?: string | number, badgeVariant?: 'default' | 'destructive' | 'outline', items?: Array<...>, // Nested menu items (collapsible children) active?: boolean, disabled?: boolean }>, // Items can also be grouped using NavGroup: // items: Array<{ label: string, items: NavItem[] }> collapsible?: boolean, defaultOpen?: boolean, searchEnabled?: boolean, // Show search input to filter navigation searchPlaceholder?: string, // Placeholder for search input className?: string } ``` ### Features [#features-1] * Nested menu items (2 levels) with collapsible expand/collapse * Active state highlighting via React Router * Icon support (Lucide icons) * Badge/counter support with variant styling (`default`, `destructive`, `outline`) * NavGroup support for grouped navigation sections * Built-in search filtering (`searchEnabled`) across all items and children * Collapse/expand animation ## Common Layout Patterns [#common-layout-patterns] ### Full Application Layout [#full-application-layout] ```json { "type": "app-shell", "header": { "type": "header-bar", "title": "My App", "logo": "/logo.png", "actions": [ { "type": "button", "text": "Profile", "variant": "ghost" } ] }, "sidebar": { "type": "sidebar-nav", "items": [ { "label": "Home", "href": "/", "icon": "home" }, { "label": "Products", "href": "/products", "icon": "package" }, { "label": "Orders", "href": "/orders", "icon": "shopping-cart" } ] }, "sidebarCollapsible": true, "sidebarDefaultOpen": true, "body": { "type": "page", "title": "Dashboard", "body": { "type": "object-grid", "object": "orders" } } } ``` ### Landing Page (No Sidebar) [#landing-page-no-sidebar] ```json { "type": "app-shell", "header": { "type": "header-bar", "title": "Welcome" }, "body": { "type": "container", "className": "mx-auto py-12", "children": [ { "type": "text", "value": "Landing page content" } ] } } ``` ### Settings Page with Tabs [#settings-page-with-tabs] ```json { "type": "page", "title": "Settings", "maxWidth": "2xl", "body": { "type": "tabs", "tabs": [ { "label": "General", "value": "general", "content": { "type": "form", "fields": [...] } }, { "label": "Security", "value": "security", "content": { "type": "form", "fields": [...] } }, { "label": "Notifications", "value": "notifications", "content": { "type": "form", "fields": [...] } } ] } } ``` ### Detail Page with Actions [#detail-page-with-actions] ```json { "type": "page", "title": "${record.name}", "breadcrumbs": [ { "label": "Home", "href": "/" }, { "label": "Customers", "href": "/customers" }, { "label": "${record.name}" } ], "actions": [ { "type": "button", "text": "Edit", "variant": "default", "icon": "pencil", "onClick": "editRecord" }, { "type": "button", "text": "Delete", "variant": "destructive", "icon": "trash", "onClick": "deleteRecord" } ], "body": { "type": "card", "children": [ { "type": "text", "value": "Record details..." } ] } } ``` ## Responsive Behavior [#responsive-behavior] Layout components automatically adapt to different screen sizes: ### Desktop (≥1024px) [#desktop-1024px] * Full sidebar visible * Header spans full width * Content area uses remaining space ### Tablet (768px - 1023px) [#tablet-768px---1023px] * Collapsible sidebar (overlay mode) * Full header * Content uses most of screen ### Mobile (\<768px) [#mobile-768px] * Hidden sidebar (toggle button in header) * Compact header * Full-width content ## Styling and Customization [#styling-and-customization] ### Custom Classes [#custom-classes] Add Tailwind classes to layout components: ```json { "type": "app-shell", "className": "custom-app", "headerClassName": "bg-primary text-primary-foreground", "sidebarClassName": "bg-muted", "contentClassName": "bg-background", "body": {...} } ``` ### Page Padding [#page-padding] Control page content padding: ```json { "type": "page", "padding": false, // Remove default padding "body": { "type": "container", "className": "p-8", // Custom padding "children": [...] } } ``` ## Best Practices [#best-practices] ### 1. Consistent Structure [#1-consistent-structure] Use the same layout structure across your app: ```json // Every page should follow this pattern { "type": "app-shell", "header": { ... }, "sidebar": { ... }, "body": { "type": "page", "title": "...", "body": { ... } } } ``` ### 2. Breadcrumbs for Deep Navigation [#2-breadcrumbs-for-deep-navigation] Add breadcrumbs to help users navigate: ```json { "breadcrumbs": [ { "label": "Home", "href": "/" }, { "label": "Products", "href": "/products" }, { "label": "Electronics", "href": "/products/electronics" }, { "label": "Laptops" } ] } ``` ### 3. Action Buttons in Headers [#3-action-buttons-in-headers] Place primary actions in page headers: ```json { "type": "page", "title": "Orders", "actions": [ { "type": "button", "text": "New Order", "variant": "default" } ] } ``` ### 4. Max Width for Forms [#4-max-width-for-forms] Use constrained width for forms and reading content: ```json { "type": "page", "maxWidth": "lg", // Better for forms "body": { "type": "form", "fields": [...] } } ``` ### 5. Sidebar Organization [#5-sidebar-organization] Group related items in the sidebar: ```json { "items": [ { "label": "Dashboard", "icon": "home", "href": "/" }, { "label": "divider" }, // Visual separator { "label": "Sales", "icon": "dollar-sign", "items": [ { "label": "Orders", "href": "/orders" }, { "label": "Invoices", "href": "/invoices" } ]}, { "label": "divider" }, { "label": "Settings", "icon": "settings", "href": "/settings" } ] } ``` ## Related Documentation [#related-documentation] * [Components Overview](/docs/components) - All available components * [Schema Rendering](/docs/guide/schema-rendering) - How schemas work * [Architecture Overview](/docs/guide/architecture) - System architecture # Metadata Diagnostics # Metadata Diagnostics [#metadata-diagnostics] Every metadata item shipped by a package — `object`, `view`, `report`, `dashboard`, `flow`, `app`, … — is validated against its Zod schema when the framework loads it. The validation result travels alongside the item as a `_diagnostics` envelope, and Studio surfaces it at four levels so authors and operators can fix problems without grepping logs. > **Backend agnostic.** The shape and the REST endpoint described below > are part of the ObjectStack protocol. Studio is one consumer; any > custom UI built on `@object-ui/data-objectstack` can render the same > envelope. ## The `_diagnostics` envelope [#the-_diagnostics-envelope] ```ts interface MetadataDiagnostics { valid: boolean; errors?: Array<{ path: string; message: string; code?: string }>; warnings?: Array<{ path: string; message: string; code?: string }>; } ``` * `valid === false` means **at least one error** — features that depend on the item (rendering, queries, automation) are unsafe to use. * `warnings[]` is advisory — items remain `valid: true` but operators should review (deprecations, performance hints, missing-but-defaultable fields). * `path` is dot-delimited, matching the same convention Zod uses (`fields.email.type`, `columns.0.bind`). The envelope is attached to: | Endpoint | Where the envelope lives | | :------------------------------------------------ | :------------------------------------ | | `GET /api/v1/meta/items/:type` | Each list entry (`item._diagnostics`) | | `GET /api/v1/meta/items/:type/:name` | Top-level (`item._diagnostics`) | | `GET /api/v1/meta/items/:type/:name?layered=true` | `effective._diagnostics` | | `GET /api/v1/meta/diagnostics` | Sweep — see next section | ## The diagnostics sweep endpoint [#the-diagnostics-sweep-endpoint] `GET /api/v1/meta/diagnostics` runs validation across **every metadata type and item** in one round-trip. It powers the governance overview page and the per-type tile badges. ```http GET /api/v1/meta/diagnostics?severity=error ``` | Query | Default | Effect | | :--------- | :------ | :----------------------------------------------------------------------------------- | | `severity` | `error` | `error` returns invalid items only; `warning` also returns items with only warnings. | | `type` | — | Limit to a single metadata type. | | `package` | — | Limit to one package id. | Response: ```ts interface MetadataDiagnosticsSummary { entries: Array<{ type: string; name: string; diagnostics: MetadataDiagnostics; }>; total: number; // entries.length scannedTypes: number; // how many metadata types were checked scannedItems: number; // how many items were checked in total /** * Per-type aggregate stats — count of items and the list of * packages contributing to each type. Computed in the same sweep so * directory tiles render counts and a package filter without * additional round-trips. Empty `{}` on framework versions older * than the 7.x line. */ stats: Record; } ``` Use this as a CI gate too — `total === 0` is the green-build condition. ## Studio UI surfaces [#studio-ui-surfaces] ### 1. Directory page badges [#1-directory-page-badges] `/apps//metadata` — the directory is **scoped to the active project software package** (the sidebar `active_package` selector, published as `?package=`). Only metadata types that the selected project package contributes are listed — system/cloud types never appear, and there is no in-page "All packages" dropdown. If the URL holds no valid project package the page repairs it to the first available one. Each visible type tile shows: * A neutral count badge with the total items of that type. (Note: this total spans all packages — the per-type *list* page it links to is strictly scoped to the active project package.) * A red ⚠ + count when any items fail validation (errors). * An amber ⚠ + count when items have warnings but no errors. Tiles deep-link into the list page carrying the active `?package=`, so the scope survives navigation. The "View all issues (N)" link in the filter row jumps straight to the governance page. ### 2. Resource list rows [#2-resource-list-rows] `/apps//metadata/` — invalid rows get a red ⚠ icon next to the name and a destructive-tinted background; warning-only rows get an amber ⚠ and amber tint. The list header shows aggregate "Invalid N" and "Warnings N" chips. Hover the ⚠ for the first three messages. The list page is **always scoped to a single project software package**. Studio's sidebar exposes a mandatory **Package** scope selector (the app's `active_package` context selector) whose options are the installed *project* packages — system/cloud packages are never offered and there is no "All" choice. The selection is published as the `?package=` URL parameter, which every metadata list reads to filter rows by their `_packageId`. If the URL holds no valid project package the list repairs it to the first available one, so system metadata never leaks into the view. (The page no longer renders its own per-type package dropdown — scope is owned solely by the sidebar selector.) ### 3. Resource edit banners [#3-resource-edit-banners] `/apps//metadata//` — a destructive banner at the top of the edit page lists the first three errors with their paths; the same errors are also threaded into the form so the offending fields get inline messages **without** the user having to click Save first. Warnings, when present, render as a parallel amber banner. Edits clear the matching diagnostic immediately — the inline error on a field disappears as soon as you start typing in it, then re-validates on save. For **object** drafts the live validation goes beyond the Zod shape check: every field conditional rule (`visibleWhen` / `readonlyWhen` / `requiredWhen`, plus the deprecated `conditionalRequired` alias) is linted as a CEL predicate with the same `@objectstack/formula` validators the server uses. A predicate that parses but references an unknown field, or references a field bare instead of as `record.`, surfaces under its `fields..` path in the banner. The field inspector's *Conditional rules* editors give the same verdict inline as you type — with autocomplete for the object's fields (after `record.` / `previous.`), the runtime-bound scope roots (`record`, `previous`, `parent`), and the CEL stdlib. Formula fields get the same treatment for their value `expression`: the inline editor lints it in `role: 'value'` mode and shows the **inferred result type** (only a proven-Number formula is offered as a dataset measure), while the draft-wide pass surfaces a broken formula on any field under its `fields..expression` path. ### 4. Governance overview page [#4-governance-overview-page] `/apps//metadata/_diagnostics` — a single sortable table of every invalid item across every type, grouped by type, with deep-links to the offending edit page. Toggle the severity tab to include warning-only items. This is the page to open during a release readiness review. ## Authoring metadata that validates cleanly [#authoring-metadata-that-validates-cleanly] Validation rules are defined by the Zod schemas in `@objectstack/spec`. A few high-leverage patterns: * **Use `defineObject`, `defineView`, … helpers** from `@objectstack/spec` — TypeScript catches most shape issues at compile time before they ever reach the diagnostics path. * **Run `os check`** locally before publishing. It calls the same validators the server uses on load. * **Treat warnings like errors in CI.** Pass `severity=warning` to the sweep endpoint and assert `total === 0`. * **Layered overlays merge first, then validate.** If only your runtime overlay fails, the source artifact is fine — the bad value is in the overlay. The edit banner reflects the *effective* item, so what you see is what features will get. ## Client SDK [#client-sdk] ```ts import { MetadataClient } from '@object-ui/data-objectstack'; const client = new MetadataClient(/* config */); const summary = await client.diagnostics({ severity: 'error' }); console.log(summary.total, 'invalid item(s)'); ``` The hook used by the Studio surfaces: ```ts import { useGlobalDiagnostics, useMetadataClient } from '@object-ui/app-shell'; const client = useMetadataClient(); const { loading, error, summary, byType, // Record warnByType, // Record (severity='warning' only) countsByType, // Record packagesByType, // Record allPackages, // packageId[] — deduped union for filter dropdowns reload, } = useGlobalDiagnostics(client, 'warning'); ``` Pass `severity: 'warning'` when you need `warnByType` populated — the server omits warning-only entries when the default `'error'` severity is in effect. # ObjectOS Integration Guide # ObjectOS Integration Guide [#objectos-integration-guide] ObjectUI is designed to be the official frontend renderer for the **ObjectOS** ecosystem. This guide provides comprehensive instructions for integrating ObjectUI components with ObjectOS, ObjectStack, and building enterprise applications. ## Overview [#overview] ObjectUI serves as the **UI Layer** in the ObjectOS architecture: ``` ┌─────────────────────────────────────────────────┐ │ ObjectUI (UI Layer) │ │ React Components + Tailwind + Schema Rendering │ └──────────────────┬──────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ ObjectStack (Runtime Layer) │ │ Kernel + Plugins + ObjectQL + Data Drivers │ └──────────────────┬──────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────┐ │ ObjectOS (Platform Layer) │ │ Multi-tenant + RBAC + System Objects + APIs │ └─────────────────────────────────────────────────┘ ``` ## Quick Start Integration [#quick-start-integration] ### 1. Install Dependencies [#1-install-dependencies] ```bash # Core ObjectUI packages pnpm add @object-ui/react @object-ui/components @object-ui/fields # ObjectStack runtime pnpm add @objectstack/core @objectstack/runtime @objectstack/objectql # Data adapter pnpm add @object-ui/data-objectstack # Optional: Plugins as needed pnpm add @object-ui/plugin-form @object-ui/plugin-grid ``` ### 2. Set Up ObjectStack Kernel [#2-set-up-objectstack-kernel] ```typescript // src/kernel.ts import { Kernel } from '@objectstack/runtime'; import { ObjectQLPlugin } from '@objectstack/objectql'; import { AppPlugin } from '@objectstack/plugin-app'; import { HonoServerPlugin } from '@objectstack/plugin-hono-server'; export async function createKernel() { const kernel = new Kernel(); // Register essential plugins kernel.registerPlugin(new ObjectQLPlugin()); kernel.registerPlugin(new AppPlugin()); kernel.registerPlugin(new HonoServerPlugin({ port: 3000, cors: true })); await kernel.start(); return kernel; } ``` ### 3. Create ObjectStack Configuration [#3-create-objectstack-configuration] ```typescript // objectstack.config.ts import type { AppManifest } from '@objectstack/spec'; export const manifest: AppManifest = { name: 'my-app', version: '1.0.0', description: 'My ObjectUI + ObjectOS Application', // Define your data objects objects: { contact: { name: 'contact', label: 'Contact', fields: { name: { name: 'name', label: 'Name', type: 'text', required: true }, email: { name: 'email', label: 'Email', type: 'email', required: true }, phone: { name: 'phone', label: 'Phone', type: 'phone' }, company: { name: 'company', label: 'Company', type: 'text' }, status: { name: 'status', label: 'Status', type: 'select', options: [ { label: 'Active', value: 'active' }, { label: 'Inactive', value: 'inactive' } ] } } } }, // Define UI pages pages: [ { name: 'contacts', label: 'Contacts', path: '/contacts', icon: 'users', component: { type: 'object-view', objectName: 'contact', viewTypes: ['grid', 'kanban', 'calendar'] } } ], // Define app navigation navigation: { items: [ { label: 'Dashboard', path: '/', icon: 'home' }, { label: 'Contacts', path: '/contacts', icon: 'users' } ] } }; export default manifest; ``` ### 4. Set Up Frontend with Console [#4-set-up-frontend-with-console] ```typescript // src/index.tsx import React from 'react'; import ReactDOM from 'react-dom/client'; import { SchemaRenderer } from '@object-ui/react'; import { ObjectStackAdapter } from '@object-ui/data-objectstack'; // Import required plugins import '@object-ui/components'; import '@object-ui/fields'; import '@object-ui/plugin-form'; import '@object-ui/plugin-grid'; // Initialize ObjectStack adapter const dataSource = new ObjectStackAdapter({ baseUrl: 'http://localhost:3000/api' }); function App() { return (
); } ReactDOM.createRoot(document.getElementById('root')!).render( ); ``` ## ObjectOS-Specific Features [#objectos-specific-features] ### Multi-Tenancy Support [#multi-tenancy-support] ```typescript // Configure tenant isolation const adapter = new ObjectStackAdapter({ baseUrl: 'http://localhost:3000/api', headers: { 'X-Tenant-ID': 'tenant-123', 'X-Workspace-ID': 'workspace-456' } }); ``` ### Role-Based Access Control (RBAC) [#role-based-access-control-rbac] ```typescript // Define permissions in object schema { objects: { contact: { name: 'contact', label: 'Contact', permissions: { create: ['admin', 'sales'], read: ['admin', 'sales', 'support'], update: ['admin', 'sales'], delete: ['admin'] }, fields: { salary: { name: 'salary', label: 'Salary', type: 'currency', permissions: { read: ['admin', 'hr'], update: ['admin', 'hr'] } } } } } } ``` ### System Objects Integration [#system-objects-integration] ObjectOS provides system objects like `sys_user`, `sys_organization`, `sys_role`, etc. Integrate them in your UI: ```typescript { type: 'object-view', objectName: 'sys_user', dataSource: objectStackAdapter, viewTypes: ['grid'], fieldNames: ['username', 'email', 'role', 'status', 'last_login'] } ``` ### Workflow Integration [#workflow-integration] ```typescript // Define workflow-enabled object { objects: { opportunity: { name: 'opportunity', label: 'Opportunity', workflow: { enabled: true, states: ['lead', 'qualified', 'proposal', 'negotiation', 'closed_won', 'closed_lost'], transitions: [ { from: 'lead', to: 'qualified', label: 'Qualify', role: ['sales'] }, { from: 'qualified', to: 'proposal', label: 'Create Proposal', role: ['sales'] }, { from: 'proposal', to: 'negotiation', label: 'Negotiate', role: ['sales', 'manager'] }, { from: 'negotiation', to: 'closed_won', label: 'Close Won', role: ['manager'] }, { from: 'negotiation', to: 'closed_lost', label: 'Close Lost', role: ['manager'] } ] }, fields: { // ... field definitions } } } } ``` ## Data Layer Integration [#data-layer-integration] ### Using ObjectQL for Queries [#using-objectql-for-queries] ```typescript import { ObjectStackAdapter } from '@object-ui/data-objectstack'; const dataSource = new ObjectStackAdapter({ baseUrl: 'http://localhost:3000/api' }); // ObjectQL queries are automatically handled const schema = { type: 'object-grid', objectName: 'contact', dataSource, // ObjectQL filter syntax filter: { and: [ { field: 'status', operator: 'eq', value: 'active' }, { field: 'created_date', operator: 'gte', value: '2024-01-01' } ] }, // ObjectQL sorting sort: [ { field: 'created_date', order: 'desc' } ] }; ``` ### Custom Data Hooks [#custom-data-hooks] ```typescript // Implement custom hooks for data operations import { useObjectQuery, useObjectMutation } from '@object-ui/data-objectstack'; function ContactList() { const { data, loading, error } = useObjectQuery('contact', { filter: { field: 'status', operator: 'eq', value: 'active' }, sort: [{ field: 'name', order: 'asc' }], page: 1, pageSize: 20 }); const { mutate: createContact } = useObjectMutation('contact', 'create'); const handleCreate = async (formData: any) => { await createContact(formData); }; if (loading) return
Loading...
; if (error) return
Error: {error.message}
; return ( ); } ``` ## Deployment Strategies [#deployment-strategies] ### Strategy 1: Monolithic Deployment [#strategy-1-monolithic-deployment] Deploy ObjectUI and ObjectStack together in a single Node.js process: ```typescript // server.ts import { createKernel } from './kernel'; import { ConsolePlugin } from './console-plugin'; async function start() { const kernel = await createKernel(); // Register console UI plugin kernel.registerPlugin(new ConsolePlugin()); console.log('🚀 Server started at http://localhost:3000'); console.log('📊 Console UI at http://localhost:3000/console'); } start(); ``` ### Strategy 2: Microservices Deployment [#strategy-2-microservices-deployment] Deploy ObjectUI (frontend) and ObjectStack (backend) separately: **Backend (ObjectStack API):** ```typescript // backend/server.ts import { createKernel } from './kernel'; async function start() { const kernel = await createKernel(); console.log('🚀 API Server started at http://localhost:3000'); } start(); ``` **Frontend (ObjectUI):** ```typescript // frontend/src/config.ts export const config = { apiBaseUrl: process.env.VITE_API_URL || 'http://localhost:3000/api' }; // frontend/src/index.tsx const dataSource = new ObjectStackAdapter({ baseUrl: config.apiBaseUrl }); ``` ### Strategy 3: Cloud-Native Deployment [#strategy-3-cloud-native-deployment] Deploy on Kubernetes with separate services: ```yaml # k8s/deployment.yaml apiVersion: v1 kind: Service metadata: name: objectstack-api spec: selector: app: objectstack-api ports: - port: 3000 --- apiVersion: v1 kind: Service metadata: name: objectui-frontend spec: selector: app: objectui-frontend ports: - port: 80 --- apiVersion: apps/v1 kind: Deployment metadata: name: objectstack-api spec: replicas: 3 template: spec: containers: - name: api image: myregistry/objectstack-api:latest env: - name: DATABASE_URL value: postgresql://... --- apiVersion: apps/v1 kind: Deployment metadata: name: objectui-frontend spec: replicas: 2 template: spec: containers: - name: frontend image: myregistry/objectui-frontend:latest env: - name: API_URL value: http://objectstack-api:3000 ``` ## Advanced Integration Patterns [#advanced-integration-patterns] ### Custom Component Registration [#custom-component-registration] ```typescript // Register custom components with ObjectUI import { ComponentRegistry } from '@object-ui/core'; ComponentRegistry.register('my-custom-widget', MyCustomWidget, { namespace: 'custom', lazy: true }); // Use in schema { type: 'my-custom-widget', config: { // custom props } } ``` ### Event Handling & Callbacks [#event-handling--callbacks] ```typescript { type: 'object-grid', objectName: 'contact', dataSource, callbacks: { onRowClicked: (event) => { // Navigate to detail page window.location.href = `/contact/${event.data.id}`; }, onCellValueChanged: async (event) => { // Auto-save on edit await dataSource.update('contact', event.data.id, { [event.column.field]: event.newValue }); } } } ``` ### Real-time Updates with WebSockets [#real-time-updates-with-websockets] ```typescript // Configure WebSocket connection const adapter = new ObjectStackAdapter({ baseUrl: 'http://localhost:3000/api', websocket: { enabled: true, url: 'ws://localhost:3000/ws' } }); // Subscribe to real-time updates adapter.subscribe('contact', (event) => { if (event.type === 'create' || event.type === 'update') { // Refresh grid gridRef.current?.refresh(); } }); ``` ## Migration from Other Platforms [#migration-from-other-platforms] ### From Retool [#from-retool] ```typescript // Retool table → ObjectUI Grid { type: 'object-grid', objectName: 'users', dataSource, editable: true, rowSelection: 'multiple', exportConfig: { enabled: true } } ``` ### From Appsmith [#from-appsmith] ```typescript // Appsmith form → ObjectUI form { type: 'object-form', objectName: 'contact', dataSource, mode: 'create', fieldNames: ['name', 'email', 'phone', 'company'], onSubmit: async (data) => { await dataSource.create('contact', data); } } ``` ### From Mendix [#from-mendix] ```typescript // Mendix page → ObjectUI page { type: 'page', template: 'header-sidebar-main', header: { /* ... */ }, sidebar: { /* ... */ }, main: { type: 'tabs', items: [ { label: 'Overview', content: { /* ... */ } }, { label: 'Details', content: { /* ... */ } } ] } } ``` ## Performance Optimization [#performance-optimization] ### Bundle Optimization [#bundle-optimization] ```typescript // Lazy load plugins const plugins = { grid: () => import('@object-ui/plugin-grid'), charts: () => import('@object-ui/plugin-charts'), kanban: () => import('@object-ui/plugin-kanban') }; // Load on demand await plugins.grid(); ``` ### Caching Strategy [#caching-strategy] ```typescript const adapter = new ObjectStackAdapter({ baseUrl: 'http://localhost:3000/api', cache: { enabled: true, ttl: 60000, // 1 minute strategies: { 'contact': 'stale-while-revalidate', 'sys_user': 'cache-first' } } }); ``` ## Testing & Quality Assurance [#testing--quality-assurance] ### Unit Tests [#unit-tests] ```typescript import { render } from '@testing-library/react'; import { SchemaRenderer } from '@object-ui/react'; test('renders contact grid', () => { const { getByText } = render( ); expect(getByText('Contact')).toBeInTheDocument(); }); ``` ### Integration Tests [#integration-tests] ```typescript import { test, expect } from '@playwright/test'; test('create contact workflow', async ({ page }) => { await page.goto('http://localhost:3000/console/contacts'); await page.click('button:has-text("New Contact")'); await page.fill('[name="name"]', 'John Doe'); await page.fill('[name="email"]', 'john@example.com'); await page.click('button:has-text("Save")'); await expect(page.locator('text=John Doe')).toBeVisible(); }); ``` ## Resources [#resources] * [ObjectStack Documentation](https://docs.objectstack.ai) * [ObjectUI Components Reference](/docs/components) * [ObjectQL Schemas](/docs/api/schema-reference#objectql-schemas) * [Example: CRM Application](/examples/crm) * [Example: Kitchen Sink](/examples/kitchen-sink) ## Support [#support] * GitHub Issues: [https://github.com/objectstack-ai/objectui/issues](https://github.com/objectstack-ai/objectui/issues) * Discord Community: [https://discord.gg/objectui](https://discord.gg/objectui) * Email: [hello@objectui.org](mailto:hello@objectui.org) # Custom Plugin Development This guide walks you through creating custom ObjectUI plugins — from scaffolding to publishing. Plugins extend ObjectUI with new view types, field widgets, or complex interactive components while keeping your application bundle lean through lazy loading. ## What Is an ObjectUI Plugin? [#what-is-an-objectui-plugin] A plugin is a self-contained package that registers one or more components into the [Component Registry](./component-registry.md). When a JSON schema references a plugin's component type, the renderer resolves and renders it automatically. Plugins differ from regular components in two ways: * **Lazy-loaded** — heavy dependencies are code-split and fetched on demand. * **Self-registering** — importing the package is enough; no manual wiring required. Official plugins (`@object-ui/plugin-grid`, `@object-ui/plugin-kanban`, `@object-ui/plugin-charts`, etc.) all follow this pattern, and your custom plugins should too. ## Plugin Anatomy [#plugin-anatomy] Every plugin has three key parts: ``` packages/plugin-board/ ├── src/ │ ├── index.tsx # Entry point: lazy wrapper + ComponentRegistry.register() │ ├── BoardImpl.tsx # Heavy implementation (imported lazily) │ ├── BoardImpl.test.tsx # Tests │ └── types.ts # TypeScript interfaces & schema types ├── package.json ├── vite.config.ts ├── tsconfig.json └── README.md ``` | File | Role | | --------------- | ---------------------------------------------------------------------------------------------------------- | | `index.tsx` | Lightweight entry — sets up `React.lazy()`, `Suspense` fallback, and calls `ComponentRegistry.register()`. | | `BoardImpl.tsx` | The actual renderer. All heavy dependencies live here so they are tree-shaken from the initial bundle. | | `types.ts` | Schema interfaces extending `BaseSchema` from `@object-ui/types`. | ## Scaffolding With the CLI [#scaffolding-with-the-cli] The fastest way to start is the `create-plugin` generator: ```bash npx @object-ui/create-plugin board --description "Kanban-style board view" # Or with pnpm / npm create aliases: pnpm create @object-ui/plugin board npm create @object-ui/plugin board ``` This produces a ready-to-build plugin under `packages/plugin-board/` with the correct `package.json`, Vite config, test file, and registry call already in place. After scaffolding, install dependencies: ```bash pnpm install ``` ## Implementing a Custom View Plugin [#implementing-a-custom-view-plugin] Let's build a **board** view plugin that renders items in columns (similar to a Kanban but simplified). ### 1. Define the Schema Types [#1-define-the-schema-types] ```typescript // src/types.ts import type { BaseSchema } from '@object-ui/types'; export interface BoardColumn { id: string; title: string; } export interface BoardItem { id: string; columnId: string; title: string; description?: string; } export interface BoardSchema extends BaseSchema { type: 'board'; columns: BoardColumn[]; items: BoardItem[]; onItemMove?: (itemId: string, toColumnId: string) => void; } export interface BoardProps { schema: BoardSchema; className?: string; } ``` ### 2. Build the Implementation [#2-build-the-implementation] ```tsx // src/BoardImpl.tsx import React from 'react'; import { Card, CardHeader, CardTitle, CardContent } from '@object-ui/components'; import { cn } from '@object-ui/components'; import type { BoardProps } from './types'; export default function BoardImpl({ schema, className }: BoardProps) { const { columns, items } = schema; return (
{columns.map((col) => (

{col.title}

{items .filter((item) => item.columnId === col.id) .map((item) => ( {item.title} {item.description && ( {item.description} )} ))}
))}
); } ``` ### 3. Create the Entry Point [#3-create-the-entry-point] ```tsx // src/index.tsx import React, { Suspense } from 'react'; import { ComponentRegistry } from '@object-ui/core'; import { Skeleton } from '@object-ui/components'; const LazyBoard = React.lazy(() => import('./BoardImpl')); export const BoardRenderer: React.FC<{ schema: any; [key: string]: any }> = ({ schema, ...props }) => ( }> ); // Auto-register on import ComponentRegistry.register('board', BoardRenderer, { namespace: 'plugin-board', label: 'Board View', category: 'plugin', inputs: [ { name: 'columns', type: 'array', label: 'Columns', required: true }, { name: 'items', type: 'array', label: 'Items', required: true }, ], defaultProps: { columns: [ { id: 'todo', title: 'To Do' }, { id: 'done', title: 'Done' }, ], items: [], }, }); export { default as BoardImpl } from './BoardImpl'; export type { BoardSchema, BoardProps, BoardColumn, BoardItem } from './types'; ``` Now any schema with `"type": "board"` will resolve to your component. ## Implementing a Custom Field Widget [#implementing-a-custom-field-widget] Field widgets follow the `FieldWidgetProps` interface from `@object-ui/fields`. ```typescript // FieldWidgetProps shape (from packages/fields/src/widgets/types.ts) type FieldWidgetProps = { value: T; onChange: (val: T) => void; field: FieldMetadata; readonly?: boolean; disabled?: boolean; className?: string; errorMessage?: string; }; ``` ### Example: Color Picker Field [#example-color-picker-field] ```tsx // src/ColorPickerField.tsx import React from 'react'; import { Input } from '@object-ui/components'; import type { FieldWidgetProps } from '@object-ui/fields'; export function ColorPickerField({ value, onChange, field, readonly, disabled, errorMessage, }: FieldWidgetProps) { if (readonly) { return (
{value || '—'}
); } return (
onChange(e.target.value)} disabled={disabled} className="h-8 w-8 cursor-pointer rounded border-0 p-0" /> onChange(e.target.value)} placeholder={field?.placeholder || '#000000'} disabled={disabled} className="font-mono text-sm" />
{errorMessage && ( {errorMessage} )}
); } ``` Register it as a field widget: ```tsx // src/index.tsx import { ComponentRegistry } from '@object-ui/core'; import { ColorPickerField } from './ColorPickerField'; ComponentRegistry.register('field-color', ColorPickerField, { namespace: 'plugin-board', label: 'Color Picker', category: 'field', inputs: [ { name: 'value', type: 'string', label: 'Value' }, { name: 'placeholder', type: 'string', label: 'Placeholder' }, ], }); export { ColorPickerField }; ``` ## Using the ComponentRegistry [#using-the-componentregistry] ### Namespaced Registration [#namespaced-registration] Namespaces prevent type collisions between plugins: ```tsx import { ComponentRegistry } from '@object-ui/core'; // Register with a namespace — accessible as 'plugin-board:board' AND 'board' ComponentRegistry.register('board', BoardRenderer, { namespace: 'plugin-board', }); // Explicit lookup by namespace ComponentRegistry.get('board', 'plugin-board'); // Fallback lookup (works when the type is unambiguous) ComponentRegistry.get('board'); ``` Use `skipFallback: true` in the metadata if you do **not** want the component to be available without a namespace prefix. ### Querying Registered Components [#querying-registered-components] ```tsx ComponentRegistry.has('board'); // boolean ComponentRegistry.getAllTypes(); // string[] ComponentRegistry.getNamespaceComponents('plugin-board'); // ComponentConfig[] ``` ## Plugin Configuration & Schema Types [#plugin-configuration--schema-types] Define your schema interface in `types.ts` and extend `BaseSchema`: ```typescript import type { BaseSchema } from '@object-ui/types'; export interface BoardSchema extends BaseSchema { type: 'board'; columns: BoardColumn[]; items: BoardItem[]; } ``` Declare `ComponentInput` entries when registering so the visual designer can offer a property panel: ```tsx ComponentRegistry.register('board', BoardRenderer, { inputs: [ { name: 'columns', type: 'array', label: 'Columns', required: true }, { name: 'items', type: 'array', label: 'Items', required: true }, { name: 'layout', type: 'enum', label: 'Layout', enum: ['horizontal', 'vertical'], defaultValue: 'horizontal', }, ], }); ``` ## Testing Plugins [#testing-plugins] ObjectUI uses **Vitest + React Testing Library**. Place tests next to the implementation. ```tsx // src/BoardImpl.test.tsx import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import BoardImpl from './BoardImpl'; const schema = { type: 'board' as const, columns: [ { id: 'todo', title: 'To Do' }, { id: 'done', title: 'Done' }, ], items: [ { id: '1', columnId: 'todo', title: 'Write tests' }, { id: '2', columnId: 'done', title: 'Ship plugin' }, ], }; describe('BoardImpl', () => { it('renders all columns', () => { render(); expect(screen.getByText('To Do')).toBeInTheDocument(); expect(screen.getByText('Done')).toBeInTheDocument(); }); it('renders items in correct columns', () => { render(); expect(screen.getByText('Write tests')).toBeInTheDocument(); expect(screen.getByText('Ship plugin')).toBeInTheDocument(); }); it('handles empty items gracefully', () => { render(); expect(screen.getByText('To Do')).toBeInTheDocument(); }); }); ``` Run tests: ```bash pnpm vitest run packages/plugin-board ``` ## Publishing Guidelines [#publishing-guidelines] ### Package Checklist [#package-checklist] Before publishing, verify: * [ ] `package.json` has correct `name`, `version`, `exports`, and `peerDependencies`. * [ ] `react` and `react-dom` are **peer** dependencies, not direct dependencies. * [ ] `@object-ui/core` and `@object-ui/components` are in `devDependencies` (or `peerDependencies`). * [ ] `vite.config.ts` marks React and ObjectUI packages as **external**. * [ ] Types are exported via `"types"` field in `package.json`. * [ ] All tests pass (`pnpm vitest run`). * [ ] The entry point is lightweight — heavy code lives in `*Impl.tsx` files. ### Build & Verify [#build--verify] ```bash pnpm build --filter @object-ui/plugin-board ls -lh packages/plugin-board/dist/ ``` The entry chunk should be under 1 KB; the lazy chunk carries the bulk. ### Publish [#publish] ```bash cd packages/plugin-board npm publish --access public ``` ### Consumers Install & Use [#consumers-install--use] ```bash pnpm add @object-ui/plugin-board ``` ```tsx // app/main.tsx — import once, auto-registers import '@object-ui/plugin-board'; ``` ```json { "type": "board", "columns": [ { "id": "todo", "title": "To Do" }, { "id": "done", "title": "Done" } ], "items": [ { "id": "1", "columnId": "todo", "title": "Write docs" } ] } ``` ## Related Documentation [#related-documentation] * [Component Registry](./component-registry.md) — registry internals and advanced usage * [Plugins Overview](./plugins.md) — official plugin catalog * [Schema Rendering](./schema-rendering.md) — how schemas become UI * [Fields Guide](./fields.md) — built-in field widgets and `FieldWidgetProps` # Plugins Object UI supports a powerful plugin system that allows you to extend the framework with additional components. Plugins are separate packages that load on-demand, keeping your main application bundle small while providing rich functionality. ## Overview [#overview] Plugins are lazy-loaded component packages that: * **Auto-register** components when imported * **Lazy-load** heavy dependencies on-demand * **Keep bundles small** - only load when needed * **Are type-safe** with full TypeScript support * **Follow best practices** with built-in loading states ## Official Plugins [#official-plugins] Object UI provides 14+ official plugins for common use cases: ### Data Visualization & Dashboards [#data-visualization--dashboards] #### [@object-ui/plugin-charts](../plugins/plugin-charts.md) [#object-uiplugin-charts] Data visualization components powered by Recharts. * Bar, line, area, and pie charts * Responsive design * Customizable colors * Lazy-loaded (\~80 KB) [Read full documentation →](../plugins/plugin-charts.md) *** #### [@object-ui/plugin-dashboard](../plugins/plugin-dashboard.md) [#object-uiplugin-dashboard] Dashboard layouts with metric cards and widgets. * Dashboard grid layouts * Metric/KPI cards with trends * Widget system * Lazy-loaded (\~22 KB) [Read full documentation →](../plugins/plugin-dashboard.md) *** #### [@object-ui/plugin-timeline](../plugins/plugin-timeline.md) [#object-uiplugin-timeline] Timeline component with multiple layout variants. * Vertical, horizontal layouts * Customizable markers * Date formatting * Lazy-loaded (\~20 KB) [Read full documentation →](../plugins/plugin-timeline.md) *** #### [@object-ui/plugin-gantt](../plugins/plugin-gantt.md) [#object-uiplugin-gantt] Gantt chart for project visualization. * Task dependencies * Progress tracking * ObjectQL integration * Lazy-loaded (\~40 KB) [Read full documentation →](../plugins/plugin-gantt.md) *** #### [@object-ui/plugin-calendar](../plugins/plugin-calendar.md) [#object-uiplugin-calendar] Calendar visualization for events. * Month/week/day views * Event management * ObjectQL integration * Lazy-loaded (\~25 KB) [Read full documentation →](../plugins/plugin-calendar.md) *** #### [@object-ui/plugin-map](../plugins/plugin-map.md) [#object-uiplugin-map] Map visualization with markers. * Interactive maps * Location markers * ObjectQL integration * Lazy-loaded (\~60 KB) [Read full documentation →](../plugins/plugin-map.md) *** ### Data Management [#data-management] #### [@object-ui/plugin-grid](../plugins/plugin-grid.md) [#object-uiplugin-grid] Advanced data grid with sorting, filtering, and pagination. * Column sorting and filtering * Pagination controls * Row selection * Lazy-loaded (\~45 KB) [Read full documentation →](../plugins/plugin-grid.md) *** #### [@object-ui/plugin-form](../plugins/plugin-form.md) [#object-uiplugin-form] Advanced form builder with validation. * Multi-step forms * Field validation * Custom field types * Lazy-loaded (\~28 KB) [Read full documentation →](../plugins/plugin-form.md) *** #### [@object-ui/plugin-view](../plugins/plugin-view.md) [#object-uiplugin-view] ObjectQL-integrated views for automatic CRUD. * Auto-generated forms and grids * CRUD operations * Field mapping * Lazy-loaded (\~35 KB) [Read full documentation →](../plugins/plugin-view.md) *** ### Content & Editing [#content--editing] #### [@object-ui/plugin-editor](../plugins/plugin-editor.md) [#object-uiplugin-editor] Code editor component powered by Monaco Editor. * Syntax highlighting for 100+ languages * IntelliSense and code completion * Multiple themes * Lazy-loaded (\~120 KB) [Read full documentation →](../plugins/plugin-editor.md) *** #### [@object-ui/plugin-markdown](../plugins/plugin-markdown.md) [#object-uiplugin-markdown] Markdown renderer with GitHub Flavored Markdown support. * GitHub Flavored Markdown * XSS protection * Code syntax highlighting * Lazy-loaded (\~30 KB) [Read full documentation →](../plugins/plugin-markdown.md) *** #### [@object-ui/plugin-chatbot](../plugins/plugin-chatbot.md) [#object-uiplugin-chatbot] Chat interface component. * Message history * User and assistant roles * Timestamps and avatars * Responsive floating panel for console assistants * Inline responding, stop, and retry states * Lazy-loaded (\~35 KB) [Read full documentation →](../plugins/plugin-chatbot.md) *** ### Workflows & Tasks [#workflows--tasks] #### [@object-ui/plugin-kanban](../plugins/plugin-kanban.md) [#object-uiplugin-kanban] Kanban board component with drag-and-drop powered by @dnd-kit. * Drag and drop cards between columns * Column limits (WIP limits) * Card badges for status/priority * Lazy-loaded (\~100 KB) [Read full documentation →](../plugins/plugin-kanban.md) *** ## How Plugins Work [#how-plugins-work] ### Lazy Loading Architecture [#lazy-loading-architecture] Plugins use React's `lazy()` and `Suspense` to load heavy dependencies on-demand: ```typescript // The plugin structure import React, { Suspense } from 'react' import { Skeleton } from '@object-ui/components' // Lazy load the heavy implementation const LazyEditor = React.lazy(() => import('./MonacoImpl')) export const CodeEditorRenderer = (props) => ( }> ) ``` **Benefits:** * **Smaller initial bundle**: Main app loads faster * **Progressive loading**: Components load when needed * **Better UX**: Loading skeletons while chunks download * **Automatic code splitting**: Vite handles chunking ### Bundle Impact [#bundle-impact] | Plugin | Initial Load | Lazy Load | Description | | ---------------- | ------------ | --------- | ---------------------- | | plugin-editor | \~0.2 KB | \~120 KB | Monaco editor | | plugin-charts | \~0.2 KB | \~80 KB | Recharts visualization | | plugin-kanban | \~0.2 KB | \~100 KB | Drag-and-drop board | | plugin-markdown | \~0.2 KB | \~30 KB | Markdown rendering | | plugin-dashboard | \~0.2 KB | \~22 KB | Dashboard layouts | | plugin-form | \~0.2 KB | \~28 KB | Form builder | | plugin-grid | \~0.2 KB | \~45 KB | Data grid | | plugin-view | \~0.2 KB | \~35 KB | ObjectQL views | | plugin-timeline | \~0.2 KB | \~20 KB | Timeline layouts | | plugin-chatbot | \~0.2 KB | \~35 KB | Chat interface | | plugin-calendar | \~0.2 KB | \~25 KB | Calendar views | | plugin-gantt | \~0.2 KB | \~40 KB | Gantt charts | | plugin-map | \~0.2 KB | \~60 KB | Map visualization | Without lazy loading, all this code would be in your main bundle! ### Auto-Registration [#auto-registration] Plugins automatically register their components when imported: ```typescript // In the plugin's index.tsx import { ComponentRegistry } from '@object-ui/core' ComponentRegistry.register('code-editor', CodeEditorRenderer) ``` You just need to import the plugin once: ```typescript // In your App.tsx or main.tsx import '@object-ui/plugin-editor' import '@object-ui/plugin-charts' import '@object-ui/plugin-kanban' import '@object-ui/plugin-markdown' import '@object-ui/plugin-dashboard' import '@object-ui/plugin-form' import '@object-ui/plugin-grid' // ... import other plugins as needed ``` Now all plugin components are available in your schemas! ## Creating Custom Plugins [#creating-custom-plugins] You can create your own plugins following the same pattern: ### 1. Create Package Structure [#1-create-package-structure] ```bash mkdir -p packages/plugin-myfeature/src cd packages/plugin-myfeature ``` ### 2. Create Heavy Implementation [#2-create-heavy-implementation] ```typescript // src/MyFeatureImpl.tsx import HeavyLibrary from 'heavy-library' export default function MyFeatureImpl(props) { return } ``` ### 3. Create Lazy Wrapper [#3-create-lazy-wrapper] ```typescript // src/index.tsx import React, { Suspense } from 'react' import { ComponentRegistry } from '@object-ui/core' import { Skeleton } from '@object-ui/components' // Lazy load implementation const LazyFeature = React.lazy(() => import('./MyFeatureImpl')) // Create renderer with Suspense export const MyFeatureRenderer = (props) => ( }> ) // Auto-register ComponentRegistry.register('my-feature', MyFeatureRenderer) // Export for manual use export const myFeatureComponents = { 'my-feature': MyFeatureRenderer } ``` ### 4. Add TypeScript Types [#4-add-typescript-types] ```typescript // src/types.ts import type { BaseSchema } from '@object-ui/types' export interface MyFeatureSchema extends BaseSchema { type: 'my-feature' customProp?: string } ``` ### 5. Configure Build [#5-configure-build] ```typescript // vite.config.ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import { resolve } from 'path' export default defineConfig({ plugins: [react()], build: { lib: { entry: resolve(__dirname, 'src/index.tsx'), name: 'ObjectUIPluginMyFeature', fileName: (format) => `index.${format}.js` }, rollupOptions: { external: [ 'react', 'react-dom', '@object-ui/components', '@object-ui/core' ], output: { globals: { react: 'React', 'react-dom': 'ReactDOM' } } } } }) ``` ### 6. Add Package.json [#6-add-packagejson] ```json { "name": "@object-ui/plugin-myfeature", "version": "1.0.0", "type": "module", "main": "./dist/index.umd.js", "module": "./dist/index.es.js", "types": "./dist/index.d.ts", "exports": { ".": { "import": "./dist/index.es.js", "require": "./dist/index.umd.js", "types": "./dist/index.d.ts" } }, "files": ["dist"], "scripts": { "build": "vite build && tsc --emitDeclarationOnly" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "dependencies": { "heavy-library": "^1.0.0" }, "devDependencies": { "@object-ui/components": "workspace:*", "@object-ui/core": "workspace:*", "@object-ui/types": "workspace:*", "typescript": "^5.0.0", "vite": "^5.0.0" } } ``` ## Best Practices [#best-practices] ### 1. Keep Entry Point Light [#1-keep-entry-point-light] The main index file should only contain: * Lazy loading wrapper * Component registration * Type exports Heavy imports go in the `*Impl.tsx` file. ### 2. Provide Good Loading States [#2-provide-good-loading-states] Always show a meaningful skeleton while loading: ```typescript }> ``` ### 3. Export Types [#3-export-types] Make your plugin type-safe: ```typescript export type { MyFeatureSchema } from './types' ``` ### 4. Document Your Plugin [#4-document-your-plugin] Include a README with: * Installation instructions * Usage examples * Schema API reference * Bundle size information ### 5. Test Lazy Loading [#5-test-lazy-loading] Verify that: * The main bundle is small (\~200 bytes) * The lazy chunk is separate * Components load correctly when rendered ```bash pnpm build ls -lh dist/ ``` ## Plugin vs Component Package [#plugin-vs-component-package] **Use a Plugin when:** * The component depends on large libraries (>50 KB) * Not all apps will use this component * You want on-demand loading **Use regular Components when:** * The component is lightweight * Most apps will use it * It's part of core functionality ## Troubleshooting [#troubleshooting] ### Plugin not loading [#plugin-not-loading] Check that you imported it in your app: ```typescript import '@object-ui/plugin-myfeature' ``` ### TypeScript errors [#typescript-errors] Make sure types are exported: ```typescript export type { MyFeatureSchema } from '@object-ui/plugin-myfeature' ``` ### Bundle size too large [#bundle-size-too-large] Check that the implementation is in a separate file: ``` ✅ src/index.tsx (light, uses React.lazy) ✅ src/MyFeatureImpl.tsx (heavy, imported lazily) ``` ### Component not registering [#component-not-registering] Check that ComponentRegistry.register() is called at the module level: ```typescript // ✅ Good - runs on import ComponentRegistry.register('my-feature', MyFeatureRenderer) // ❌ Bad - never runs export function registerComponents() { ComponentRegistry.register('my-feature', MyFeatureRenderer) } ``` ## Related Documentation [#related-documentation] * [Component Registry](./component-registry.md) - Understanding the registry * [Schema Rendering](./schema-rendering.md) - How schemas become UI * [Lazy-Loaded Plugins Architecture](./lazy-loading.md) - Deep dive * [Creating Components](/spec/component-package.md) - Component development * **[Create Plugin Utility](/docs/utilities/create-plugin)** - Scaffold new plugins quickly * **[CLI Tool](/docs/utilities/cli)** - Test plugins with the CLI * **[All Utilities](/docs/utilities)** - Complete toolkit for development ## Next Steps [#next-steps] 1. Install official plugins you need 2. Try creating a custom plugin 3. Share your plugins with the community 4. Contribute new plugins to Object UI # Public Forms # Public Forms [#public-forms] `@object-ui/plugin-form` ships an `EmbeddableForm` component that renders a public-facing form (contact us, lead capture, signup, RSVP) with the security defaults you'd expect from Airtable Forms, Typeform or HubSpot Forms — without asking app authors to bolt them on themselves. The console exposes a turnkey route at `/f/:slug` (`FormPage`) that loads a `FormView` spec from the server's `GET /api/v1/forms/:slug` resolver and renders the merged form. The same component also serves authed internal forms at `/forms/:name` (`mode="internal"`), reading the FormView spec from `/api/v1/meta/view/:name` and posting to `/api/v1/data/:object`. ## Quick start [#quick-start] ```tsx import { EmbeddableForm } from '@object-ui/plugin-form'; import { restDataSource } from '@object-ui/data-rest'; ``` ## Security defaults (at a glance) [#security-defaults-at-a-glance] | Defence | Default | Config key | | ------------------------------------------------ | ------------------------------------------------------------------------ | ------------------------------------------------------------ | | **Honeypot field** (silent fake-success on bots) | On | `honeypot` (string to rename, `false` to disable) | | **Min-fill-time guard** | 1500 ms | `minFillTime` (ms, `0` to disable) | | **URL prefill whitelist** | *none* — prefill is fully off | `allowedPrefillFields: string[]` | | **Open-redirect guard** | Same-origin only | `allowedRedirectHosts: string[]` (supports `*.example.com`) | | **Default `maxLength`** | text 200 · email 254 · url 2048 · phone 32 · textarea/markdown/html 5000 | Per-field `maxLength` overrides | | **GDPR consent gate** | Off (opt-in) | `consent: { required, label }` + `privacyPolicyUrl` | | **CAPTCHA token** | Off (opt-in) | `captchaToken` (string sent as `_captcha`) | | **Demo mode (`?demo=1`)** | DEV only | gated by `import.meta.env.DEV` in `EmbeddableForm` consumers | All gates run **before** the network call. If the consent gate or min-fill timer trips, the backend is never contacted. The honeypot silently shows the thank-you page so bots can't tell they failed. ## Configuration reference [#configuration-reference] ```ts interface EmbeddableFormConfig { formId: string; objectName: string; title?: string; description?: string; // Fields — either from a registered schema or inline fields?: string[]; customFields?: FormField[]; // Anti-spam honeypot?: string | false; // field name (default '_company_website_2'), false disables minFillTime?: number; // ms before submit allowed (default 1500) captchaToken?: string; // forwarded as payload._captcha // Prefill & redirect hardening allowedPrefillFields?: string[]; // empty/undefined → no URL prefill allowedRedirectHosts?: string[]; // supports '*.example.com' // GDPR consent?: { required?: boolean; label?: string }; privacyPolicyUrl?: string; // UI branding?: { logo?: string; primaryColor?: string; coverImage?: string }; thankYouPage?: { title?: string; message?: string; redirectUrl?: string; redirectDelay?: number }; texts?: EmbeddableFormTexts; // i18n-friendly string overrides } ``` ### URL prefill (safe-by-default) [#url-prefill-safe-by-default] Public form URLs are user-controlled, so prefill is **off** unless you explicitly opt fields in: ```tsx ``` With the snippet above, visiting `/f/contact?email=alice@x.com&secret=foo` fills the `email` field and silently ignores `secret`. The `prefillParams` prop (used by trusted hosts such as the console) bypasses this whitelist. ### Open-redirect guard [#open-redirect-guard] `thankYouPage.redirectUrl` is validated against the current origin **plus** `allowedRedirectHosts` before navigation. Wildcards like `*.example.com` match subdomains; the apex itself must be listed explicitly. Dangerous schemes (`javascript:`, `data:`) are always rejected. ### GDPR consent [#gdpr-consent] ```tsx consent: { required: true, label: 'I agree to the privacy policy.' } privacyPolicyUrl: '/legal/privacy' ``` When `required: true`, submitting before the box is ticked shows `texts.consentRequired` and the network call is suppressed. ### Honeypot [#honeypot] The hidden input is rendered off-screen with `tabIndex={-1}` and `autocomplete="off"`. Bots that blindly fill every field trigger a **silent fake-success** — the visitor sees the thank-you screen, but `dataSource.create()` is never called. Honeypot data is also stripped from the payload defensively. ### Min-fill-time [#min-fill-time] Genuine users take more than \~1.5 s to read and fill a form. Faster submissions are soft-rejected with `texts.rateLimited` rather than a hard error, so legit speed-typers can simply retry. ### CAPTCHA hook [#captcha-hook] `EmbeddableForm` doesn't bundle a specific provider. Mount your preferred widget (hCaptcha, Turnstile, reCAPTCHA) and feed the token in: ```tsx const [token, setToken] = useState(); ``` The token is forwarded as `payload._captcha` for server-side validation. ## i18n [#i18n] `EmbeddableForm` is i18n-agnostic — every user-visible string is overridable via `config.texts: EmbeddableFormTexts`. The console wires this up through `@object-ui/i18n`: ```tsx const { t } = useObjectTranslation(); ``` ## Console route — `/f/:slug` and `/forms/:name` [#console-route--fslug-and-formsname] The console's `FormPage` component renders both modes from the same spec-merging code path: * **`/f/:slug`** (public, anonymous) — loads `GET /api/v1/forms/:slug` which resolves the `FormView` whose `sharing.publicLink` matches the slug, then submits to `POST /api/v1/forms/:slug/submit`. * **`/forms/:name`** (internal, authed) — loads `GET /api/v1/meta/view/:name` for the FormView spec plus `GET /api/v1/meta/object/:object` for field metadata, then submits to `POST /api/v1/data/:object` with the authenticated session cookie. URL parameters of the form `?prefill_=` populate the matching fields on mount; the rest of the chrome (label, section columns, post-submit behaviour) comes from the `FormView` spec itself. For richer public forms with anti-spam, GDPR consent, prefill whitelisting and open-redirect protection, host `EmbeddableForm` directly inside your own route — see the Quick start above. ## Testing [#testing] Pure helpers (`isRedirectUrlSafe`, `applyDefaultMaxLengths`) are exported from `@object-ui/plugin-form` for unit testing. See `packages/plugin-form/src/__tests__/EmbeddableForm.test.tsx` for the reference test suite covering all gates. # Quick Start # Quick Start [#quick-start] Get up and running with ObjectUI in a small Vite app. This guide installs the core renderer, registers the built-in component packages, and renders a first JSON schema. ## Prerequisites [#prerequisites] * **Node.js** 20+ * **pnpm** 9+ or npm/yarn * Basic knowledge of **React** and **TypeScript** ## Step 1: Create a React Project [#step-1-create-a-react-project] If you don't have an existing React project, create one with Vite: ```bash pnpm create vite my-app --template react-ts cd my-app ``` ## Step 2: Install ObjectUI [#step-2-install-objectui] Install the core ObjectUI packages: ```bash pnpm add @object-ui/react @object-ui/core @object-ui/types @object-ui/components @object-ui/fields ``` Install Tailwind CSS for styling: ```bash pnpm add -D tailwindcss @tailwindcss/vite ``` ## Step 3: Configure Tailwind CSS [#step-3-configure-tailwind-css] Add Tailwind to your `vite.config.ts`: ```ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ plugins: [react(), tailwindcss()], }); ``` Add to your `src/index.css`: ```css @import "tailwindcss"; @import "@object-ui/components/style.css"; @import "@object-ui/fields/style.css"; @source "../node_modules/@object-ui/components/**/*.{js,ts,tsx}"; @source "../node_modules/@object-ui/fields/**/*.{js,ts,tsx}"; ``` The `@source` lines let Tailwind see the utility classes used by ObjectUI packages. ## Step 4: Render Your First Schema [#step-4-render-your-first-schema] Replace `src/App.tsx` with: ```tsx import '@object-ui/components'; import '@object-ui/fields'; import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react'; const schema = { type: 'card', title: 'Team Directory', description: 'Rendered from JSON metadata', className: 'mx-auto max-w-3xl', body: { type: 'data-table', caption: 'Users', columns: [ { header: 'Name', accessorKey: 'name', sortable: true }, { header: 'Email', accessorKey: 'email' }, { header: 'Role', accessorKey: 'role' }, ], data: [ { name: 'Ada Lovelace', email: 'ada@example.com', role: 'Admin' }, { name: 'Grace Hopper', email: 'grace@example.com', role: 'Editor' }, { name: 'Katherine Johnson', email: 'katherine@example.com', role: 'Viewer' }, ], pagination: false, searchable: false, }, } as const; function App() { return (
); } export default App; ``` Importing `@object-ui/components` and `@object-ui/fields` registers their renderers with the shared `ComponentRegistry`. `SchemaRendererProvider` supplies the data scope used by expressions, smart fields, and data-aware plugins. ## Step 5: Run the App [#step-5-run-the-app] ```bash pnpm dev ``` Open [http://localhost:5173](http://localhost:5173). You should see a card and data table rendered from JSON. ## What Just Happened? [#what-just-happened] 1. **Schema** - the UI was described as JSON with `type`, visual props, and nested `body`. 2. **Registry** - importing the component packages registered renderers for `card` and `data-table`. 3. **Renderer** - `SchemaRenderer` resolved each `type` and rendered React components. 4. **Provider** - `SchemaRendererProvider` made a data scope available for expressions and plugins. ## Next Steps [#next-steps] ### Add Actions [#add-actions] Actions are data, not inline functions. Define them in schema events: ```json { "type": "button", "label": "Open details", "events": { "onClick": [ { "action": "navigate", "params": { "url": "/users/ada" } } ] } } ``` Learn the full action model in [Enhanced Actions](/docs/core/enhanced-actions). ### Connect a Data Source [#connect-a-data-source] ```bash pnpm add @object-ui/data-objectstack ``` ```tsx import { createObjectStackAdapter } from '@object-ui/data-objectstack'; const dataSource = createObjectStackAdapter({ baseUrl: 'https://api.example.com' }); ``` Pass the adapter to `SchemaRendererProvider` and let data-aware renderers call the `DataSource` interface. See [Data Connectivity](/docs/guide/data-source). ### Learn More [#learn-more] * [Architecture Overview](/docs/guide/architecture) — Understand how ObjectUI works * [Schema Rendering](/docs/guide/schema-rendering) — Deep dive into schema rendering * [Component Registry](/docs/guide/component-registry) — Customize and extend components * [Plugins](/docs/guide/plugins) — Add views like Grid, Kanban, Charts * [Fields Guide](/docs/guide/fields) — Field widgets and cell renderers # Record Edit Modes # Record Edit Modes [#record-edit-modes] ObjectUI's default console shell (`@object-ui/app-shell`) supports two ways to render the create/edit form for a record: * **Modal** (default) — the form opens in an overlay dialog above the current view. Best for short forms, quick edits, and contextual data entry. * **Page** — the form takes over a full route. Best for long forms, multi-tab or wizard layouts, or anywhere you need a deep-linkable URL that survives a refresh and integrates with the browser back button. Both modes use the same `` pipeline under the hood, so all field types, sections, validations, and visibility expressions work identically in either mode. ## Choosing a mode [#choosing-a-mode] Set `editMode` on the object metadata: ```jsonc // metadata/objects/account.json { "name": "account", "label": "Account", "editMode": "page", // "modal" (default) | "page" "fields": { "name": { "type": "text", "label": "Name", "required": true }, "industry": { "type": "picklist", "label": "Industry" }, "owner": { "type": "lookup", "label": "Owner", "reference_to": "user" } } } ``` Omitting `editMode` (or setting it to `"modal"`) keeps the existing behavior — clicking **Create** or **Edit** opens the global `ModalForm` overlay. ## URL patterns [#url-patterns] When `editMode: "page"` is set, the console renders the form on a dedicated route under the active app: | Action | URL | | ------ | -------------------------------------------------- | | Create | `/apps/:appName/:objectName/new` | | Edit | `/apps/:appName/:objectName/record/:recordId/edit` | Examples (for an app `sales` and an object `account`): * Create: `https://your-console.example/apps/sales/account/new` * Edit: `https://your-console.example/apps/sales/account/record/0015e000abcd/edit` These URLs are stable. Users can bookmark them, share them in chat, or refresh the page mid-edit (the form rehydrates from the URL `:recordId`). ## Triggering the routes from JSON [#triggering-the-routes-from-json] In addition to the implicit "click create/edit on a list" entry point, two declarative actions let you open the page-mode routes from any `` in metadata: ```jsonc { "type": "action:button", "label": "New Account", "icon": "plus", "action": { "action": "navigate_create", "params": { "objectName": "account" } } } ``` ```jsonc { "type": "action:button", "label": "Edit", "icon": "pencil", "action": { "action": "navigate_edit", "params": { "objectName": "account", "recordId": "${record.id}" } } } ``` When invoked from inside an `ObjectView`, the action context already carries the active `objectName`, so `objectName` may be omitted from the `params`: ```jsonc { "type": "action:button", "label": "New", "action": { "action": "navigate_create" } } ``` ## Behavior summary [#behavior-summary] | Aspect | Modal | Page | | ----------------------- | ----------- | -------------------------- | | Default | ✅ | — | | Deep-linkable URL | ❌ | ✅ | | Survives refresh | ❌ | ✅ | | Back button closes form | n/a | ✅ | | Best for | quick edits | long / multi-section forms | ## Migrating an existing object [#migrating-an-existing-object] The change is additive — existing apps continue to work unchanged. To migrate a single object to page mode: 1. Add `"editMode": "page"` to the object metadata. 2. (Optional) Adjust the form layout — page mode pairs well with `formType: "tabbed"` or `formType: "wizard"` for long forms. 3. Reload the console. Existing **Create** / **Edit** entry points automatically route to the new pages; no UI code changes required. ## See also [#see-also] * [`@object-ui/app-shell` README](https://www.objectui.org/docs/layout/app-shell) * [`ObjectForm` API](../plugins/plugin-form.md) * [Schema rendering](./schema-rendering.md) # Release Notes # Release Notes [#release-notes] This page summarises every released version of ObjectUI. For the granular package-level changelog, see the monorepo [CHANGELOG.md](https://github.com/objectstack-ai/objectui/blob/main/CHANGELOG.md). ## v3.3.0 — 2026-04-17 · First Official Release 🚀 [#v330--2026-04-17--first-official-release-] v3.3.0 is the **first official release** of ObjectUI published for third-party consumption. All 39 packages under `packages/*` are now published to npm with complete release metadata and aligned with `@objectstack/spec` ^4.0.4 and `@objectstack/client` v3.3.0. ### Highlights [#highlights] * **39 published packages** (`@object-ui/*`) with standardized `package.json` metadata, per-package `LICENSE` and `CHANGELOG.md`. * **Standard README template** applied across every package (Installation → Quick Start → API → Compatibility → Links → License). * **Refreshed docs site** with up-to-date architecture overview, plugin coverage and schema reference. * **Thin integration packages** — `@object-ui/app-shell` (\~50 KB) and `@object-ui/providers` (\~10 KB) enable third-party integrations without inheriting the full console. * **Spec v4 alignment** — plain-string `label` types across Navigation schemas; Protocol bridges (`DndProtocol`, `KeyboardProtocol`, `NotificationProtocol`) updated. * **Unified Copilot Skills** — single `skills/objectui/` tree aligned with shadcn/ui best practices. ### Upgrade Notes [#upgrade-notes] If you were pinning to the earlier `0.x` prerelease tags: 1. Bump every `@object-ui/*` dependency to `^3.3.0`. 2. Ensure peer dependencies match the new baselines (`react ^18 || ^19`, `react-dom ^18 || ^19`, TypeScript `>=5.0`). 3. Replace any `i18n` label objects (`{ key, defaultValue }`) on Navigation schemas with plain strings — runtime `resolveI18nLabel()` still handles both formats for backward compatibility. 4. Remove imports of the deprecated `ViewDesigner` — its capabilities are now delivered by `ViewConfigPanel`. ### Compatibility Matrix [#compatibility-matrix] | Package | Version | | --------------------- | ---------------- | | `@object-ui/*` | `3.3.0` | | `@objectstack/spec` | `^4.0.4` | | `@objectstack/client` | `3.3.0` | | React | `18.x` or `19.x` | | Node.js | `≥ 18` | | TypeScript | `≥ 5.0` (strict) | | Tailwind CSS | `≥ 3.4` | ## Previous Versions [#previous-versions] See the [monorepo CHANGELOG](https://github.com/objectstack-ai/objectui/blob/main/CHANGELOG.md) for the full history, including the `0.x` development series. # Schema Catalog # Schema Catalog [#schema-catalog] Every interactive demo on this site renders a JSON schema that lives in [`@object-ui/example-schema-catalog`](https://github.com/objectstack-ai/objectui/tree/main/examples/schema-catalog). This page is the visual gallery over that catalog — live thumbnails for every entry, grouped by domain. Click a card to open a full interactive preview with the JSON, copy a stable id, or grab a ready-to-paste `` snippet for any MDX page. ## How the catalog works [#how-the-catalog-works] * **Source of truth.** Each schema lives as a single `.json` file under `examples/schema-catalog/src/schemas//.json` and is imported into the registry by `examples/schema-catalog/src/index.ts`. * **One id, one schema.** Ids are stable (e.g. `auth/login-simple`). MDX pages reference schemas with ``, which looks the schema up via `getExample(id)` from the catalog package. * **Smoke-tested.** The catalog ships a Vitest suite that resolves every id and instantiates every schema; CI fails if any entry breaks. Adding a schema is a one-file change; renaming or deleting one fails CI loudly. * **Not published.** The catalog package is `private: true` — it is a test/docs fixture, not a runtime dependency for consuming apps. ## Browse [#browse] ## Adding a new example [#adding-a-new-example] 1. Drop the JSON under `examples/schema-catalog/src/schemas//.json`. 2. Run `pnpm -F @object-ui/example-schema-catalog regenerate` to update `src/index.ts` (the import map is generated). 3. Reference it from any MDX page: ```mdx ``` 4. `pnpm -F @object-ui/example-schema-catalog test` smoke-tests the new entry; the same test runs in CI on every push. # Schema Overview # Schema Overview [#schema-overview] ObjectUI provides powerful schemas that enable you to build sophisticated enterprise applications with advanced features like theming, reporting, reusable components, and complex workflows. This guide provides an overview of all available schemas and helps you get started quickly. ## Key Capabilities [#key-capabilities] ObjectUI includes enterprise-grade capabilities to build production-ready applications: * **Application Structure** - Define complete multi-page applications with navigation * **Dynamic Theming** - Brand your applications with custom themes and light/dark modes * **Advanced Actions** - Build complex workflows with API calls, chaining, and conditions * **Enterprise Reporting** - Generate, schedule, and export comprehensive reports * **Reusable Components** - Create and share component blocks across projects ## Core Schemas [#core-schemas] ### Application Configuration [#application-configuration] #### [App Schema](/docs/core/app-schema) [#app-schema] Define your entire application structure with navigation, branding, and global settings. ```typescript const app: AppSchema = { type: 'app', title: 'My Application', layout: 'sidebar', menu: [...], actions: [...] }; ``` **Use Cases:** * Multi-page applications * Admin dashboards * CRM systems * Internal tools *** ### Theming & Branding [#theming--branding] #### [Theme Schema](/docs/core/theme-schema) [#theme-schema] Dynamic theming with light/dark modes, color palettes, and typography. ```typescript const theme: ThemeSchema = { type: 'theme', mode: 'dark', themes: [{ name: 'professional', light: { primary: '#3b82f6', ... }, dark: { primary: '#60a5fa', ... } }] }; ``` **Features:** * Light/dark mode switching * 20+ semantic colors * Typography system * CSS variables * Tailwind integration *** ### Advanced Actions [#advanced-actions] #### [Enhanced Actions](/docs/core/enhanced-actions) [#enhanced-actions] Powerful action system with AJAX calls, chaining, conditions, and callbacks. ```typescript const action: ActionSchema = { type: 'action', actionType: 'ajax', api: '/api/submit', chain: [...], condition: { expression: '${...}', then: {...} }, onSuccess: {...}, tracking: {...} }; ``` **New Action Types:** * **`ajax`** - API calls with full request configuration * **`confirm`** - Confirmation dialogs * **`dialog`** - Modal/dialog actions **Key Features:** * Action chaining (sequential/parallel) * Conditional execution (if/then/else) * Success/failure callbacks * Event tracking * Retry logic *** ### Reporting [#reporting] #### [Report Schema](/docs/core/report-schema) [#report-schema] Enterprise reports with aggregation, export, and scheduling. ```typescript const report: ReportSchema = { type: 'report', title: 'Sales Report', fields: [ { name: 'revenue', aggregation: 'sum' }, { name: 'orders', aggregation: 'count' } ], schedule: { frequency: 'monthly', recipients: ['team@company.com'] } }; ``` **Features:** * Field aggregation (sum, avg, count, min, max) * Multiple export formats (PDF, Excel, CSV) * Scheduled reports * Email distribution * Interactive builder *** ### Reusable Components [#reusable-components] #### [Block Schema](/docs/blocks/block-schema) [#block-schema] Reusable component blocks with variables, slots, and marketplace support. ```typescript const block: BlockSchema = { type: 'block', meta: { name: 'hero-section', category: 'Marketing' }, variables: [ { name: 'title', type: 'string', defaultValue: 'Welcome' } ], slots: [ { name: 'content', label: 'Content Area' } ], template: { type: 'div', children: [...] } }; ``` **Features:** * Typed variables (props) * Content slots * Block templates * Marketplace support * Version control *** ## Quick Comparison [#quick-comparison] | Schema | Purpose | Best For | | -------------------- | --------------------- | ------------------------------------- | | **AppSchema** | Application structure | Multi-page apps, dashboards | | **ThemeSchema** | Visual theming | Brand consistency, white-labeling | | **Enhanced Actions** | Complex workflows | API integration, multi-step processes | | **ReportSchema** | Data reporting | Analytics, business intelligence | | **BlockSchema** | Reusable components | Marketing pages, component libraries | ## View Components [#view-components] ObjectUI also includes enhanced view components: ### [Detail View](/docs/plugins/plugin-detail) [#detail-view] Rich detail pages with sections, tabs, and related records. ### [View Switcher](/docs/components/complex/view-switcher) [#view-switcher] Toggle between list, grid, kanban, calendar, timeline, and map views. ### [Filter UI](/docs/components/complex/filter-ui) [#filter-ui] Advanced filtering interface with multiple field types. ### [Sort UI](/docs/components/complex/sort-ui) [#sort-ui] Sort configuration with multiple fields. ## Installation & Setup [#installation--setup] ### Package Installation [#package-installation] All schemas are included in `@object-ui/types`. Install it in your project: ```bash npm install @object-ui/types # or pnpm add @object-ui/types # or yarn add @object-ui/types ``` ### TypeScript Usage [#typescript-usage] Import the type definitions you need: ```typescript import type { AppSchema, ThemeSchema, ActionSchema, ReportSchema, BlockSchema } from '@object-ui/types'; ``` ### Runtime Validation [#runtime-validation] For runtime validation, use the included Zod schemas: ```typescript import { AppSchema, ThemeSchema, ActionSchema, ReportSchema, BlockSchema } from '@object-ui/types/zod'; const result = AppSchema.safeParse(myConfig); if (result.success) { // Valid configuration const app = result.data; } else { // Handle validation errors console.error(result.error); } ``` ## Quick Start Example [#quick-start-example] Here's a complete example showing how to build a simple CRM application using ObjectUI schemas: ```typescript import type { AppSchema, ThemeSchema } from '@object-ui/types'; // Define your application structure const app: AppSchema = { type: 'app', name: 'enterprise-crm', title: 'Enterprise CRM', layout: 'sidebar', menu: [ { type: 'item', label: 'Dashboard', icon: 'LayoutDashboard', path: '/dashboard' }, { type: 'group', label: 'Sales', children: [ { type: 'item', label: 'Leads', path: '/leads' }, { type: 'item', label: 'Deals', path: '/deals' } ] } ], actions: [ { type: 'user', label: 'User Name', items: [ { type: 'item', label: 'Profile', path: '/profile' }, { type: 'item', label: 'Logout', path: '/logout' } ] } ] }; // Configure your theme const theme: ThemeSchema = { type: 'theme', mode: 'system', themes: [{ name: 'professional', light: { primary: '#3b82f6', background: '#fff' }, dark: { primary: '#60a5fa', background: '#0f172a' } }], allowSwitching: true, persistPreference: true }; ``` This creates a professional-looking CRM application with: * A sidebar layout with navigation menu * Sales section with leads and deals * User menu with profile and logout options * Professional theme with light/dark mode support ## Advanced Features [#advanced-features] ObjectUI provides advanced schemas and capabilities for enterprise applications: ### Core Schemas [#core-schemas-1] ObjectUI includes these top-level schemas: * **`AppSchema`** - Define your entire application structure * **`ThemeSchema`** - Configure themes and color palettes * **`ReportSchema`** - Create data reports with aggregation * **`BlockSchema`** - Build reusable component blocks ### Enhanced ActionSchema [#enhanced-actionschema] The `ActionSchema` provides comprehensive action handling: * ✅ Action types: `ajax`, `confirm`, `dialog` * ✅ Action chaining via the `chain` array (sequential or parallel) * ✅ Conditional execution with the `condition` property * ✅ Success/failure callbacks: `onSuccess` and `onFailure` * ✅ Event tracking with the `tracking` configuration * ✅ Automatic retry logic ### View Components [#view-components-1] ObjectUI includes enhanced view components: * **`DetailViewSchema`** - Rich detail pages with sections and tabs * **`ViewSwitcherSchema`** - Toggle between list, grid, kanban, calendar views * **`FilterUISchema`** - Advanced filtering interface * **`SortUISchema`** - Multi-field sort configuration ## Getting Started [#getting-started] ### Installation Steps [#installation-steps] 1. **Install package** - Add `@object-ui/types` to your project ```bash npm install @object-ui/types@latest ``` 2. **Configure application** - Define your app structure with AppSchema (optional) 3. **Set up theming** - Add a ThemeSchema for consistent styling (optional) 4. **Implement actions** - Use advanced action features like `confirm` and callbacks 5. **Test your application** - Verify all functionality works as expected ## Learning Resources [#learning-resources] * **[Schema Type Reference](/docs/api/schema-reference)** - Complete schema reference with JSON examples. * **[Quick Start](/docs/guide/quick-start)** - Render your first ObjectUI schema. * **[Schema Rendering](/docs/guide/schema-rendering)** - Understand the renderer pipeline. * **[Component Registry](/docs/guide/component-registry)** - Learn how schema `type` values resolve to components. ## Getting Help [#getting-help] ### Community Support [#community-support] * **[GitHub Discussions](https://github.com/objectstack-ai/objectui/discussions)** - Ask questions and share ideas * **[GitHub Issues](https://github.com/objectstack-ai/objectui/issues)** - Report bugs and request features ### Official Documentation [#official-documentation] * **[Documentation Site](https://www.objectui.org/docs)** - Full documentation and guides * **[Schema Reference](/docs/api/schema-reference)** - Detailed schema documentation ## Next Steps [#next-steps] Ready to build with ObjectUI? Here's what to do next: 1. **[Review schema documentation](/docs/api/schema-reference)** - Learn about each schema in detail 2. **[Try the Quick Start](/docs/guide/quick-start)** - Build your first ObjectUI application 3. **[Explore components](/docs/components)** - See the core renderer catalog 4. **[Explore plugins](/docs/plugins)** - Add heavier widgets such as grids, kanban, charts, maps, and reports # Schema Playground # Schema Playground [#schema-playground] The Schema Playground is the fastest way to learn ObjectUI. Write JSON schemas in an editor, see the rendered UI instantly, and iterate on your designs without setting up a project. ## How It Works [#how-it-works] The playground follows the core ObjectUI rendering pipeline: ``` JSON Editor → Schema Validation → SchemaRenderer → Live Preview ``` 1. **Write** a JSON schema in the left panel 2. **Preview** the rendered UI in the right panel in real-time 3. **Iterate** by modifying properties and seeing changes instantly ## Setting Up the Playground [#setting-up-the-playground] Add the playground to any React project with ObjectUI installed: ```tsx import { useState } from 'react'; import { SchemaRenderer } from '@object-ui/react'; import { registerDefaultRenderers } from '@object-ui/components'; import { registerAllFields } from '@object-ui/fields'; import { Registry } from '@object-ui/core'; registerDefaultRenderers(); registerAllFields(Registry); function SchemaPlayground() { const [schema, setSchema] = useState('{\n "type": "button",\n "label": "Click me"\n}'); const [error, setError] = useState(null); const parsed = (() => { try { const obj = JSON.parse(schema); if (error) setError(null); return obj; } catch (e) { setError((e as Error).message); return null; } })(); return (

JSON Schema