Skip to content

Detail views

Opening a record shows a record view, not a form: a hero header (title, badge, key stats, actions), the fields laid out read-first in titled sections, and a meta sidebar. Hit Edit to turn the fields into inputs; changes batch into one save (⌘S). Inline child tables are declared separately in relations { }.

The detail { } block is entirely optional — it only refines this view.

Zero-config

With no detail block at all, cartapel already produces a good record view:

  • the hero shows the title, the first badge field, and a copyable id;
  • fields group by their field { group = … } tags, with ungrouped fields in one untitled card;
  • metadata columns are detected by name (id, *_id, *_at, *_ts) and pulled into the meta sidebar automatically — the primary key stays in the hero;
  • every configured table with a foreign key into this one appears as an inline automatically;
  • everything is read-first with an Edit toggle.

You reach for detail { } only to name sections, add hero stats, or change how the record opens.

detail { }

hcl
detail {
  mode    = "page"        # "page" | "drawer" | "modal"
  columns = 1             # section grid column count
  tabs    = true          # render sections as tabs
  stats   = ["mrr", "plan", "country"]

  section {
    title       = "Identity"
    fields      = ["name", "email", "country"]
    span        = 2         # span both columns
    collapsible = true
  }

  section {
    title  = "Account"
    fields = ["plan", "mrr", "active"]
  }
}
KeyTypeDescription
modeenumHow the record opens: page (default), drawer (side panel), or modal.
columnsnumberHow many columns the section cards flow into (1 stacks them full-width).
tabsboolRender each section as a tab instead of stacking them.
statslistFields shown as at-a-glance chips in the hero. See Hero stats.
sidebar { }blockOverride the auto-detected meta rail. Usually unnecessary.
section { }block (repeatable)A titled group of fields. Order is preserved.

Hero stats

stats lists the fields to surface as chips across the top of the record — the numbers you'd otherwise hunt for in the body. Each renders with its own widget, so a number shows a big figure and a custom:minibar rating shows its bar.

hcl
detail {
  stats = ["mrr", "plan", "active", "country"]
}

A stat can repeat a field that also lives in a section — the chip is a shortcut, not a move.

Sections

A section groups related fields under a heading. Sections render in the order written; any field not placed in a section falls into a trailing "Other" group.

hcl
detail {
  section {
    title  = "Identity"
    fields = ["id", "name", "email", "country"]
  }
  section {
    title  = "Plan"
    fields = ["plan", "mrr"]
  }
  section {
    title  = "Status"
    fields = ["active", "created_at"]
  }
}

Per-section options:

KeyDescription
titleSection heading. Required.
fieldsColumns in this section, in order.
spanHow many form columns the section spans.
collapsibleWhether the section can be collapsed.

Two ways to assign a field to a section

You can either list fields inside section { fields = [...] }, or tag a field from its own block with field "email" { group = "Contact" }. Use whichever reads better; they compose.

The meta rail is automatic: any metadata-named column (id, *_id, *_at, *_ts) you don't place in a section is detected and shown there — so you rarely write a sidebar block at all. A meta field you do list in a section stays in that section.

Override the auto rail only when you want a specific, curated set:

hcl
detail {
  sidebar {
    fields = ["id", "customer_id", "product_id", "started_at", "renews_at"]
  }
}

Modes

mode chooses how a record is presented when opened from a list:

  • page (default) — a full detail page with its own URL.
  • drawer — a side panel that slides over the list.
  • modal — a centered dialog.

Inlines

Inlines embed rows of a related child table directly in a parent's detail view — the classic "orders on a customer" layout.

Zero-config

Inlines are on by default: with no relations { } block at all, every configured table with an introspected foreign key into this one appears as an inline automatically. Opt out with:

hcl
relations {
  auto = false
}

Declaring any inlines = [...] list takes manual control — only the listed inlines render.

Simple form

List child table names and cartapel infers the foreign key:

hcl
relations {
  inlines = ["orders", "subscriptions"]
}

Each inline renders the child table's own list columns, paginated 50 rows at a time, and respects that child table's permissions and role rules.

Full form

For control over the FK, label, columns, and whether rows can be added/removed inline, use object syntax:

hcl
relations {
  inlines = [
    {
      table      = "order_items"
      fk_col     = "order_id"        # explicit FK column (inferred if omitted)
      label      = "Line items"      # section heading (defaults to the table name, humanized)
      columns    = ["product_id", "qty", "unit_price"]
      can_create = false             # allow creating child rows inline
      can_delete = true              # allow deleting child rows inline
    },
  ]
}
KeyDescription
tableThe child table name. Required.
fk_colThe child column pointing back at this record. When omitted, inferred from the introspected FK, then guessed by name (<parent>_id / <parent-singular>_id).
labelHeading for the inline section. Defaults to the child table name, humanized.
columnsChild columns to show. Omit → the child's list columns.
can_createWhether new child rows can be created inline.
can_deleteWhether child rows can be deleted inline.

Inline edits still pass through the child table's permission checks — an inline can never grant access the child config withholds. See Roles & permissions.

Released under the MIT License.