Skip to content

Latest commit

 

History

History
1001 lines (844 loc) · 29.7 KB

File metadata and controls

1001 lines (844 loc) · 29.7 KB
title Layout DSL
description Declarative layout description language for pages, sections, and responsive grids

import { Layout, Grid, Columns, Box, Monitor, Smartphone, Tablet, MousePointer } from 'lucide-react';

The Layout DSL (Domain-Specific Language) is ObjectUI's declarative syntax for defining page structure, sections, and responsive grids. It abstracts away CSS Grid, Flexbox, and platform-specific layout engines.

Philosophy: Constraints Enable Creativity

Instead of giving you infinite layout freedom (and infinite ways to break responsive design), ObjectUI provides a constrained, opinionated layout system:

  • 12-Column Grid: All layouts use a 12-column responsive grid
  • Section-Based: Pages are composed of sections, sections contain fields/widgets
  • Auto-Responsive: Breakpoints applied automatically (no media queries)
  • Platform-Agnostic: Same DSL renders as CSS Grid (web), Auto Layout (iOS), ConstraintLayout (Android)

Why Constraints?

  • Consistency: All forms look professionally designed
  • Accessibility: Keyboard navigation and screen readers work automatically
  • Maintainability: Designers can't create 47 different button sizes

Authoring Modes: Structured, HTML, and React

A page's kind selects how it is authored. This document describes the structured mode (regions + the 12-column grid below) — but for layouts the fixed schema can't express, two source-authoring tiers let an author (or AI) write the page body as a string. Pick by what the page needs:

kind You write JavaScript Use when
full / slotted (this doc) structured regions / slots record, detail, home, and app layouts from the component catalogue
html constrained JSX = registered components + safe native HTML, parsed, never executed none free-form layout, landing pages, or dashboards that compose blocks — no interactivity
react real React (hooks, .map, onClick, expressions) executed in the app complex interactive business UIs — master/detail, wizards, state-driven filters

For html / react, the page body is the source string (it is the source-of-truth; regions is ignored). html is safe by construction (it is parsed into the same SDUI tree this document describes, never executed). react executes author code, so it is gated by a host capability that is ON by default and disabled per-deployment with the OS_PAGE_REACT=off env variable. Both tiers can embed the platform's real data components (<ObjectTable>, <ObjectForm>, charts, metrics).

Learn more: ADR-0080 (the html tier — parse ≠ execute; the registry is the contract) and ADR-0081 (the trusted react tier). The rest of this page covers the structured mode.

Layout Hierarchy

Page
 ├─ Header (Title, Actions, Breadcrumbs)
 ├─ Main Region
 │   ├─ Section 1
 │   │   ├─ Field Group
 │   │   │   ├─ Field A (span: 6)
 │   │   │   └─ Field B (span: 6)
 │   │   └─ Widget
 │   └─ Section 2
 │       └─ Related List
 └─ Sidebar Region
     ├─ Widget: Quick Stats
     └─ Widget: Activity Feed

Page Templates

Top-level layout structures that define macro organization.

Standard Template

context: record
object: account
template: standard
regions:
  - name: header
    components:
      - type: page:header
        properties:
          title: Customer Details
          actions: [edit, delete, share]
  - name: main
    components:
      - type: page:section
        properties:
          label: Contact Information
          fields: [name, email, phone]
  - name: sidebar
    components:
      - type: widget
        properties:
          component: activity_feed

Visual Layout:

┌────────────────────────────────────────────────────┐
│ Header: Customer Details        [Edit] [Delete]    │
├──────────────────────────────┬─────────────────────┤
│                              │                     │
│  Main Region                 │  Sidebar            │
│                              │                     │
│  ┌────────────────────────┐  │  ┌───────────────┐ │
│  │ Contact Information    │  │  │ Activity Feed │ │
│  │ Name: ____________     │  │  │ • Called      │ │
│  │ Email: ___________     │  │  │ • Emailed     │ │
│  │ Phone: ___________     │  │  └───────────────┘ │
│  └────────────────────────┘  │                     │
│                              │                     │
└──────────────────────────────┴─────────────────────┘

Breakpoint Behavior:

  • Desktop (≥1024px): Sidebar on right (25% width)
  • Tablet (768-1023px): Sidebar below main (full width)
  • Mobile (less than 768px): Sidebar below main (full width)

Console Template

High-density layout for power users (like Salesforce Service Console).

