Audited: 2026-06-26
GridContainerViewModel : ContainerSurfaceViewModel : ComponentViewModel — D26gpO3masyyNNHxjvRtdcRo7 · 20785:1418 (My MEC screen — UAL Home) · 2026-06-23.
The grid is an ALPA-defined scaffold — there is no single published Figma component_set for it; it is composed from stacked
"row" frames each containing card-btn instances. Decision:
D25.
v1 covers the exact pattern shown in My MEC: a fixed 3-column grid of equal-width
card-btn (ButtonCardViewModel) items on phone. Items are ordered by their
position in the collection — the server delivers them pre-sorted. No flexible column count, no
mixed content types, no tablet layout in this version.
Defer strategy: the ViewModel type boundary (ObservableCollection<ButtonCardViewModel>)
is the intentional constraint. Widening to ObservableCollection<ComponentViewModel> is the
extension path when mixed types are needed — the DataTemplate selector already supports this.
See § 6 Deferred Extensions.
var(--tiny)var(--text/on-brand)DynamicResource Surface/Brand.ResolvedBackground — the view's code-behind (or a IValueConverter) calls IThemeResolver.Resolve(ViewModel.BackgroundToken) to produce the concrete Color. The ViewModel never holds a Color directly. When the MEC theme changes, the resolver re-reads the merged ResourceDictionary and the binding updates.
ResolvedCornerRadius — similarly, IThemeResolver.ResolveRadius(ViewModel.CornerRadiusToken) returns a double pixel value. Falls back to BorderRadius/M (8 px) when CornerRadiusToken is null. See design-tokens.html — BorderRadius/*.
IsScrollEnabled="False" — the grid sits inside the screen's root ScrollView. Nested scroll on the same axis causes gesture conflicts on iOS.
HeightRequest binding — each cell must declare a fixed height because GridItemsLayout does not auto-size rows from content. The default CellHeight=84 matches the MEC design.
Follow RawRepresentationFactory<T> pattern. The factory receives an ordered list of raw items and maps each to a ButtonCardViewModel. Collection order from the API response is the display order — no client-side sort needed.
| API field | ButtonCardViewModel property | Notes |
|---|---|---|
backgroundToken | BackgroundToken: string | Surface/* token (e.g. "Surface/Brand"); resolved via IThemeResolver at render time (D37) |
cornerRadiusToken | CornerRadiusToken: string? | BorderRadius/* token (e.g. "BorderRadius/M"); null → fallback 8 px (D38) |
isSortable | IsSortable: bool | false locks container position — user cannot reorder it on device (D36). Default true. |
icon | Icon: ImageSource | Resolved via ImageSource.FromUri or local asset key |
label | Label: string | Uppercase applied in XAML via TextTransform |
isFavorited | IsFavorite: bool | Drives heart icon state |
navigationTarget | NavigationTarget: string | Shell route or external URI |
| array index | collection position | No explicit sort field — API ordering is authoritative |
Do not implement these in v1. Each has a defined extension path that avoids breaking the ViewModel contract.
| Feature | Trigger | Extension path | Breaking? |
|---|---|---|---|
| Mixed content types | Design adds non-card-btn items to a grid section | Widen Items to ObservableCollection<ComponentViewModel>; add DataTemplate selector for each type |
ViewModel API change — factory updated, XAML DataTemplate expanded |
| Flexible column count | A non-3-column grid section appears in design | Make Columns a settable property on the factory-built VM; GridItemsLayout.Span binding unchanged |
No — additive |
| Responsive / tablet | Post-D12 / Epic #2087 tablet pass | Add MinCellWidth: double; computed EffectiveColumns from SizeChanged; bind Span to EffectiveColumns |
No — additive property |
| Server-driven sort order | API needs to reorder items without client changes | Add SortOrder: int to DTO; factory sorts before building collection; VM unchanged |
No — factory-only change |
| Wide items (ColumnSpan > 1) | A full-width button-sm appears inside a grid section | Introduce GridItemViewModel wrapper with ColumnSpan: int = 1 + Content: ComponentViewModel; replace Items collection type |
Yes — ViewModel + factory + XAML all change; isolate behind a feature flag or new VM type |
The MEC screen uses two independent GridContainerViewModel instances — one for MEC Actions
(PDR · DYK · QRG / DOCUMENTS · COMMITTEES · UPDATES) and one for Quick Links
(MEC Website · Global Entry · TSA PreCheck / Passport Renewal · ALPA PAC · United PAC).
Each is a separate VM bound to its own section feed slot. The section header ("MEC ACTIONS", "QUICK LINKS")
is rendered above the grid by the containing screen ViewModel, not by GridContainerViewModel.
| MEC instance | Items | Rows rendered | Figma nodes |
|---|---|---|---|
| MEC Actions | 6 | 2 | I…;20772:1699 · I…;20772:1739 |
| Quick Links | 6 | 2 | I…;20915:2965 · I…;20920:3086 |
Cell background color — #12233e in MEC is the MEC brand Surface/Brand token.
The ButtonCardView should resolve this via DynamicResource so the same view renders correctly on
both the standard ALPA Home (white background) and MEC (dark navy) surfaces without any property override on the grid.
GridItemsLayout does not auto-size rows; all cells in a row share CellHeight. Content taller than CellHeight clips.Items.Count % Columns ≠ 0, the last row has empty trailing cells. The current MEC instances always send 6 items (2 full rows); if the API ever sends fewer, trailing gaps appear. Pad with invisible placeholder ButtonCardViewModel items at the factory if needed.IsScrollEnabled="False" is required. The grid must not be the sole scrollable surface on a screen.GridItemsLayout divides width equally. Unequal column widths require a hand-written Grid — defer until a design case arises.