Documentation source
Display Type Simplification
Unify the display type system — collapse media types, extract shared renderers, clean up inconsistencies
> **Superseded.** Phase 1 (media-type collapse) and Phase 3 (format utility cleanup) shipped — that work stands and is the foundation. Phase 2 (extract a shared `FieldValueRenderer` from the four entity-only renderers) is **superseded** by `docs/superpowers/specs/2026-04-16-unified-tool-platform.md`. The new spec defines `FieldDefinition` (storage `type` + `displayType` separation, generalized to tools / entities / criteria / blocks / future schemas) along with `<FieldDefinitionForm/>` and `<FieldDefinitionDisplay/>` that subsume what the entity-only `FieldValueRenderer` would have been. Adoption order is documented in the new spec's Phase Roadmap (Phase 7 — entity types and criteria sets adopt the unified primitive).
>
> What you can rely on from this spec: the canonical 16-member `FieldDisplayType` union, the `displayType: "media"` collapse with `mediaLayout` + `mediaKind` config, `classifyValue()` and `normalizeDisplayType()` semantics, and the `statusMap` pattern for status display. All preserved and built on by the new spec.
## Problem
The display type system has grown to 20 types across 4 independent renderers that each re-implement the same type-to-rendering mapping. Adding the 5 media types exposed the pattern: each new type requires touching all 4 renderer files with duplicated switch/if logic.
Additionally, `displayType` and `mediaLayout` address overlapping concerns. Having `displayType: "images"` AND `mediaLayout: "gallery"` is redundant — the 5 media display types (`image`, `images`, `video`, `videos`, `media`) are really just shorthand for `displayType: "media"` + layout/kind config.
## Design Decision: Collapse Media Types to `media` + Config
**Keep `displayType: "media"` as the single media type. Remove `image`, `images`, `video`, `videos` from the union.**
Rationale:
- `displayType` answers "what kind of thing is this?" — the answer is "media" for all 5 variants
- `mediaLayout` answers "how should it be shown?" — showcase, gallery, carousel, grid, inline
- `mediaKind` answers "what flavor?" — image, video, mixed
- Having `displayType: "images"` is encoding layout preference INTO the type, which is what `mediaLayout` already does
- This is the same pattern as `displayType: "status"` + `statusMap` — the type says it's a status, the config says how to render it
**The field name heuristics still work** — they just set config instead of different types:
- `hero_image` → `displayType: "media"`, `mediaKind: "image"`, `mediaLayout: "showcase"`
- `gallery` → `displayType: "media"`, `mediaKind: "image"`, `mediaLayout: "gallery"`
- `video_url` → `displayType: "media"`, `mediaKind: "video"`, `mediaLayout: "showcase"`
- `videos` → `displayType: "media"`, `mediaKind: "video"`, `mediaLayout: "gallery"`
**Union goes from 20 → 16 types.** Every renderer drops 5 cases and adds 1.
## Design Decision: Extract Shared Renderer Families
The 16 remaining types group into 5 rendering families:
| Family | Types | Rendering |
|--------|-------|-----------|
| **numeric** | metric, currency, percentage, bytes, duration | `formatDisplayValue()` + tabular-nums styling |
| **categorical** | enum, status, boolean, tags | Badges / chips / switches |
| **linkable** | url, email, phone | Clickable `<a>` with icon |
| **media** | media | `MediaFieldValue` with layout/kind config |
| **text** | text, long-text, date | Plain text / markdown / date format |
Create `features/entities/components/field-value-renderer.tsx` — a single shared component that takes `(value, displayType, config, mode)` and renders correctly for any context. Each current renderer calls this instead of maintaining its own switch.
`mode` controls density:
- `"full"` — field-card bento grid (rich, spacious)
- `"compact"` — data table cells (inline, truncated)
- `"card"` — entity card fields (small badges)
- `"panel"` — detail panel (medium density)
This eliminates the 4x duplication. New display types are added in one place.
## Phases
### Phase 1: Collapse media types ✅ COMPLETE
1. ✅ Removed `image`, `images`, `video`, `videos` from `FieldDisplayType` union and `VALID_DISPLAY_TYPES`
2. ✅ Single `media` entry in the union
3. ✅ Updated `classifyValue()`:
- Field name heuristics return `"media"` for all image/video patterns
- Added `classifyMediaConfig(fieldName)` → `{ mediaLayout, mediaKind }` helper
- Added `normalizeDisplayType(t)` — maps legacy stored values (`image`/`images`/`video`/`videos`) → `"media"` for backwards compatibility
4. ✅ Removed `MEDIA_DISPLAY_TYPES` set and `isMediaDisplayType()` — callers now use `=== "media"` (or `normalizeDisplayType()` at boundaries where stored legacy values may appear)
5. ✅ Updated all 4 renderers: collapsed 5 media cases into 1 single `"media"` case
6. ✅ Updated admin DisplayTypePicker: removed individual media types, single "Media (image / video)" entry
7. ✅ Updated admin DisplaySection: shows media layout + kind pickers when `displayType === "media"` (including legacy values via normalization)
8. ✅ `FieldConfig.mediaLayout` + `FieldConfig.mediaKind` are now the canonical way to configure media rendering
9. ✅ Tests: `classifyValue` media heuristics, `classifyMediaConfig`, `normalizeDisplayType`, and DisplaySection media UI
### Phase 2: Extract shared field value renderer — DEFERRED
Not required for production readiness. The 4 renderers already funnel through the same `MediaFieldValue` / `StatusChip` / `FormattedFieldValue` sub-components. The remaining switch duplication is small (each renderer's switch is tight and context-appropriate: different density, different interactions). Extracting a shared `FieldValueRenderer` would remove ~80 lines of duplication at the cost of an additional layer of indirection and a multi-mode component that each caller would have to thread through. Punting until we add the 17th display type.
### Phase 3: Clean up format utilities ✅ COMPLETE
1. ✅ `features/tools/lib/format.ts` `DisplayType` documented as a subset of canonical `FieldDisplayType`, with `"number"` removed from the type (stored schemas using `"number"` keep working via a runtime alias — the type just steers new code toward `"metric"`).
## Acceptance Criteria
- [x] `FieldDisplayType` union has 16 members (not 20)
- [x] Only one `"media"` display type, configured via `mediaLayout` + `mediaKind`
- [ ] ~~Single `FieldValueRenderer` component~~ (deferred — see Phase 2)
- [x] Admin picker matches the canonical union exactly (no mismatches)
- [x] All renderers handle media consistently via `=== "media"` (or `normalizeDisplayType` at stored-value boundaries)
- [x] All existing tests pass + new tests for `classifyMediaConfig`, `normalizeDisplayType`, media UI visibility
- [x] Field name heuristics still auto-detect media fields correctly (now producing `{ displayType: "media", mediaKind, mediaLayout }`)
- [x] Legacy stored values (`x-display-type: "image"`, etc.) keep rendering — no data migration required
## What This Does NOT Change
- The `classifyValue()` priority chain (schema > format > enum > heuristic > value)
- How `FieldConfig` is stored in `entity_types.config.fields`
- How `x-display-type` works on JSON schema
- The `statusMap` pattern for status display type
- Block-level rendering (blocks handle their own rendering; this is about field-level display)