← Back to Component Index

Documents

Updated: 2026-07-31 · Audited: 2026-07-31 (adversarial ambiguity pass: 20 findings dispositioned) · 4 screens · Design template Figma file owEYzHf7FrHRvWC2u82UOl · Component-spec Figma file psH738AqHDxuMyFm897f9r · Task AB#2368

Document browsing for the ALPA / MEC / LEC scopes: a hub screen composing category icon-card grids, curated quick-link tiles, and inline document lists, plus a per-category document list — all backed by GET /api/doc/getdocuments and rendered by one multi-mode RCL control (DocumentsBrowser). Replaces native DocumentCenterPage.xaml / DocumentsListPage.xaml for MEC/LEC; ALPA keeps its shipped Internal Comms rows design.

First requirements artifact for this area — page-compositions.html has no #documents anchor and there is no documents-screen-data-gap.html; closest prior art is page-compositions.html#internal-comms. Note: the hub design template lives in a different Figma file (owEYzHf7… "ALPA mobile app") than the component-spec file (psH738Aq…) that the docs sync pipeline tracks.
Documents hub
Interior sub-page
Documents hub (Document Center replacement — MEC/LEC)
Node 20916:16952 · owEYzHf7… ("MEC Documents - Template")
The no-category behavior of /documents?scope=MEC|LEC becomes this hub (route pinned — keeps the AB#2400 deep-link mapping trivial). Composes configured DocumentsBrowser instances: top row = CategoryGrid/IconCards (Scope only, ungrouped categories); one CategoryGrid section per distinct Grouping value in the scope's data (data-driven, section title = grouping value, client-side filter on the stamped key — same as native); curated sections (Quick Links tiles, Latest Documents) declared in page markup (page owns curation; component stays generic). "View All" → list view with the instance's keys. Title from the scope→banner map ("MEC Documents"/"LEC Documents"; Grouping overrides when set). Hearts on all cards/rows (top-right); no unread-count badges on grid cards. Visiting the hub never marks documents read.
Documents list rows
Interior sub-page
Documents List — per-category rows
Node 4867:34739 · psH738Aq… (ALP Mag row pattern)
EXISTS: DocumentsListPage.razor (@page "/documents", query scope/category/grouping) — extend, don't rebuild: gains scope-only document-list support (via existing GetDocumentsForScopeAsync) and sort passthrough so Latest-Documents "View All" shows the same feed it previews. Rows = DocumentRow: red unread dot at the LEFT edge (resolves DQ-27 for documents), heart top-right (document favorite, raw id = FileID), timestamp + headline. Tap = mark read + SaveDocument + DocumentHeroFactory.OpenRoute(FileID) — never raw DocumentItem.Path hrefs (P1). This page (only) keeps dispose-marks-all-read. EmptyState on empty results.
Category rows
Interior sub-page
Category rows — ALPA scope (unchanged design)
Node 4565:9260 · psH738Aq… (Internal Comms)
ALPA keeps this shipped design: /documents?scope=ALPA renders the scope's categories as CardSmall rows via CategoryCardFactory (favoritable — FavoriteItemTypes.DocumentCategory, raw key {scope}|{categoryKey}); title "ALPA International Comms" per the scope→banner map. Equals the component's CategoryGrid/Rows presentation. If design later ships an ALPA hub template, the hub is one page-markup composition away.
Interior sub-page
Document Open — viewer
No Figma node (design gap per design-source-gap-inventory)
EXISTS: DocumentOpenPage.razor (@page "/document-open", fileId) — inline iframe/blob for pdf/htm/html via authenticated download (POST /api/doc/downloadfileDocumentDownload { fileName, fileBytes }); "webpage"/unknown hand off to native SharedActionsService.
Out of scope for AB#2368 beyond keeping OpenRoute the single entry point.

Component contract — DocumentsBrowser (RCL)

DECIDED 2026-07-31 (José): one multi-mode Library component grouping the documents area of concern — a deliberate NEW design-system pattern that will carry to future controls (record in naming-decisions: multi-mode "browser-class" controls; leaf cards/rows stay presentational). Lives in ALPAMobile.Presentation/Components/Library/; VMs in ALPAMobile.Presentation/ViewModels/.

ParameterValues / defaultBehavior
ModeCategoryGrid · DocumentList (enum, D18) What data renders: the scope's categories vs a document set.
Presentationnullable enum; null = mode default Full matrix — CategoryGrid: IconCards (default; CardIcon tiles in GridContainer, 3-per-row wrapping to flex past 3) · Rows (CardSmall rows, Internal Comms pattern). DocumentList: DatedRows (default; DocumentRow) · Tiles (doc-icon card-sm, no date — the "Quick Links" look). Any other Mode×Presentation combination is invalid → throw ArgumentException at parameter validation (fail loud in dev, not silent fallback).
Scope / Category / Groupingstring setters Filter keys; resolution via injected IDocumentsQueries, precedence Scope → Category → Grouping, exact match on the stamped keys. Grouping filtering is client-side on CategoryItem.Grouping / DocumentItem.Grouping (same as native GetCategoryItemsAsync) — no new query methods. Scope-only document feeds use the existing GetDocumentsForScopeAsync.
SortOrderPublishDateDescending (default) · None NEW mechanism (nothing sorts today). Compare on ParsedPublishDate — harden the Domain getter to TryParse (sanctioned: current DateTimeOffset.Parse throws on malformed PublishDate); unparseable dates sort last.
MaxItemsint?, default 5 (per Figma template) in DocumentList Row cap. "Latest Documents" = DocumentList + Scope-only keys + defaults; not a special mode.
Tile items + ReferentTypecurated item list; ReferentType enum: Document · Category · Grouping (explicit — never inferred from key shape) A Tiles instance renders a curated list of tile items declared in page markup, each with its own keys + explicit referent. Tap: Document → direct open (DocumentHeroFactory.OpenRoute); Category → its list (CategoryCardFactory.ListRoute); Grouping → the grouping's hub section / grouping-filtered list. A Document-referent tile whose keys resolve to 0 or 2+ documents degrades: hide the tile, report a Sentry breadcrumb (curation lives client-side; server renames must not crash the hub).

Grouping section (CategoryGrid/IconCards) and Latest Documents (DocumentList/DatedRows) from the Figma templateFigma template detail — a grouping section (CategoryGrid/IconCards) above the Latest Documents secondary view (DocumentList/DatedRows). The dark band is the template's fixed bottom-nav baked into the canvas render.

Data access (DECIDED 2026-07-31)

DocumentsBrowser injects IDocumentsQueries. DI registration changes so IDocumentsQueries resolves to the auth-aware DocumentsQueriesRouter (live when authenticated, mock otherwise) — one-line change in HeadServiceCollectionExtensions; preserves mock parity for logged-out/PreviewHost surfaces. Browser-class controls may inject their domain query interface; leaf cards/rows remain purely presentational (record in naming-decisions).

Text & icon mappings (DECIDED 2026-07-31)

Data-driven, same concept as the home documents carousel (MockHomePreviewFeedService, PreviewHost home-mock.json): titles from CategoryItem.DisplayTitle/DocumentItem fields via pure factory (extend CategoryCardFactory: CategoryItem → CardIconViewModel). Icon resolution is a configurable chain: API CategoryItem.IconUrl (behind a toggle, default OFF — unvetted/uncurated today) → local DocumentIconMask map (new; keyed on pulled DisplayTitle, sibling of MenuItemIconMask, falls back to it) → generic glyph. Flipping the toggle lights up server-driven icons with no component change. Factory must stamp FavoriteItemTypeId = DocumentCategory and Id = RawId(scope, categoryKey) on grid-card VMs or hearts silently write MenuItem favorites; hub page must cascade FavoriteToggleContext for hearts to render.

Read state & favorites (DECIDED 2026-07-31 — resolves DQ-27 for documents)

Unread renders as a red dot at the LEFT edge of DatedRows; heart top-right (standard quadrant) on all cards/rows. Grid cards: hearts only — native per-category unread-count machinery is NOT ported. The component is read-state display-only + mark-on-open; only the category list page keeps dispose-marks-all-read. Favorites required, riding AB#2182 plumbing: categories = FavoriteItemTypes.DocumentCategory, raw key {scope}|{categoryKey}; documents by FileID (the key DocumentHeroFactory/favorites already use).

Refresh (DECIDED 2026-07-31)

No pull-to-refresh in v1 — reload on navigation/parameter change plus re-render on DocumentsReadyMessage (background fetch pipeline unchanged). RefreshCommand analytics event retired. Escape hatch: a hidden setting on the Settings page force-triggers GetDocumentListAsync(true) (diagnostic-settings pattern). If product later wants explicit refresh it lands as a platform affordance shared across Blazor pages, not a DocumentsBrowser feature.

Analytics

New plumbing, stated targets (nothing fires on the existing Blazor list today): hub page fires scope entry ONCE per visit (keep event name Event_DocumentCenterPage for continuity of dashboards); category tap CategoryItem.{Title} and document tap DocumentItem.{CompositeId} fire from component tap callbacks (taps are callbacks, not bare hrefs). Auth guards preserved: empty-not-throw on logged-out reads (AB#2043). EmptyState for empty scope/category/section (native blank pages — do not replicate). Add AutomationIds to hub, grid cards, rows, tiles (native pages have none).

Data contract (verified against DotNet10 + imp/blazor-hybrid source)

WIP branch migration (PR 1932 — feature/aa/2368-…)

The in-flight branch's documents pieces are the right shapes in the wrong plumbing — dispositions, so nothing is silently invalidated:

Deferred & follow-ups

Open questions: none as of 2026-07-31 — all resolved with José, including the 20-finding adversarial ambiguity pass (8 blockers, 8 decisions, 4 minors — dispositions recorded inline above and on AB#2368).