context: app
template: console
regions:
  - name: left
    width: small
    components:
      - type: record:list
        properties:
          object: case
          
  - name: right
    width: full
    components:
      - type: record:details
        properties:
          tabs: [details, related, activity]

Visual Layout:

┌──────────────┬─────────────────────────────────────┐
│              │  Case #12345                        │
│ Case List    │  ┌───┬─────────┬──────────┬────────┐│
│              │  │Det│ Related │ Activity │        ││
│ □ Case #123  │  └───┴─────────┴──────────┴────────┘│
│ ■ Case #124  │                                     │
│ □ Case #125  │  Subject: Email not working         │
│ □ Case #126  │  Priority: High                     │
│              │  Status: In Progress                │
│              │                                     │
│              │  [Resolve] [Escalate]               │
└──────────────┴─────────────────────────────────────┘

Use Cases:

  • Customer service (Case management)
  • Sales (Opportunity pipeline + details)
  • Helpdesk (Ticket queue + ticket details)

Wizard Template

Multi-step flow with progress indicator.

# page_template: wizard
steps:
  - name: account_info
    label: Account Information
    fields: [company_name, industry, size]
  - name: contact_details
    label: Contact Details
    fields: [name, email, phone]
  - name: preferences
    label: Preferences
    fields: [newsletter, notifications]
  - name: review
    label: Review & Submit
    type: summary

Visual Layout:

┌────────────────────────────────────────────────────┐
│  New Customer Setup                                │
│  ● Account Info → ○ Contact → ○ Preferences → ○   │
├────────────────────────────────────────────────────┤
│                                                    │
│  Account Information                               │
│                                                    │
│  Company Name: _______________________________     │
│  Industry:     [Select Industry ▼]                 │
│  Company Size: ○ 1-10  ○ 11-50  ○ 51-200          │
│                                                    │
│                          [Cancel]  [Next Step →]   │
└────────────────────────────────────────────────────┘

The 12-Column Grid System

All layouts use a responsive grid that divides space into 12 columns.

Basic Grid Layout

section:
  label: Contact Information
  columns: 2  # Each field takes 6/12 columns (50% width)
  fields:
    - name      # Column 1-6
    - email     # Column 7-12
    - phone     # Column 1-6 (new row)
    - company   # Column 7-12

Rendered Grid:

┌──────────────────────┬──────────────────────┐
│ Name: ______________ │ Email: _____________ │
├──────────────────────┼──────────────────────┤
│ Phone: _____________ │ Company: ___________ │
└──────────────────────┴──────────────────────┘

Custom Span Widths

section:
  label: Product Details
  layout:
    - field: product_name
      span: 12  # Full width
    - field: price
      span: 4   # 33% width
    - field: quantity
      span: 4   # 33% width
    - field: total
      span: 4   # 33% width
    - field: description
      span: 12  # Full width

Rendered Grid:

┌─────────────────────────────────────────────────┐
│ Product Name: _________________________________│
├───────────────┬───────────────┬────────────────┤
│ Price: $_____ │ Qty: ________ │ Total: $______ │
├───────────────┴───────────────┴────────────────┤
│ Description:                                    │
│ ____________________________________________    │
│ ____________________________________________    │
└─────────────────────────────────────────────────┘

Responsive Breakpoints

Grid automatically collapses on smaller screens:

section:
  columns: 3  # Desktop: 3 columns, Tablet: 2 columns, Mobile: 1 column
  fields: [field_a, field_b, field_c, field_d, field_e, field_f]

Desktop (≥1024px): 3 columns

┌──────────┬──────────┬──────────┐
│ Field A  │ Field B  │ Field C  │
├──────────┼──────────┼──────────┤
│ Field D  │ Field E  │ Field F  │
└──────────┴──────────┴──────────┘

Tablet (768-1023px): 2 columns

┌──────────────┬──────────────┐
│ Field A      │ Field B      │
├──────────────┼──────────────┤
│ Field C      │ Field D      │
├──────────────┼──────────────┤
│ Field E      │ Field F      │
└──────────────┴──────────────┘

Mobile (less than 768px): 1 column

┌────────────────────────┐
│ Field A                │
├────────────────────────┤
│ Field B                │
├────────────────────────┤
│ Field C                │
├────────────────────────┤
│ Field D                │
├────────────────────────┤
│ Field E                │
├────────────────────────┤
│ Field F                │
└────────────────────────┘

Sections: Organizing Fields

Sections are collapsible containers for related fields.

Basic Section

