| Decision | ID | Summary |
|---|---|---|
| Three-track component pattern | D17 | Track A: scaffold VMs + Razor; Track B: typed ContentView; Track C: deferred |
| Typed variant + ComponentStyle | D18 | CardVariant enum; ComponentStyle value object of typed tokens — no CSS strings in VM |
| Vendor strategy | D19 | Build primitives from scratch; own the markup; third-party only for complex generics, always wrapped |
| Component library is an RCL | D20 | ALPAMobile.Presentation targets net10.0, no MAUI dep — consumed by both BlazorWebView host and ASP.NET Core RemoteHost |
The typed surface binding is the ViewModel itself — a ComponentViewModel subclass is the single binding contract consumed by all surfaces (Razor [Parameter] Vm, XAML BindingContext, future surfaces).
ALPAMobile.Presentation/Components/Library/ — RCL since 2026-07-15, WI-2254/D61)Already built, bind via [Parameter, EditorRequired] public ComponentViewModel Vm { get; set; }:
ComponentView.razor — dispatcher switching on ComponentViewModel typeCard.razor, CardHero.razor, CardSmall.razorButton.razor, ButtonGroup.razor, Carousel.razorCardText.razor, ButtonCard.razor, List.razor ← added Phase BResolved 2026-07-15 (WI-2254/D61): the renderers now live where they belong — ALPAMobile.Presentation/Components/Library/, with the project converted to a Razor Class Library. The paragraph below records the pre-migration state for history: WI-2087 resolved 2026-07-01 (ViewModel/Presentation layer extraction), which cleared Phase D's gate; the renderer move itself then executed 2026-07-15 as WI-2254.
ALPAMobile.Presentation/ViewModels/: full hierarchy built and tested. ALPAMobile/Components/ViewModels/ComponentViewModels.cs: all preflight fixes applied.
AlpaTheme.xaml ResourceDictionary built and merged in App.xaml. Dual mapping:
| Token | XAML | Razor (CSS) |
|---|---|---|
Surface/Brand | DynamicResource SurfaceBrand | var(--surface-brand) |
| Base ALPA tokens | StaticResource | CSS custom property |
| MEC-themed tokens | DynamicResource — merged at runtime | CSS custom property swap |
ComponentViewModel (abstract) ← root surface binding type
└── SurfaceViewModel (abstract) ← Background, CornerRadius, Padding, TapCommand
├── CardViewModel (abstract) ← content surface base
│ ├── CardHeroViewModel ← D5 / hero-card spec ✅
│ ├── CardTextViewModel ← D6 / text-card spec ✅
│ ├── CardSmallViewModel ← D7 / card-small spec ✅
│ └── ButtonCardViewModel ← D15 / button-card spec ✅
└── ButtonViewModel ← D8 ✅
ButtonGroupViewModel ← D9 ✅
CarouselViewModel ← D10 ✅
ListViewModel ← D11 ✅
ComponentStyle (D18), CardVariant, SpacingToken, ThemeColor value objects also built in ALPAMobile.Presentation/ValueObjects/.
| Track | Description | Gate | Status |
|---|---|---|---|
| A | Scaffold VMs in ALPAMobile.Presentation + Razor renderers consume them | None | ⚠ Partially accurate — see finding below — VM shapes exist in ALPAMobile.Presentation/ViewModels/ (Phase A + B, commits ac1f7f98, b3fad8f2), but Razor renderers do not consume them: every .razor file still binds to the legacy ALPADocs.Components.ViewModels namespace (ALPAMobile/Components/ViewModels/ComponentViewModels.cs), globally imported via _Imports.razor. Verified 2026-07-13: zero .razor files or production code reference ALPADocs.Presentation.ViewModels. |
| B | Typed ContentView BindableProperty controls for domain types | None | ✅ Complete — Phase C; all 6 Priority-1/2 controls built (commits c8f8ef50 · 2d524033 · e2260f11 · 08b72427 · 60149120) |
| C | Migrate Razor renderers from head into ALPAMobile.Presentation RCL | Phase 5 / WI-2087 complete | ✅ Delivered 2026-07-15 (WI-2254/D61) — all 25 renderers + ComponentView, the adapter factories, FavoriteToggleContext, and the ViewModel cutover (dual-write retired) migrated to the RCL |
Resolution: the staged copies were re-namespaced to ALPADocs.Components.ViewModels and the head bundle deleted (namespace-stable cutover, member parity verified across all 34 types). ALPAMobile.Presentation/ViewModels/ is now the single home; the dual-write convention is retired. The finding below is retained as written for history.
Two parallel definitions of the component-library ViewModels exist under different namespaces, with the
same class names (CardViewModel, ButtonCardViewModel,
ToggleSwitchViewModel, ButtonPillViewModel, PromoBannerViewModel, etc.):
ALPADocs.Components.ViewModels namespace, defined in
ALPAMobile/Components/ViewModels/ComponentViewModels.cs (713 lines, legacy MAUI-head
location). Globally imported via _Imports.razor's @using ALPADocs.Components.ViewModels
— every .razor renderer, ItemComponentFactory.cs, FavoritesViewModel.cs,
and the unit tests all bind to this copy.ALPADocs.Presentation.ViewModels namespace, in
ALPAMobile.Presentation/ViewModels/*.cs (target RCL project, a real
ProjectReference from ALPAMobile.csproj, so it compiles). Verified 2026-07-13:
zero .razor files or production code reference this namespace.Not a one-time snapshot — actively growing. New components are being added to
both copies in the same commits: the PromoBanner (D43) and SearchSecondaryButton (D42) feature
commits each touch ComponentViewModels.cs (legacy, live) and add a matching class to
ALPAMobile.Presentation/ViewModels/ (target, unconsumed). Every new atom/primitive added this
way risks the two copies drifting out of sync, since nothing enforces they stay identical.
This finding is not resolved here — recorded per the
Component Library Reconciliation
Procedure's rule to surface risks for a human decision, not act on them unilaterally. Open questions
for whoever picks this up: is the ALPAMobile.Presentation copy intentional pre-staging for
the eventual Track C cutover, or accidental drift? Have the two copies already diverged in behavior for
any of the 5 newer components? Should new component work stop touching the unconsumed copy until Track C
actually starts, to avoid growing the duplication further? Tracked as WI-2253.
Five components landed via the 2026-07-02 Flight Finder sync and 2026-07-06 Figma gap sync but were never added to this plan's tracking. All were built in the then-legacy location — the same Track C migration gap as the original Track A set — and migrated with the rest of the library 2026-07-15 (WI-2254/D61); the duplicate-namespace finding above (WI-2253) was resolved by the same cutover.
| Component | Decision | Razor renderer | ViewModel (legacy, live) |
|---|---|---|---|
| Search Secondary Button | D42 | SearchSecondaryButton.razor | SearchSecondaryButtonViewModel |
| Promo Banner | D43 | PromoBanner.razor | PromoBannerViewModel |
| Expander | D44 | Expander.razor | ExpanderViewModel |
| Button Pill | D46 | composed within TabGroup.razor, no standalone renderer needed | ButtonPillViewModel |
| Toggle Switch | D47 | ToggleSwitch.razor | ToggleSwitchViewModel |
Also found, not previously flagged: GridContainerViewModel (D25) and
StackContainerViewModel (D30) exist only in the legacy
ALPAMobile/Components/ViewModels/ComponentViewModels.cs — unlike the rest of the original
Track A set, they were never given a matching entry in ALPAMobile.Presentation/ViewModels/ at
all (not even an unconsumed copy). GridContainer.razor and Stack.razor renderers
exist in the legacy location as expected. Folded into WI-2254 above.
| Control | Figma node | Uses | Domain type | Pages | Status |
|---|---|---|---|---|---|
FlightCardView | 5452:679 | 10+ | Flight | JSFF Results | ✅ commit c8f8ef50 |
DutyPeriodCardView | 4617:13876 | 6 | DutyPeriodListViewItemViewModel | FTDTFlightDutyPeriodListPage | ✅ commit 2d524033 |
FlightLegInfoView | 4325:6399 | 6+ | per-leg computed props from Flight | JumpseatFlightFinderDetailsPage (3 legs) | ✅ commit e2260f11 — DQ-10 open |
| Control | Figma label | Uses | Status | Notes |
|---|---|---|---|---|
FormFieldListRowView | "Master Form Field" (list-row variant) | 85 | ✅ commit 08b72427 | Replaces inline DataTemplate in KCMAirportsPage, JumpseatAirlinePolicyPage. KCM Airlines check-grid stays page-owned (distinct table layout). |
NotificationCardView | "card-notification" | 7 | ✅ commit 60149120 | Replaces inline BindableLayout DataTemplate in AdvocacyPage. PushNotificationsListPage keeps existing UnreadContentView. |
FlightStatusNotificationCardView | "notification card flight status" | 3 | ✅ commit 60149120 (scaffold) | No native host page yet — Notifications Flight tab migration pending. |
| Control | Why not a ContentView |
|---|---|
| SettingsToggle | MAUI Style on a Switch + Label row — no custom ContentView needed |
| ProgressBar / Stepper | Custom control (FTDT + JSFF flow nav) — closer to a TemplatedView |
| Segmented / TabGroup | Platform control or SegmentedControl from CommunityToolkit.Maui |
| Form check / radio | Standard CheckBox / RadioButton rows with a Style |
AlpaTheme.xaml, ComponentViewModel hierarchy, ComponentStyle value object + CardVariant enum, architecture test. ✅ Complete (commit ac1f7f98)CardText.razor, ButtonCard.razor, List.razor), CSS component classes, simulator validation. ✅ Complete (commits ac1f7f98, b3fad8f2)ALPAMobile.Presentation. ✅ Delivered 2026-07-15 (WI-2254/D61) — all 25 renderers migrated, project converted to Microsoft.NET.Sdk.Razor, Presentation architecture tests green (MAUI-free / Infrastructure-free).| Component | File | Verdict |
|---|---|---|
| Card.razor | Welcome/Pilot Card | Minor gaps |
| CardHero.razor | card-lg | Minor gaps |
| CardSmall.razor | card-sm | Aligned (renderer) — VM gap |
| Button.razor | Button | Minor gaps |
| ButtonGroup.razor | 3-up tile | Aligned |
| Carousel.razor | Horizontal Feed | Significant gaps |
All ViewModel structural gaps fixed in Phase A. Key renderer gap: CardHero.razor AccentStyle still inlines raw CSS string — deferred to ComponentStyle wiring in Phase D.
| WI | Action |
|---|---|
| WI-2096 | Close/resolve — architecture gate answered by D20 (Blazor Hybrid Razor RCL). Update with D20 as the resolution. |
| WI-2087 | Phase 5 — Presentation Extraction. Resolved 2026-07-01 — was the gate for Phase D above; Phase D itself is now unblocked but not yet started/scoped. |
| WI-1821 | Epic — update description to reference D20 as the architecture anchor. |
docs/ui-refresh-1821/figma-component-specs.json