Documentation source
Entity Card System
Unified schema-driven entity cards with custom overrides, container-query responsiveness, and agent-writable card configuration.
## Problem
Entity cards are rendered with generic, identical layouts everywhere -- grid view, kanban, chat results, entity-card blocks. The old compatibility alias at `features/entities/components/registry.ts` was unused and has since been removed; the canonical registry lives under `features/entities/components/entity-card/`. A custom `OpportunityCard` component exists but is never registered or rendered. The grid/kanban/mobile views have inline card rendering with no customization hooks.
Entity types already declare `config.ui.cardFields` in seed data, but nothing reads this config for card rendering. Every entity type looks the same regardless of its schema, and there is no path for agents to customize card appearance without code deployment.
## Solution
Build a unified `EntityCard` component with a three-tier resolution chain: code-registered custom components, DB-stored declarative `cardConfig`, and smart schema-derived defaults. All three tiers render into the same layout zones and use CSS container queries for responsive behavior. One component replaces all inline card rendering across grid, kanban, chat results, block system, and mobile fallback views.
## Design
### Architecture
The resolution chain determines which renderer handles each entity type:
```
resolveEntityCard(typeSlug, entityType) ->
1. Code registry hit? -> CustomCardComponent
2. entityType.config.ui.cardConfig exists? -> ConfigDrivenCard
3. fallback -> SchemaCard (smart default)
```
All three tiers produce the same `EntityCardRenderProps` contract and render inside the same container-query responsive shell. The `EntityCard` component is the single public API -- consumers never interact with inner components directly.
Component hierarchy:
```
EntityCard (resolution + shell + container queries)
CustomCardComponent (code-defined, registered per type slug)
ConfigDrivenCard (renders from cardConfig JSON)
SchemaCard (auto-classifies fields from json_schema)
```
### Card Layout Zones
All tiers render into a consistent zone structure:
- **Header**: Avatar, title, type label, badges, score
- **Hero**: Large prominent metric (currency values, key numbers)
- **Body**: Description text, truncated via line-clamp based on container size
- **Metrics**: Small stats row (numbers, dates, tag counts)
- **Secondary**: Additional fields, visible only at expanded size
Container query breakpoints:
- Compact (below 280px): header only (title + badges + score) -- kanban columns
- Standard (280-479px): header + hero + body (2 lines) + metrics -- grid cells
- Expanded (480px+): all zones including secondary fields -- detail previews
Container queries are CSS-only (SSR-safe). No JS-based size detection.
### CardConfig -- Agent-Writable Configuration
Stored in `entity_types.config.ui.cardConfig`. Agents write this via the `updateEntityTypeSchema` admin tool.
```typescript
interface CardConfig {
hero?: string; // Field name for large metric
badges?: string[]; // Field names for header badges
description?: string; // Field name for body text
metrics?: string[]; // Field names for stat row
accent?: {
field: string;
map: Record<string, string>; // value -> color token
};
hide?: string[]; // Fields to exclude from card
}
```
Accent color values are restricted to an allowlist of Tailwind color tokens to prevent CSS injection. The `hide` array takes precedence over the legacy `cardFields` array.
### Field Classification (Smart Defaults)
When no `cardConfig` exists, `SchemaCard` auto-classifies fields from `json_schema.properties`:
| Pattern | Zone | Examples |
|---------|------|----------|
| Field with `enum` in schema | badges | status, category, priority |
| Named `description`, `summary`, `notes`, `overview` | description | -- |
| Number with currency-ish name (`value`, `revenue`, `cost`, `price`, `amount`) | hero | estimated_annual_value |
| Other number fields | metrics | score, progress_pct |
| Boolean fields | badges (as icon) | is_active, approved |
| Array fields | badges (tag row) | tags, categories |
| Date fields | metrics | date, deadline |
| Everything else | secondary (hidden on compact) | -- |
If `config.ui.cardFields` is set, those fields get priority placement regardless of heuristics. Classification results are memoized per entity type (keyed by type slug + schema hash), not per entity.
### API
No new API endpoints. The card system is purely client-side rendering.
Key exported functions:
- `registerEntityCard(typeSlug, component)` -- register a custom card renderer
- `resolveEntityCard(typeSlug, entityType)` -- resolve which component to render
- `classifyFields(schema, content, config)` -- classify schema fields into layout zones
### Integration Points
The `EntityCard` component replaces inline card rendering in:
- `EntityGridView` -- replace inline cards with `<EntityCard variant="standard" />`
- `EntityKanbanView` -- replace inline cards with `<EntityCard variant="compact" />`
- `DataTable` mobile fallback -- replace simple card list
- `entity-card-block` -- delegate to `EntityCard` when full entity data is available, keep lightweight fallback for chat tool results with partial data
- `from-chat.ts` bridge -- enhance to pass full entity/entityType when available
### Custom Component Registration
Custom cards register in `features/custom/components/entity-cards.ts`, imported once at app startup:
```typescript
import { registerEntityCard } from "@/features/entities/components/entity-card/registry";
import { OpportunityCard } from "./opportunity/opportunity-card";
registerEntityCard("opportunity", OpportunityCard);
```
The existing `OpportunityCard` gets refactored to accept the `EntityCardRenderProps` contract.
## Trade-offs
**Three-tier resolution vs. config-only**: A config-only approach would be simpler but cannot handle genuinely custom rendering logic (charts inside cards, complex conditional layouts). The three-tier chain keeps config as the 90% path while allowing code overrides for the 10%.
**Container queries vs. variant prop**: Container queries make the card truly context-independent -- it adapts to whatever space it is placed in. A variant prop would require every consumer to know the right variant. Container queries are well-supported in modern browsers and are SSR-safe.
**Flat CardConfig vs. nested layout tree**: A flat config with named zones is easy for agents to generate in one tool call. A nested layout tree would be more powerful but harder to validate and more error-prone for AI authoring.
**No DB migration**: `cardConfig` is stored in the existing `config` JSONB column on `entity_types`. This avoids a migration but means the config is not independently queryable. This is acceptable because card config is always read alongside the entity type.
## Acceptance Criteria
- [ ] Single `EntityCard` component renders everywhere: grid, kanban, chat, blocks, mobile
- [ ] Schema-driven default card shows meaningful field layout without any configuration
- [ ] `cardConfig` in entity type config controls card layout zones
- [ ] Custom card components register and resolve correctly via registry
- [ ] Container queries adapt card layout to available width without prop changes
- [ ] Existing `OpportunityCard` works through the new registry
- [ ] `classifyFields` correctly categorizes all JSON schema field types
- [ ] Memoization prevents re-classification per entity in a list
- [ ] Unit tests for `classifyFields` covering all field type heuristics
- [ ] Unit tests for registry resolution chain
- [ ] Visual verification in grid, kanban, chat, and block contexts
- [ ] No regressions in existing entity type rendering
## Files
### New
- `features/entities/components/entity-card/types.ts` -- EntityCardProps, EntityCardRenderProps, CardConfig, ClassifiedFields, CardConfigSchema (Zod)
- `features/entities/components/entity-card/registry.ts` -- registerEntityCard, resolveEntityCard
- `features/entities/components/entity-card/classify-fields.ts` -- field classification heuristics
- `features/entities/components/entity-card/classify-fields.test.ts` -- unit tests for classification
- `features/entities/components/entity-card/schema-card.tsx` -- smart default card from schema
- `features/entities/components/entity-card/config-driven-card.tsx` -- renders from CardConfig JSON
- `features/entities/components/entity-card/entity-card.tsx` -- public API component (resolution + shell + container queries)
- `features/entities/components/entity-card/entity-card.test.ts` -- registry and resolution tests
- `features/entities/components/entity-card/index.ts` -- barrel export
- `features/custom/components/entity-cards.ts` -- custom card registration barrel
### Modified
- `features/entities/components/entity-grid-view.tsx` -- replace inline cards with EntityCard
- `features/entities/components/entity-kanban-view.tsx` -- replace inline cards with EntityCard
- `features/entities/components/data-table/data-table.tsx` -- replace mobile card fallback with EntityCard
- `features/blocks/components/entity-card-block.tsx` -- delegate to EntityCard when full entity data available
- `features/custom/components/opportunity/opportunity-card.tsx` -- refactor to EntityCardRenderProps contract
- `features/entities/types.ts` -- add CardConfig to EntityTypeConfig.ui
- `features/entities/components/entity-card/` -- canonical registry/types location after the old compatibility alias was removed