sections:
  - label: Contact Information
    collapsible: true
    collapsed: false  # Expanded by default
    fields:
      - name
      - email
      - phone

Rendered:

┌─ Contact Information ──────────────────────────▼─┐
│                                                  │
│  Name:  __________________________________________│
│  Email: __________________________________________│
│  Phone: __________________________________________│
│                                                  │
└──────────────────────────────────────────────────┘

Conditional Sections

Show sections based on field values or permissions:

sections:
  - label: Basic Info
    fields: [name, email]
  
  - label: Billing Information
    visible:
      field: account_type
      value: premium  # Only show for premium accounts
    fields: [payment_method, billing_address]
  
  - label: Admin Settings
    visible:
      permission: admin  # Only show to admins
    fields: [api_key, rate_limit]

Section Variants

sections:
  - label: Quick Summary
    variant: compact  # Reduced padding
    columns: 4
    fields: [status, priority, assignee, due_date]
  
  - label: Description
    variant: spacious  # Extra padding
    fields: [long_description]
  
  - label: Danger Zone
    variant: danger  # Red border, warning icon
    fields: [archive, delete]

Field Groups and Inline Layout

Inline Field Group

Group related fields on the same row:

sections:
  - label: Name
    layout:
      - type: field_group
        inline: true
        fields:
          - field: first_name
            span: 6
            placeholder: First
          - field: last_name
            span: 6
            placeholder: Last

Rendered:

Name
┌──────────────────────┬──────────────────────┐
│ First: _____________ │ Last: ______________ │
└──────────────────────┴──────────────────────┘

Address Field Group

- type: field_group
  label: Address
  fields:
    - field: street
      span: 12
    - field: city
      span: 6
    - field: state
      span: 3
    - field: postal_code
      span: 3

Rendered:

Address
┌─────────────────────────────────────────────────┐
│ Street: _______________________________________│
├──────────────────────┬───────────┬────────────┤
│ City: _______________ │ State: __ │ ZIP: _____ │
└──────────────────────┴───────────┴────────────┘

Tabs: Multi-Page Layouts

Organize large forms into tabbed sections.

Basic Tabs

layout:
  mode: tabbed
  tabs:
    - name: details
      label: Details
      icon: file-text
      sections:
        - label: Basic Info
          fields: [name, email, phone]
    
    - name: address
      label: Address
      icon: map-pin
      sections:
        - label: Primary Address
          fields: [street, city, state, zip]
    
    - name: preferences
      label: Preferences
      icon: settings
      sections:
        - label: Notifications
          fields: [email_notifications, sms_notifications]

Rendered:

┌──────────────────────────────────────────────────┐
│ [Details] [Address] [Preferences]                │
├──────────────────────────────────────────────────┤
│                                                  │
│  Basic Info                                      │
│  Name:  __________________________________________│
│  Email: __________________________________________│
│  Phone: __________________________________________│
│                                                  │
└──────────────────────────────────────────────────┘

Lazy-Loaded Tabs

Load tab content only when clicked (performance optimization):

tabs:
  - name: details
    label: Details
    lazy: false  # Load immediately
  
  - name: history
    label: History (1,234 records)
    lazy: true   # Load when tab clicked
    source: /api/customers/123/history

Tab Badges and Counters

tabs:
  - name: details
    label: Details
  
  - name: tasks
    label: Tasks
    badge: 5  # Show "5" badge
    badgeVariant: danger  # Red badge
  
  - name: notes
    label: Notes
    badge: { count: 12, variant: info }

Responsive Layout Modifiers

Device-Specific Visibility

fields:
  - name: detailed_description
    span: 12
    visible:
      desktop: true   # Show on desktop
      tablet: true    # Show on tablet
      mobile: false   # Hide on mobile
  
  - name: short_summary
    span: 12
    visible:
      desktop: false  # Hide on desktop
      tablet: false   # Hide on tablet
      mobile: true    # Show on mobile

Orientation-Specific Layout

section:
  columns:
    portrait: 1   # 1 column in portrait mode
    landscape: 2  # 2 columns in landscape mode
  fields: [field_a, field_b, field_c]

Related Lists: Embedding Child Records

Display child records within a parent record's page.

Basic Related List

sections:
  - type: related_list
    label: Contacts
    object: contact
    relationField: account_id  # contact.account_id → account.id
    columns:
      - name
      - email
      - phone
    actions:
      - type: standard_new
        label: New Contact

Rendered:

