Skip to content

Fields & widgets

A field "column" { } block controls how one column is rendered and edited. Only the columns you want to customize need a block; the rest use introspected defaults.

hcl
field "price" {
  label  = "Unit price"
  widget = "money"
  params = { currency = "USD" }
  format = "currency"
  prefix = "$"
  color  = "sign"
}

Field options

KeyTypeDescription
labelstringOverride the column header / detail label.
labelsmapPer-locale label overrides (labels = { es = "Precio" }); the instance locale picks one, label is the fallback.
widgetstringThe renderer — a built-in name or custom:<name>. See the widget library.
readonlyboolField is shown but not editable (per-field variant of edit.readonly).
maskedboolValue is masked in lists, detail, search and export — for everyone, admins included. Secret-shaped columns (names containing token, secret, password, api_key, private_key, …) mask automatically even without a field block; declaring any field block for such a column takes back control (add masked = true to keep it hidden). See Security.
sqlstringA trusted SQL expression that makes this a computed, read-only column (see below).
sortableboolComputed columns only: list sort orders by the sql expression (see Computed columns).
sort_bystringComputed columns only: sort by another real column instead of the expression.
groupstringDetail-form section this field belongs to (an alternative to detail { section { } }).
paramsmapWidget-specific parameters (see each widget).
imageblockMarks the field as an uploadable image (see image { }).
formatstringA number/date formatter applied to the value.
prefix / suffixstringText prepended / appended to the displayed value.
truncatenumberTruncate the displayed string to N characters.
displaystringA {column} template that replaces the displayed text.
hrefstringA {column} template turning the value into a link target.
colorstring / blockConditional coloring — a named strategy or a rule set (see Conditional color).

Computed columns (sql)

A field with a sql expression is a virtual, read-only column that doesn't exist in the table. cartapel selects it as (<sql>) AS "<name>". The current row is aliased t, so you can aggregate related tables:

hcl
field "orders_30d" {
  label  = "Orders 30d"
  widget = "number"
  sql    = "(SELECT count(*) FROM orders o WHERE o.customer_id = t.id AND o.placed_at > now() - interval '30 days')::int"
}

field "age_days" {
  label  = "Age (days)"
  widget = "number"
  sql    = "extract(day from now() - t.created_at)::int"
}

The expression is trusted config, not user input, and is read-only.

By default a computed column is display-only. Make it sortable — like Django's @admin.display(ordering=…):

hcl
field "line_total" {
  label    = "Line total"
  sql      = "t.qty * t.unit_price"
  sortable = true                 # list sort orders BY the expression
}

field "order_count" {
  label   = "Orders"
  sql     = "(SELECT count(*) FROM orders o WHERE o.customer_id = t.id)::int"
  sort_by = "created_at"          # …or sort by another real column instead
}
KeyDescription
sortableOrder the list by the sql expression when this column's header is used.
sort_byOrder by another real column instead of the expression.

