# Display Type System Architecture
This document describes the design contracts and extension points for the
display type system in `potato/server_utils/displays/`.
## Overview
The display system separates **content presentation** from **annotation
collection**. Each field in `instance_display.fields` has a `type` that
maps to a registered display class. Displays produce HTML; annotation
schemas collect labels.
```
Config YAML
└─ instance_display.fields[].type
└─ DisplayRegistry.render()
└─ BaseDisplay.render() → inner HTML
└─ render_display_container() → wrapped HTML
└─ template {{ display_html | safe }}
```
## Key Files
| File | Purpose |
|------|---------|
| `base.py` | `BaseDisplay` ABC — class attributes, abstract `render()`, helpers |
| `registry.py` | `DisplayRegistry` singleton — registration, lookup, render dispatch |
| `../instance_display.py` | `InstanceDisplayRenderer` — orchestrates field rendering |
| `__init__.py` | Package exports |
## BaseDisplay Contract
### Required to implement
| Method / Attribute | Description |
|--------------------|-------------|
| `name: str` | Unique type identifier (e.g., `"dialogue"`) |
| `render(field_config, data) -> str` | Return inner HTML for the field content |
### Optional to override
| Method | Default | When to override |
|--------|---------|------------------|
| `get_css_classes(field_config)` | `["display-field", "display-type-{name}"]` | Add type-specific classes |
| `get_data_attributes(field_config, data)` | `{"field-key", "field-type", "span-target"}` | Add custom data attrs |
| `get_js_init()` | `None` | Return JS to run on page load |
| `validate_config(field_config)` | Checks `required_fields` | Add enum/range validation |
| `has_inline_label(field_config)` | `False` | Return `True` if display renders its own label (avoids duplicate) |
| `get_display_options(field_config)` | Merges `optional_fields` with `display_options` | Rarely needed |
### Class attributes
| Attribute | Type | Description |
|-----------|------|-------------|
| `required_fields` | `List[str]` | Config keys that must be present |
| `optional_fields` | `Dict[str, Any]` | Default values for optional display_options |
| `description` | `str` | Human-readable description |
| `supports_span_target` | `bool` | Whether this type implements the span annotation contract |
## Span Target Contract
**If `supports_span_target = True`, the display MUST satisfy these requirements
when `field_config["span_target"]` is `True`:**
### 1. `.text-content` wrapper
The rendered HTML must contain:
```html
{content HTML}
```
Use the `render_span_wrapper()` helper:
```python
if field_config.get("span_target"):
inner_html = self.render_span_wrapper(field_key, inner_html, plain_text)
```
### 2. `data-original-text` must contain plain text
The `plain_text` argument to `render_span_wrapper()` must be the canonical
plain text that `routes.py` will use for span offset extraction. For
structured data (dialogue, lists), use `concatenate_dialogue_text()` from
`base.py` so both rendering and API extraction use identical formats.
### 3. CSS classes on the outer container
Override `get_css_classes()` to add `"span-target-field"` and
`"span-target-{name}"` when span_target is true.
### 4. Text format consistency
The text format used in `data-original-text` **MUST** match the text
extraction logic in `routes.py` (`/api/spans/` endpoint). If the
data is a list of dicts, both sides must use `concatenate_dialogue_text()`.
### Why this matters
SpanManager (span-core.js) discovers span-target fields via:
```javascript
document.querySelectorAll('.display-field[data-span-target="true"]')
```
Then looks for the text element inside each:
```javascript
const textContent = field.querySelector('.text-content');
```
If `.text-content` is missing, SpanManager silently skips the field and
span annotation will not work.
## Registry
The `display_registry` singleton provides:
- `render(field_type, field_config, data)` — render a field
- `get_supported_types()` — list all registered type names
- `type_supports_span_target(field_type)` — check span target support
- `get_span_target_types()` — list all types supporting span targets
- `validate(field_type, field_config)` — validate config
- `list_displays()` — metadata for all displays
The registry wraps each display's `render()` output in
`render_display_container()`, which adds the outer `.display-field` div,
label, and `.display-field-content` wrapper.
## Instance Display Renderer
`InstanceDisplayRenderer` in `instance_display.py`:
1. Reads `instance_display.fields` from config
2. Queries `display_registry.type_supports_span_target()` for span targets
(no hardcoded list)
3. Warns if `span_target: true` is set on an unsupported type
4. Renders each field via `display_registry.render()`
5. Optionally wraps in resizable container (`_wrap_resizable()`)
## Adding a New Display Type
1. Create `my_display.py` with a class extending `BaseDisplay`
2. Set `name`, `required_fields`, `optional_fields`, `description`
3. If supporting span annotation:
- Set `supports_span_target = True`
- Use `render_span_wrapper()` in `render()` when `span_target` is True
- Override `get_css_classes()` to add `span-target-field`
4. Register in `registry.py` via `DisplayDefinition`
5. Add to `__init__.py` exports
6. Add CSS to `styles.css` using `.display-type-{name}` convention
7. Write unit tests verifying render output
8. Write a contract enforcement test (see `test_display_span_contract.py`)
## Shared Utilities
| Function | Location | Purpose |
|----------|----------|---------|
| `render_span_wrapper(field_key, inner_html, plain_text)` | `BaseDisplay` method | Standard `.text-content` wrapper |
| `concatenate_dialogue_text(data, speaker_key, text_key)` | `base.py` module | Canonical dialogue→plain text conversion |
| `render_display_container(inner_html, classes, attrs, label)` | `base.py` module | Standard outer container wrapper |