← Back to Component Index

Component Architecture — ViewModel Hierarchy

The component-library ViewModel tree and a reference table of every ViewModel. Use this to find a ViewModel, see what it derives from, and jump to its component spec. Rooted at ComponentViewModel (decision D6).

Track A scaffold: 9 components Track B typed domain: 3 components Updated: 2026-07-06 17:22 ET (D46D49) Audited: 2026-07-02 13:37 ET

1. Hierarchy — Track A Scaffold

The reusable, domain-agnostic scaffold (Track A). Cards and Button share tappable chrome through SurfaceViewModel; containers share a paintable wrapper through ContainerSurfaceViewModel — both abstract intermediates under ComponentViewModel. Both surface bases use token-name strings (not concrete Color) for background so the same ViewModel works on both XAML and CSS/HTML rendering paths. See § 5 for the dual-target surface model. Complex flight/duty-period components use Track B (see § Track B). Domain controls compose scaffold ViewModels via factories (see § Domain Controls).

ComponentViewModel (abstract) — library root · renderable state (IsVisible / IsEnabled / IsLoading)
├── SurfaceViewModel : ComponentViewModel (abstract) — tappable chrome (BackgroundToken / CornerRadius / Padding / TapCommand)
│   ├── CardViewModel : SurfaceViewModel — content-surface base → Card
│   │   ├── DocumentCardViewModel : CardViewModel (abstract) → CardHero
│   │   ├── CardTextViewModel : CardViewModel → CardText
│   │   ├── CardSmallViewModel : CardViewModel → CardSmall
│   │   └── ButtonCardViewModel : CardViewModel → Button Card (CallToAction : ButtonViewModel · D14)
│   └── ButtonViewModel : SurfaceViewModel → Button (optional Icon; absorbs former Icon Tile)
└── ContainerSurfaceViewModel : ComponentViewModel (abstract) — paintable container wrapper (BackgroundToken / CornerRadius / Padding · no tap) · D26
    ├── ButtonGroupViewModel : ContainerSurfaceViewModel → Button Group · ObservableCollection<ButtonViewModel>
    ├── GridContainerViewModel : ContainerSurfaceViewModel → Grid Container · Items: ObservableCollection<ButtonCardViewModel> · 3-col fixed · D25
    ├── CarouselViewModel : ContainerSurfaceViewModel → Carousel · Items: ObservableCollection<ComponentViewModel>
    └── ListViewModel : ContainerSurfaceViewModel → List · Items: ObservableCollection<ComponentViewModel>
root base ViewModel abstract component (link)

2. ViewModel Reference

Every ViewModel. Click a column header to sort.

ViewModel Base Renders Category Figma source
ComponentViewModelObservableObject— (root)Base
SurfaceViewModelComponentViewModel— (tappable chrome base)Surface basecard-btn
ContainerSurfaceViewModel New · D26ComponentViewModel— (paintable container base)Surface base
CardViewModelSurfaceViewModelCardContent surfacecard-lg
DocumentCardViewModel (abstract)CardViewModelCardHero baseContent surface
CardHeroViewModelDocumentCardViewModelCardHeroContent surfacecard-lg · mobile hero
CardTextViewModelCardViewModelCardTextContent surfacecard-md
CardSmallViewModelCardViewModelCardSmallContent surfacecard-sm
ButtonCardViewModel New · D14CardViewModelButton CardContent surfacecard-button · node 4104:5451
FlightSegmentCardViewModel → Track B · D17movedFlight Segment CardTrack Bflight card-og · D16 amended
ButtonViewModelSurfaceViewModelButtonAction controlbutton-sm · card-btn
ButtonGroupViewModelContainerSurfaceViewModelButton GroupContainer3 × card-btn
GridContainerViewModel New · D25ContainerSurfaceViewModelGrid ContainerContainerMEC row frame pattern · 3-col · card-btn only (v1)
CarouselViewModelContainerSurfaceViewModelCarouselContainerslider · horizontal scrolling feed
ListViewModelContainerSurfaceViewModelListContainerlisty card generic

3. Domain Controls

A domain control binds real domain data to a generic scaffold component. It is not a scaffold subclass — it composes one through a pure mapper/factory (data in → scaffold ViewModel out), keeping the library domain-free. The async data gathering lives in a DI-injected domain service; the factory only maps (decision D11). Full catalog: Domain Controls.