A sort_by that names a column a role has masked is refused for that role (so ordering can't leak a hidden value). An sql expression that references a masked column can still order by it — keep masked columns out of sortable expressions.

Formatting

format runs the value through a formatter. The vocabulary is fixed:

formatRenders
currencyLocalized currency.
percentPercentage.
numberGrouped number with separators.
dateDate only.
datetimeDate and time.
bytesHuman byte size (1.4 MB).
durationHuman duration from seconds.

prefix, suffix and truncate are independent string tweaks you can combine with any widget:

hcl
field "win_rate" {
  format   = "percent"
  suffix   = "%"
  truncate = 40
}

Interpolation: display and href

Both take a template with {column} placeholders filled from the row:

hcl
field "name" {
  display = "{name} ({country})"
  href    = "https://crm.example/u/{id}"
}

display replaces the shown text; href makes the cell a link. (For an explicit link widget with a new-tab option, see link / url.)

Conditional color

color tints a value based on its content. Two forms.

Named strategies

hcl
field "pnl" {
  format = "currency"
  color  = "sign"
}
StrategyEffect
signPositive green, negative red.
positiveHighlight positive values.
negativeHighlight negative values.
staleHighlight stale / old values.

Rule sets

For explicit thresholds, use color { rule "…" { class = "…" } }. Rules are evaluated in order; the first match wins.

hcl
field "score" {
  color {
    rule ">0"          { class = "good" }
    rule "<0"          { class = "critical" }
    rule "between:1,2" { class = "warning" }
    rule "=n/a"        { class = "muted" }
  }
}

Rule conditions (when):

FormMatches
>N, >=N, <N, <=NNumeric comparison.
between:LO,HINumeric range.
=textExact string equality.

Rule classes (class) must be one of: good, warning, critical, neutral, accent, muted. Any other class or an unparseable condition is a load error.

Image uploads

An image { } block turns a column into an uploadable, on-disk-resized image:

hcl
field "image" {
  image {
    dir       = "products"     # subdirectory under the config bundle
    name_col  = "sku"          # column supplying the stored filename
    max_px    = 256            # longest edge, default 256
    normalize = true           # re-encode/normalize on upload, default true
  }
}

Uploaded images are served back through cartapel's static asset route.


Widget library

Set widget = "<name>". Widgets that take parameters read them from the field's params map. Unlisted params keys are ignored.

Text & structured

WidgetRendersNotable params
textPlain text (the default).
textareaMulti-line text; wraps in detail, truncates in lists.
codeMonospace code block; syntax-aware in detail.lang (e.g. python, sql)
jsonPretty JSON tree in detail, compact preview in lists.
maskedRenders the (already-masked) value in monospace.
truncateTruncates to N chars with a full-value tooltip.chars (default 40)
copyableValue with a click-to-copy affordance.
uuidShortened UUID with click-to-copy.

Numbers

WidgetRendersNotable params
numberRight-aligned, grouped number.
moneyCurrency-formatted amount.currency (e.g. USD)
percentPercentage (negatives tinted in lists).
durationHuman duration from a seconds value.
bytesHuman byte size.
progressA horizontal progress bar + percent.max (default 100), warn_at, color
ratingA row of icons (e.g. stars).max (default 5), icon (default )
trendSigned value with ▲/▼ arrow, colored by sign.
heatcellA cell tinted by magnitude within a range.min (default 0), max (default 100)

Booleans & enums

WidgetRendersNotable params
toggleA check / dash for truthy / falsy.
badgeA colored badge from a value → color map.colors
pillSame as badge (pill styling).colors
tagsSplits a list/CSV value into multiple badges.colors

The colors param maps values to one of blue, green, orange, red, violet, gray:

hcl
field "status" {
  widget = "badge"
  params = { colors = { active = "green", past_due = "orange", cancelled = "gray" } }
}

Time

WidgetRendersNotable params
datetimeLocalized date + time.
relative_time"3 minutes ago", tinted when stale.warn_after (seconds; older values warn)
hcl
field "renews_at" {
  widget = "relative_time"
  params = { warn_after = 900 }
}
WidgetRendersNotable params
link / urlA hyperlink; target from href or params.href.href (template), new_tab (bool)
emailA mailto: link.
phoneA tel: link.
hcl
field "homepage" {
  widget = "url"
  params = { href = "{homepage}", new_tab = true }
}

Media & identity

WidgetRendersNotable params
imageAn inline image from a URL/data-URL value.
avatarA small (optionally round) avatar image.size (12–96, default 24), rounded (default true)
colorA swatch + the color string.
country / flagA flag emoji + the country code.

Relations & arrays

WidgetRendersNotable params
fkA link to the referenced record, using its label.target (table), target_column
arrayEach array element as a small chip.

Foreign-key columns are detected during introspection and render automatically as links showing the target row's label — its name-ish column (name, title, symbol, email, label, else the first text column) — instead of the raw id, in lists, detail views and inlines alike. Masked FK columns and targets you cannot view fall back to the raw value. The fk widget is the explicit form: set params = { target = "..." } to declare (or override) the drill-through target when no database FK exists — for example on a computed column — and target_column when the reference isn't the target's primary key.

Custom widgets

Any widget name of the form custom:<name> loads a web component you ship in the config bundle at config/widgets/<name>.js. It receives the full row and the field's params. Unknown custom widgets fall back to the raw value — never a crash.

hcl
field "equity" {
  widget = "custom:sparkline"       # → /static/config/widgets/sparkline.js
  params = { field = "equity_curve", color = "blue" }
}

The three bundled custom widgets — sparkline, statuspill, minibar — and how to author your own are covered in Pages & queries.

Released under the MIT License.