> ## Documentation Index
> Fetch the complete documentation index at: https://docs.joyfill.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Decorators

Decorators are tappable indicators (icon + label) attached to a **field**, **column**, **row**, or **cell**. Taps are delivered through **`onFocus`** so your app can run custom logic — navigation, uploads, etc.

## Decorator model

```kotlin theme={null}
val decorator = Decorator().apply {
    action = "openHelp"     // optional in Kotlin, unique within scope when set
    icon   = "circle-info"  // optional, see Supported icons
    label  = "Help"         // optional
    color  = "#3B82F6"      // optional, must be #RRGGBB
}
```

| Property | Type      | Description                                             |
| -------- | --------- | ------------------------------------------------------- |
| `action` | `String?` | Non-empty and unique among decorators at the same path. |
| `icon`   | `String?` | Icon name (see [Supported icons](#supported-icons)).    |
| `label`  | `String?` | Text shown next to or instead of the icon.              |
| `color`  | `String?` | 6-digit hex color: `#RRGGBB` (e.g. `#3B82F6`).          |

The control is shown only when there is a non-empty **`icon`** or **`label`** (`isDisplayable`). Action-only entries are stored but not displayed.

## Constructing a path

`DocumentEditor` resolves decorators using a slash-separated path. Every path starts with `pageId/fieldPositionId`. What you append after that determines what gets decorated.

> **Reserved keywords.** The path grammar uses three reserved tokens — **`schemas`**, **`rows`**, and **`columns`**. Anything else in a path slot is treated as an id (page id, field-position id, row id, column id, or schema key). Don't use these keywords as ids.

### Field decorators

Just the two ids. Applies to the field's header.

```kotlin theme={null}
val fieldPath = "$pageId/$fieldPositionId"
```

### Table — `/rows`, `/columns/colId`, or specific `rowId` / `rowId/colId`

A table has four decorator scopes, two **common** (defaults applied everywhere) and two **specific** (overrides for one row or cell):

| What you want                                   | Append           | Example                          |
| ----------------------------------------------- | ---------------- | -------------------------------- |
| Common decorators on **every row**              | `/rows`          | `pageId/fpId/rows`               |
| Decorators on **one specific row**              | `/rowId`         | `pageId/fpId/row_42`             |
| Common decorators on **every cell in a column** | `/columns/colId` | `pageId/fpId/columns/col_status` |
| Decorators on **one specific cell**             | `/rowId/colId`   | `pageId/fpId/row_42/col_status`  |

Specific paths inherit from the matching common path on the first write — anything you set on `/rows` shows on `row_42` until you write to `row_42` directly.

### Collection — same as table, plus `/schemas/schemaKey/…` for nested rows

A collection's **root** rows behave like a table — the four scopes above use the exact same path shapes.

Take a "People" collection where each person row holds a nested "Addresses" schema:

```
schema "people"     (root, children: [addresses])
schema "addresses"  (nested under people)

Rows:
  p_alice                                  ← row in "people"
    addresses → [ addr_home, addr_work ]   ← rows in "addresses", under p_alice
  p_bob
    addresses → [ addr_apt ]
```

**Common rows / columns** of any schema — root or nested — are schema-level defaults. Address them directly with `schemas/schemaKey/…`, no parent walk needed:

| What you want                | Path shape                             | Example                                         |
| ---------------------------- | -------------------------------------- | ----------------------------------------------- |
| Common rows in any schema    | `pageId/fpId/schemas/sk/rows`          | `pageId/fpId/schemas/addresses/rows`            |
| Common columns in any schema | `pageId/fpId/schemas/sk/columns/colId` | `pageId/fpId/schemas/addresses/columns/col_zip` |

**A specific nested row or cell** lives under a particular parent. Walk through that parent's row id, then `schemas/sk/`, then the nested row id:

| What you want        | Path shape                             | Example                                                   |
| -------------------- | -------------------------------------- | --------------------------------------------------------- |
| Specific nested row  | `…/rowId/schemas/sk/nestedRowId`       | `pageId/fpId/p_alice/schemas/addresses/addr_home`         |
| Specific nested cell | `…/rowId/schemas/sk/nestedRowId/colId` | `pageId/fpId/p_alice/schemas/addresses/addr_home/col_zip` |

If `addresses` itself had children, you'd chain another `schemas/.../rowId/…` after `addr_home` — the same pattern repeats for every level.

> **Schema keys** come from the field's `schema` map. The schema marked `root: true` holds top-level rows; its `children` array names the nested schemas reachable from a row in this schema.

## API

Four methods, all on `documentEditor.decorators`. Errors are reported via **`onError`** as `JoyfillError.DecoratorError(DecoratorError)`.

```kotlin theme={null}
val fieldPath = "$pageId/$fieldPositionId"

documentEditor.decorators.get(fieldPath)                          // -> List<Decorator>
documentEditor.decorators.add(fieldPath, decorator)
documentEditor.decorators.update(fieldPath, action = "openHelp", decorator = updated)
documentEditor.decorators.remove(fieldPath, action = "openHelp")
```

Same shape for every path scope. A few examples:

```kotlin theme={null}
// Common row decorators on a table — applied to every row
documentEditor.decorators.add("$pageId/$fpId/rows", duplicate)

// Override on a specific row
documentEditor.decorators.add("$pageId/$fpId/$rowId", archive)

// Cell-specific decorator
documentEditor.decorators.add("$pageId/$fpId/$rowId/$colId", upload)

// Nested collection row
val nestedPath = "$pageId/$fpId/$parentRowId/schemas/$nestedSK/$nestedRowId"
documentEditor.decorators.add(nestedPath, comment)
```

## Behavior to know

* **Copy-on-write seed.** First write to a row-self / cell scope seeds from the matching common scope, so existing common decorators stay visible on that row alongside your override. Subsequent writes diverge freely.

* **Collection license gating.** Writes against a collection field require a license that enables collection features. Without it, the call emits `decoratorError` and is rejected.

## Handling taps

Decorator taps come through **`onFocus`** with the decorator's `action` exposed on the field event's `type` / `target`. `rowIds` / `columnId` / `parentPath` on `FieldIdentifier` tell you where the user tapped.

```kotlin theme={null}
onFocus = { event ->
    val field = event.fieldEvent ?: return@onFocus

    val action = field.type
    if (!action.isNullOrEmpty()) {
        // Decorator tap
        println("Decorator: $action, field: ${field.fieldID}, rows: ${field.rowIds ?: []}, column: ${field.columnId ?: "-"}")
    } else {
        // Ordinary field focus
    }
}
```

See [Event handling](../architecture/v2/event-system) for the full focus/blur flow.

## Errors

All four APIs report through `onError` as `JoyfillError.DecoratorError(DecoratorError)`:

* Path didn't resolve (bad ids, deleted row, malformed grammar)
* Validation (`action` empty, `color` not `#RRGGBB`)
* Duplicate `action` in batch or against an existing entry
* `remove` / `update` with an unknown `action`
* Collection write without a valid license

Reads (`get`) on an unresolvable path also emit `onError` and return `[]`.

## Display limits

`DecoratorConfig`, passed to `DocumentEditor` at init, controls how many decorators render inline before the rest collapse into a kebab menu.

```kotlin theme={null}
val config = DecoratorConfig(
    visibleLimitInFields = 2,   // field + column scopes; default 2
    visibleLimitInRows = 1      // row scopes; default 1
)
val editor = DocumentEditor(document = doc, decoratorConfig = config)
```

## Supported icons

The SDK maps common names to bundled artwork, including: `camera`, `import`, `paperclip`, `image`, `file`, `comment`, `comments`, `upload`, `download`, `rotate`, `cloud`, `filter`, `share`, `paper-plane`, `folder`, `folder-open`, `magnet`, `eye`, `circle-info`, `add`, `plus`, `print`, `flag`. Unknown names fall back to a default symbol.