┌─ Contacts ─────────────────────────────── [+ New Contact] ─┐
│                                                             │
│  Name              Email              Phone                │
│  ────────────────  ─────────────────  ─────────────────    │
│  John Doe          john@acme.com      555-1234            │
│  Jane Smith        jane@acme.com      555-5678            │
│                                                             │
│  Showing 2 of 2 contacts                                    │
└─────────────────────────────────────────────────────────────┘

Inline Editing Related List

sections:
  - type: related_list
    label: Invoice Line Items
    object: invoice_line
    relationField: invoice_id
    mode: inline_edit  # Edit cells directly
    columns:
      - field: product
        type: lookup
        object: product
      - field: quantity
        type: number
        editable: true
      - field: unit_price
        type: currency
        editable: true
      - field: total
        type: formula
        formula: quantity * unit_price
        editable: false
    actions:
      - type: add_row
        label: Add Line Item

Widgets: Embedding Rich Components

Widgets are pre-built UI components that display data or provide functionality.

Metric Widget

sections:
  - type: widget
    component: metric
    config:
      title: Open Opportunities
      value: 47
      trend: +12%
      trendDirection: up
      icon: trending-up
      color: success

Rendered:

┌─────────────────────┐
│ Open Opportunities  │
│                     │
│      47      ↑ +12% │
│                     │
└─────────────────────┘

Activity Feed Widget

sections:
  - type: widget
    component: activity_feed
    config:
      object: activity
      filter: { related_to: '{recordId}' }
      limit: 10
      showFilters: true

Custom Widget

sections:
  - type: widget
    component: custom.approval_timeline
    config:
      recordId: '{recordId}'
      showComments: true

Advanced Layouts

Master-Detail Split View

layout:
  type: split_view
  orientation: horizontal  # or 'vertical'
  split: 60  # Left: 60%, Right: 40%
  left:
    type: list_view
    object: opportunity
    view: my_opportunities
  right:
    type: detail_view
    object: opportunity
    recordId: '{selectedRecordId}'

Grid of Cards

layout:
  type: card_grid
  columns:
    desktop: 3
    tablet: 2
    mobile: 1
  cards:
    - type: metric_card
      title: Revenue
      value: $1.2M
    - type: metric_card
      title: Deals
      value: 47
    - type: chart_card
      title: Pipeline
      chartType: funnel

Kanban Board Layout

layout:
  type: kanban
  object: project_task
  groupBy: status
  columns:
    - value: todo
      label: To Do
      color: gray
    - value: in_progress
      label: In Progress
      color: blue
    - value: done
      label: Done
      color: green
  cardFields:
    - title
    - assignee
    - due_date

Layout Schema Reference

Page Definition

// Source: packages/spec/src/ui/page.zod.ts (PageSchema)
interface Page {
  name: string;          // lowercase snake_case
  label: string;
  description?: string;
  icon?: string;
  type?: 'record' | 'home' | 'app' | 'utility' | 'list'
       | 'dashboard' | 'form' | 'record_detail'
       | 'record_review' | 'overview' | 'blank';  // default 'record'
  object?: string;       // bound object for record pages
  template?: string;     // layout template name, default 'default'
  regions: PageRegion[]; // named regions, each holding components
}

interface PageRegion {
  name: string;          // e.g. "header", "main", "sidebar"
  width?: 'small' | 'medium' | 'large' | 'full';
  components: PageComponent[];
}

Section Definition

// Source: packages/spec/src/ui/view.zod.ts (FormSectionSchema)
interface FormSection {
  name?: string;          // stable snake_case id for i18n
  label?: string;
  description?: string;
  collapsible?: boolean;  // default false
  collapsed?: boolean;    // default false
  columns?: 1 | 2 | 3 | 4;  // default 1
  visibleWhen?: string;   // CEL visibility predicate; hides section when false
  fields: (string | FormField)[];
}

Field Layout

// Source: packages/spec/src/ui/view.zod.ts (FormFieldSchema)
interface FormField {
  field: string;     // Field name (snake_case)
  type?: FieldType;  // auto-infers widget when omitted
  colSpan?: number;  // Column span within the section grid (1-4)
  label?: string;    // Override label
  placeholder?: string;
  helpText?: string;
  required?: boolean;
  readonly?: boolean;
  hidden?: boolean;
  widget?: string;     // custom widget override
  visibleWhen?: string;  // CEL visibility predicate
}

Responsive Columns

