← Back to Index

CardHero - Data Source Analysis

Component: CardHero
Work Item: WI-2122
Type: Domain control — Document Hero: composes the generic CardHeroViewModel scaffold via DocumentHeroFactory (maps DocumentItem → scaffold; does not subclass it — see Decisions D11)
Last Updated: 2026-08-26 — image fallback changed (AB#2580): no-image/failed-image now hides the media slot entirely instead of substituting placeholder art (see Image, below); 2026-07-15 implemented: ViewModel and factory shipped (WI-2262, D58/D59); as-built sections below reconciled from a pre-implementation draft (2026-06-05)

ASCII Diagram — CardHero Layout

Shows how CardHeroViewModel properties map to the rendered control.

   ┌──────────────────────────────────────────────────────────┐
   │                                                          │
   │   ┌──────────────────────────────────────────────────┐   │
   │   │                                                  │   │
   │   │              [  Image Area  ]                    │   │ ◄── Image (ImageSource)
   │   │         background illustration                  │   │     465×500px child frame
   │   │              465 × 500 px                        │   │
   │   │                                                  │   │
   │   └──────────────────────────────────────────────────┘   │
   │                                                          │
   │   Primary Heading Text                                   │ ◄── Title (string)
   │                                                          │     Large, bold — main message
   │   Brief description text goes here                       │ ◄── Description (string)
   │   spanning two lines for content                         │     2-line supplemental text
   │                                                          │
   │   [  Entire card is tappable  ]  ────────────────────────┼──► Link (string)
   │                                                          │     Navigates to detail/article view
   └──────────────────────────────────────────────────────────┘
         718 × 504 px  |  fill: none (transparent)  |  stroke: 1px

   IsLoading ───────────►  Loading state — default strategy (no skeleton/overlay)

   ✅ Implemented — CardHeroViewModel + CardHero.razor + DocumentHeroFactory shipped (D58/D59)
   ✅ Former Scope/Category gap resolved in practice — shipped consumers don't query by fixed scope/category

Implemented (2026-07-15). CardHeroViewModel ships in ALPAMobile.Presentation/ViewModels/CardHeroViewModel.cs (the single home since the 2026-07-15 WI-2254/D61 cutover — the head's ComponentViewModels.cs bundle is deleted), rendered by ALPAMobile.Presentation/Components/Library/CardHero.razor, and DocumentHeroFactory ships in ALPAMobile.Presentation/Components/DocumentHeroFactory.cs (WI-2262, D58; migrated with WI-2254, D61). This document originally pre-dated the implementation; the mapping and factory sections below now reflect the as-built shape, with deltas from the original draft called out inline.


Summary

Property Status Source Notes
Image ✅ Resolved DocumentItem.Image via DataManager Hero illustration — same field used by document list pages
Title ✅ Resolved DocumentItem.Title via DataManager Direct string match
Description ✅ Resolved DocumentItem.Description via DataManager Field exists on DocumentItem — confirmed
Link ✅ Resolved DocumentHeroFactory.OpenRoute(fileId)/document-open Route resolves through the legacy HandleDocumentActionAsync path — never a raw Path href (P1)
PublishDate ✅ Bonus DocumentItem.FormattedPublishDate Available if hero card needs a date stamp
IsLoading ✅ Available ViewModel state Default strategy — no skeleton overlay
Scope/Category ✅ Resolved DocumentItem.Scope + DocumentItem.Category Fields on model — query via DataManager.GetDocumentCategoriesForScopeAsyncGetDocumentsForScopeCategoryAsync; scope+category string values are a content config decision, not a code gap

DocumentCardViewModel — Generic Scaffold Base (as shipped)

DocumentItem aligns directly with the CardHero content properties, but the base itself stays domain-free — it holds generic presentation properties only. The DocumentItem → scaffold mapping lives in the domain control (DocumentHeroFactory), not on the base. The shipped base (bindable, nullable) differs from the original draft in three ways: it adds Eyebrow and AccentColor, Link/IsLinkVisible live on the CardViewModel parent (where IsLinkVisible checks Link or LinkText), and there is no PublishDate property — the date is available on DocumentItem.FormattedPublishDate but no shipped consumer renders it on a hero card yet.

// As shipped — ALPAMobile/Components/ViewModels/ComponentViewModels.cs
/// Abstract base for content cards with title/description/image/link (card-lg). Domain-free.
public abstract class DocumentCardViewModel : CardViewModel
{
    public string? Eyebrow { get; set; }       // bindable via SetProperty in real code
    public string? Title { get; set; }
    public string? Description { get; set; }
    public string? Image { get; set; }
    public string? AccentColor { get; set; }   // left accent color token
    // Link + IsLinkVisible inherited from CardViewModel
}

/// Hero / featured content card — rendered by the Document Hero domain control.
public class CardHeroViewModel : DocumentCardViewModel { }

Generic scaffold cards on this base: CardHeroViewModel (hero/featured content). The domain binding that fills it is the Document Hero control below; the same base is reusable by any future content-card domain control.

Former gap resolved in practice: the original draft's open question — which Scope/Category strings feed hero content — no longer blocks anything: the shipped consumers don't query by a fixed scope/category. Favorites resolves a specific document by fileId (GetDocumentByFileIdAsync); the dynamic feed delivers hero items pre-selected by the server. A future fixed-slot hero (the BuildAsync pattern below) would still need its scope/category chosen by the content team at configuration time.


✅ CardHero — Implemented

(This section originally documented that no ViewModel existed — superseded 2026-07-15.) The shipped pieces and their consumers:

The Figma export identifies two unnamed structural child frames: - background illustration (FRAME, 465×500px) — contains the hero image/illustration - Hero (FRAME, 477×539px) — contains the text content and tap target


Mapping — Factory (as built, D58)

The Document Hero domain control keeps the D11 split: the generic CardHeroViewModel scaffold (domain-free) and a pure factory that maps a DocumentItem onto it — the async lookup stays with the caller. The scaffold never references DocumentItem.

// As shipped — ALPAMobile/Components/DocumentHeroFactory.cs (DI singleton)
public sealed class DocumentHeroFactory
{
    // The internal route that opens a document through the document-open flow.
    // Documents must ALWAYS open through this route — never a raw DocumentItem.Path
    // href (P1: relative paths navigated the WebView onto a dead local URL and
    // killed the Blazor session; public URLs can't display gated member content).
    public static string OpenRoute(string fileId) =>
        $"/document-open?fileId={Uri.EscapeDataString(fileId)}";

    // Link is caller-supplied because fileId keying differs per context:
    // Favorites keys by Favorite.ItemId; direct consumers by DocumentItem.FileID.
    public CardHeroViewModel Create(DocumentItem document, string? link) => new()
    {
        Title       = document.Title,
        Description = document.Description,
        Image       = document.Image,
        Link        = link,
    };
}

Deltas from the original draft (kept below for the record):

// Future pattern (not shipped) — a fixed-slot hero that queries by scope/category
public sealed class DocumentHeroService(IDocumentsQueries documents, DocumentHeroFactory factory)
{
    public async Task<CardHeroViewModel?> BuildAsync(string scope, string category)
    {
        var docs = await documents.GetDocumentsForScopeCategoryAsync(scope, category);
        var doc = docs?.FirstOrDefault();
        return doc is null ? null : factory.Create(doc, DocumentHeroFactory.OpenRoute(doc.FileID));
    }
}

Properties

Image

Title

Description

IsLinkVisible

IsLoading


Data Dependencies

Service Interface Notes
DataManager DataManager GetDocumentCategoriesForScopeAsync + GetDocumentsForScopeCategoryAsync — same pattern as Pilot Card
DocumentItem ALPADocs.Data.Models.DocumentItem All content fields: Title, Description, Image, FileID, FormattedPublishDate
SharedActionsService SharedActionsService Link/file resolution and navigation

Figma Structural Reference

Layer Name Type Size (pt) Fill Notes
mobile hero (root) FRAME 718×504 none (transparent) Outer card container; stroke 1px
background illustration FRAME 465×500 Hero image/illustration slot
Hero FRAME 477×539 Content slot: title, description, tap target

Note: Figma children are unnamed Group frames — structural detail is sparse in the .fig export. Layer names above are from the raw Figma export metadata provided in the component spec.



Document Owner: ALPA Mobile Team
Work Item: WI-2122
Purpose: Data source analysis for CardHero presentation-layer implementation phase