Sprinter Docs

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.

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:

PatternZoneExamples
Field with enum in schemabadgesstatus, category, priority
Named description, summary, notes, overviewdescription--
Number with currency-ish name (value, revenue, cost, price, amount)heroestimated_annual_value
Other number fieldsmetricsscore, progress_pct
Boolean fieldsbadges (as icon)is_active, approved
Array fieldsbadges (tag row)tags, categories
Date fieldsmetricsdate, deadline
Everything elsesecondary (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:

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

On this page