// Source: packages/spec/src/ui/responsive.zod.ts (BreakpointColumnMapSchema)
// Grid columns (1-12) per named breakpoint. All entries optional.
interface ResponsiveColumns {
  xs?: number;    // ~480px
  sm?: number;    // ~640px
  md?: number;    // ~768px
  lg?: number;    // ~1024px
  xl?: number;    // ~1280px
  '2xl'?: number; // ~1536px
}

Visibility Rule

Visibility is a single CEL expression string, not a structured rule object. Since ADR-0089 the one canonical key is visibleWhen across every layer — data fields, view form sections/fields, and page components — aligning with the readonlyWhen / requiredWhen family. The element is shown only when the expression evaluates truthy.

// e.g. on a PageComponent:
visibleWhen: "record.account_type == 'premium'"

// e.g. on a FormSection / FormField:
visibleWhen: "record.status != 'closed' && user.hasRole('admin')"

The predicate's binding root is set by the layer, not the key:

Layer Predicate binds
Runtime record forms & pages (*.view.ts, *.page.ts) record + current_user (pages also expose page.<var>)
Metadata-editing forms (*.form.ts) data — the row under edit

The legacy spellings visibleOn (view) and visibility (page) are @deprecated aliases: still accepted and folded into visibleWhen at parse time, so existing metadata keeps working. Author new metadata with visibleWhen.

Since @objectstack/spec 15 (ADR-0089 D3a) the form field/section and page component schemas are strict: a key they do not declare — a visibleWhen typo, a page-only visibility pasted onto a view field (or vice-versa), or a stale key — is now a loud parse error instead of being silently dropped. The error names the offending key and, when it looks like a visibility predicate, points you back at visibleWhen.

Breakpoint-based show/hide is handled separately via the component's responsive.hiddenOn array (e.g. hiddenOn: ['xs', 'sm']), see packages/spec/src/ui/responsive.zod.ts.

Real-World Examples

Customer 360 Page

name: customer_360
object: customer
template: standard
regions:
  header:
    - type: title
      field: name
    - type: actions
      buttons: [edit, delete, share, convert]
  
  main:
    - type: section
      label: Overview
      columns: 2
      fields:
        - name
        - status
        - industry
        - employee_count
        - website
        - phone
    
    - type: section
      label: Key Contacts
      columns: 1
      component:
        - type: related_list
          object: contact
          relationField: account_id
          columns: [name, title, email, phone]
    
    - type: section
      label: Open Opportunities
      component:
        - type: related_list
          object: opportunity
          relationField: account_id
          filter: { stage: { $ne: 'closed_won' } }
          columns: [name, amount, close_date, stage]
  
  sidebar:
    - type: widget
      component: metric
      config:
        title: Total Revenue
        value: $1.2M
    
    - type: widget
      component: activity_feed
      config:
        limit: 10

Multi-Step Onboarding Wizard

name: employee_onboarding
object: employee
template: wizard
steps:
  - name: personal_info
    label: Personal Information
    sections:
      - label: Basic Details
        columns: 2
        fields:
          - first_name
          - last_name
          - email
          - phone
          - date_of_birth
          - ssn
  
  - name: employment
    label: Employment Details
    sections:
      - label: Position
        fields:
          - job_title
          - department
          - manager
          - start_date
          - employment_type
  
  - name: compensation
    label: Compensation
    sections:
      - label: Salary & Benefits
        fields:
          - base_salary
          - bonus_eligible
          - equity_grant
          - benefits_plan
  
  - name: review
    label: Review & Submit
    type: summary
    sections:
      - type: summary_view
        showAllFields: true

Performance Considerations

Lazy Loading

Load sections/tabs only when visible:

sections:
  - label: Details
    lazy: false  # Load immediately
  
  - label: History (10,000 records)
    lazy: true   # Load when section expanded
    source: /api/customers/123/history

Virtual Scrolling

For long lists of fields:

section:
  label: Product Catalog (1,000 items)
  virtualScroll: true
  itemHeight: 60  # px
  fields: [...]  # Large array

Progressive Rendering

Render above-the-fold content first:

layout:
  renderStrategy: progressive
  priority:
    - sections[0]  # Render first section immediately
    - sections[1]  # Render second section after 100ms
    - sections[2]  # Render third section after 200ms

What's Next?

} title="Widget Contract" href="/docs/protocol/objectui/widget-contract" description="Standard props and events for UI components" /> } title="Action Protocol" href="/docs/protocol/objectui/actions" description="Buttons, triggers, and navigation flows" />

Related Resources