String value — plain text with optional truncation:

```vue
<div style="display: flex; flex-direction: column; gap: 8px;">
  <CnCellRenderer value="Short text" :property="{ type: 'string' }" />
  <CnCellRenderer
    value="A very long description that exceeds the truncation limit and will be cut off with an ellipsis on hover"
    :property="{ type: 'string' }"
    :truncate="40" />
</div>
```

Boolean — renders a check icon for `true`, dash for `false`:

```vue
<div style="display: flex; gap: 24px; align-items: center;">
  <div>
    <span style="font-size: 12px; color: var(--color-text-maxcontrast);">true</span>
    <CnCellRenderer :value="true" :property="{ type: 'boolean' }" />
  </div>
  <div>
    <span style="font-size: 12px; color: var(--color-text-maxcontrast);">false</span>
    <CnCellRenderer :value="false" :property="{ type: 'boolean' }" />
  </div>
</div>
```

Enum — renders as a CnStatusBadge:

```vue
<div style="display: flex; gap: 8px; align-items: center;">
  <CnCellRenderer
    value="open"
    :property="{ type: 'string', enum: ['open', 'closed', 'pending'] }" />
  <CnCellRenderer
    value="closed"
    :property="{ type: 'string', enum: ['open', 'closed', 'pending'] }" />
  <CnCellRenderer
    value="pending"
    :property="{ type: 'string', enum: ['open', 'closed', 'pending'] }" />
</div>
```

Array — joined list or dash when empty:

```vue
<div style="display: flex; flex-direction: column; gap: 8px;">
  <CnCellRenderer :value="['tag1', 'tag2', 'tag3']" :property="{ type: 'array' }" />
  <CnCellRenderer :value="[]" :property="{ type: 'array' }" />
</div>
```

Date/UUID — formatted with monospace styling:

```vue
<div style="display: flex; flex-direction: column; gap: 8px;">
  <CnCellRenderer value="2024-03-15T10:30:00Z" :property="{ type: 'string', format: 'date-time' }" />
  <CnCellRenderer value="a1b2c3d4-e5f6-7890-abcd-ef1234567890" :property="{ type: 'string', format: 'uuid' }" />
</div>
```

Column formatter — when a `formatter` id is set and resolves in the injected
`cnFormatters` registry (provided by `CnAppRoot`), the cell renders
`cnFormatters[formatter](value, row, property, formatterOptions)` as text,
overriding the type-aware paths above. The `formatterOptions` prop is the
column's declarative options map passed as the formatter's fourth argument
(e.g. `{ currency: 'USD' }` for the built-in `currency` formatter, or the
`{ negative, zero, positive }` phrases for `conditionalPhrase`) — additive,
three-argument formatters simply ignore it. `CnDataTable` passes
`:formatter="col.formatter"`, `:formatter-options="col.formatterOptions"`, and
`:row="row"` for schema columns; for manual columns it runs the value through
`formatCell(row, col)`. Unknown ids / a missing registry / a throwing formatter
fall back to the normal rendering. See `docs/migrating-to-manifest.md` →
"Column formatters" for the consumer-side wiring (`src/formatters.js` +
`:formatters` on `CnAppRoot`).

```vue
<CnCellRenderer
  value="lead.created"
  :row="{ trigger: 'lead.created', name: 'Welcome flow' }"
  :property="{ type: 'string' }"
  formatter="automationTrigger" />
```

> The example above needs an ancestor that `provide`s `cnFormatters` (e.g.
> `cnFormatters: { automationTrigger: v => ({ 'lead.created': 'Lead created' }[v] ?? v) }`);
> standalone (no provider) it falls back to rendering the raw value.

Column widget — `widget="badge"`, `widget="fkResolve"` (uuid → related object label via the shared object store with per-schema caching; config `widgetProps { register, schema, labelField }`), and `widget="link"` are built-in; any other id resolves against the app's `cellWidgets` registry (`CnAppRoot`'s `cellWidgets` prop → `cnCellWidgets`), and that component receives `{ value, row, property, formatted, ...widgetProps }`. `widget` takes precedence over `formatter`; when both are set the widget gets the formatter-shaped value as `formatted`. See `docs/migrating-to-manifest.md` → "Column widgets" + "Built-in cell formatters / widgets".

- `widget="badge"` — `CnStatusBadge` pill; `widgetProps.variant` picks the colour.
- `widget="link"` — `<router-link>` when `widgetProps.route` is a manifest page id; `<a target="_blank">` when `widgetProps.href` is set (with `{key}` placeholders substituted from the row); plain text + once-per-session `console.warn` when neither resolves (silence with `widgetProps.fallback:"silent"`). For non-`id` route params: `widgetProps.params: { routeParam: "rowField" }`. Default param map is `{ id: row[rowKey] }` — see the `row-key` prop below.

```vue
<div style="display: flex; gap: 12px; align-items: center;">
  <CnCellRenderer value="open" :property="{ type: 'string' }" widget="badge" />
  <CnCellRenderer value="failed" :property="{ type: 'string' }" widget="badge" :widget-props="{ variant: 'error' }" />
</div>
```

The `row-key` prop is used by the built-in `widget="link"` to compute the router-link's default `id` param (`{ id: row[rowKey] }`). Defaults to `'id'`; `CnDataTable` passes its own `rowKey` prop down so a table with a non-`id` identifier still works.