Domain controlRenders throughDomain sourceMapper / factorySpec
Pilot Card Card · Chevron variant (CardViewModel) UserInfo via IAuthentication + contract DocumentItem via DataManager PilotCardFactory : RawRepresentationFactory<CardViewModel> data-source analysis
Document Hero CardHero (CardHeroViewModel) featured/contract DocumentItem via DataManager DocumentHeroFactory : RawRepresentationFactory<CardHeroViewModel> data-source analysis
Menu Text CardText (CardTextViewModel) MenuItem (+ parent category) + local Settings MenuTextFactory : RawRepresentationFactory<CardTextViewModel> data-source analysis
Did You Know List ABANDONED List (ListViewModel) Change 4 abandoned 2026-06-24 — no endpoint will be built; kept here for historical ListViewModel factory-pattern reference only DidYouKnowListFactory : RawRepresentationFactory<ListViewModel> — not to be implemented data-source analysis
Home Feed Carousel Carousel (CarouselViewModel) content feed (DocumentItem[] via DataManager) FeedCarouselFactory : RawRepresentationFactory<CarouselViewModel> data-source analysis
Resource Tile CardSmall (CardSmallViewModel) MenuItem (nav / resource links) ResourceTileFactory : RawRepresentationFactory<CardSmallViewModel> data-source analysis
Emergency Hotline Button (ButtonViewModel) emergency contact config (tel:) EmergencyButtonFactory : RawRepresentationFactory<ButtonViewModel> data-source analysis
Flight Card Track B · D17 FlightCardView (typed ContentView — BindableProperty fields) Flight / Leg (flight segment data — source TBD) FlightCardView (no factory — direct property binding) spec
Duty Period Card Track B · D17 DutyPeriodCardView (typed ContentView — 3 variants: Default / Expanded / Delete) Duty period / schedule data — source TBD DutyPeriodCardView (no factory — direct property binding) pending
Flight Finder Filter Sort New · D41 composes flight finder filters · radio button · tab group (standalone, D40 pattern) JSFF flight-search filter/sort state — confirmed 2026-07-06 (D41): JumpseatFlightFinderResultsFiltersPageViewModel.cs FlightFinderFilterSortFactoryspec spec
Flight Finder Advanced Search New · D44 Expander (standalone domain-control layer over the primitive) JSFF flight-search advanced-filter state — confirmed 2026-07-06 (D44): JumpseatFlightFinderSearchPageViewModel.cs FlightFinderAdvancedSearchFactoryspec spec
Promo Banner New · Provisional · D43 PromoBannerViewModel (standalone, D40 pattern) Promo/announcement content — source confirmed 2026-07-06 (D43): existing PageBanner model (GET /api/pagebanner/list), not a new Dynamic Feed type. Field additions tracked as Change 5. PromoBannerFactoryspec spec

New component tiers (2026-06-10 re-sync): beyond the scaffold, the library now has nav chrome (Top/Bottom Navigation, Nav Controls, Navigation Drawer, Segmented Control), atoms (NavItem, Badge, Info Block, Endpoint, Logo, Back), new primitives (Form Field, Progress Bar, Section Title), and page templates (Page/Interior/Feed). Screens stay app-side (D14). Full set: New Components Catalog.

Scaffold stays generic: each control maps its domain source onto the generic scaffold ViewModel (CardHeroViewModel, CardTextViewModel, ListViewModel) via a pure factory — the scaffold types carry no domain references. Persistence (favorites, badge counts) and async gathering live in the domain service, not the scaffold.

4. Track B — Typed Domain Components

Components that always bind to exactly one domain type and require ≥ 5 domain-specific fields use typed ContentView subclasses with BindableProperty fields instead of the scaffold ViewModel hierarchy. They are registered via DataTemplate so they drop into any CollectionView without losing type safety. The factory pattern (RawRepresentationFactory<T>) is replaced by direct property binding. Track C — Blazor Razor components with @parameter typed binding — is deferred to Epic #2087 (decision D17).

ComponentContentView classDomain typeKey BindablePropertiesFigma master
Flight Card FlightCardView Flight / Leg Origin, Destination, DepartureTime, ArrivalTime, Duration, FlightNumber, AircraftType, StatusBadge spec · D15/D16 masters
Duty Period Card DutyPeriodCardView Duty period / schedule DateRange, DutyType, FlightRows, TotalTime, Status; variants: Default / Expanded / Delete-demo node 4617:13876
Flight Leg Info FlightLegInfoView Flight leg (departure or arrival) Direction, Gate, ActionLinks, DirectionLabel, UpdatedTimestamp, StatusBadge (delay duration) node 4325:6399 (297×154)

5. Dual-Target Surface Model

Both surface hierarchies (SurfaceViewModel for cards/buttons, ContainerSurfaceViewModel for containers) must render correctly on two independent targets: XAML/MAUI and CSS/HTML (Blazor Hybrid). Holding a concrete .NET Color breaks both paths — XAML bypasses ResourceDictionary merge, CSS forces an inline specificity conflict. The fix is the same for both bases: a token-name string from the Surface/* namespace.

✓ Resolved — SurfaceViewModel.Background updated to BackgroundToken: string

Both SurfaceViewModel and ContainerSurfaceViewModel now hold BackgroundToken: string. The 6 leaf classes (CardViewModel, CardHeroViewModel, CardTextViewModel, CardSmallViewModel, ButtonCardViewModel, ButtonViewModel) inherit this token automatically. Factories and domain controls pass a token name, not a resolved color.

SurfaceViewModel — canonical definition

public abstract class SurfaceViewModel : ComponentViewModel
{
    // Token name from the Surface/* namespace — not a Color.
    // XAML: IThemeResolver.Resolve(BackgroundToken) → Color
    // CSS:  BackgroundToken.ToCssVar() → var(--surface-brand)
    public string    BackgroundToken { get; set; } = "Surface/Default";
    public double    CornerRadius    { get; set; } = 0;
    public Thickness Padding         { get; set; } = Thickness.Zero;
    public ICommand? TapCommand      { get; set; }

    protected SurfaceViewModel() { }
    protected SurfaceViewModel(Object? obj) : base(obj) { }
}

How each rendering path resolves the token

Each path independently resolves the token string without forcing a concrete color into the ViewModel:

Rendering pathHow the token resolvesMEC override mechanism
XAML / MAUI IThemeResolver.Resolve(BackgroundToken)Color
View binds: BackgroundColor="{Binding ResolvedBackground}"
Resolver reads the current ResourceDictionary
ResourceDictionary.MergedDictionaries — load base AlpaTheme.xaml, merge MecTheme.xaml at runtime. IThemeResolver always reads the merged dictionary, so the MEC color is picked up without re-binding.
CSS / HTML (Blazor) BackgroundToken.ToKebabCssVar()var(--surface-brand)
Razor emits: style="background-color: var(--surface-brand)"
CSS custom property override on a MEC-scoped selector:
:root[data-mec="UAL"] { --surface-brand: #002244; }
The var() reference always resolves to the nearest declared value — no inline specificity conflict.

Both surface bases are aligned. SurfaceViewModel.BackgroundToken and ContainerSurfaceViewModel.BackgroundToken are both token strings — same pattern, same resolution path on XAML and CSS.

CornerRadius and Padding on SurfaceViewModel stay concrete. On the item base (SurfaceViewModel), neither participates in MEC theme swapping — they are geometry values. Note: ContainerSurfaceViewModel (the container base — Carousel, ButtonGroup, GridContainer, Stack) uses CornerRadiusToken: string? instead, resolved at render time via IThemeResolver.ResolveRadius() (D38).

Token name convention: Use the Figma Surface/* namespace verbatim as the key (e.g. "Surface/Brand"). XAML resource keys drop the slash: SurfaceBrand. CSS custom properties use kebab-case: --surface-brand. A shared TokenNameHelper utility (post-#2087) will provide ToResourceKey() and ToCssVar() conversions.

6. Inheritance & Mapping Overlaps

One Figma component → two ViewModels: card-lg renders both Card (CardViewModel) and CardHero (CardHeroViewModel). The difference is presentation/use case, not a separate Figma symbol.

Variant, not a sibling: the former Icon Tile is folded into Button as an optional Icon property (decision D3) — there is no IconTileViewModel.

Button Card composition: ButtonCardViewModel : CardViewModel holds a nested CallToAction : ButtonViewModel as a property — it does not multiply-inherit from ButtonViewModel. Domain controls set all three fields (Header, ContentText, CallToAction) via their factory (decision D14).

Flight Card Figma consolidation (D15): five Figma masters (flight card-og, flight segment card, flight card-TABLET, flight card-recent searches, flight card-FTDT) map to one Flight Card domain control. flight segment card (4555:7483) retired as an alias — same 353×111 px dimensions as flight card-og (5452:679). Within this family, flight card-saved & recent searches (D21, live node 4450:9884 confirmed D48 2026-07-06) provides the compact/collapsed (353×61) and expanded-parameters (353×340) states; the JSFF flight card-saved searches (4450:10395) and flight card-recent searches (4450:11208) usage frames instance it directly (swipe-action variant only), not separate masters.

FlightSegmentCardViewModel scaffold (D16, amended by D17): originally added FlightSegmentCardViewModel : CardViewModel as a 10th scaffold component. Superseded by D17 — the 8 typed fields remain a valid description of the display data but the implementation moves to FlightCardView : ContentView (Track B). Scaffold count reverts to 9. FlightCardFactory : RawRepresentationFactory<FlightSegmentCardViewModel> replaced by direct BindableProperty binding on the typed ContentView.

Two-track component pattern (D17): components that always bind to exactly one domain type and have ≥ 5 domain-specific fields use typed ContentView subclasses (Track B) instead of the scaffold hierarchy (Track A). Track A stays domain-free and generic; Track B handles flight/duty-period complexity. Three Track B components: FlightCardView, DutyPeriodCardView, FlightLegInfoView. Track C (Blazor Razor @parameter binding) deferred to Epic #2087.

Card and Button share a base, not a lineage: both derive from SurfaceViewModel (card-like chrome), so Button is aligned with the cards without being a content card — it never inherits Header / ContentText / Link (decision D10).

Containers are not cards: ButtonGroupViewModel, GridContainerViewModel, CarouselViewModel, and ListViewModel derive from ContainerSurfaceViewModel (not CardViewModel) and hold collections of components — they arrange cards, they aren't cards. ContainerSurfaceViewModel adds a paintable wrapper surface (BackgroundToken, CornerRadius, Padding) but no tap handler (decisions D5, D6, D25, D26).