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.
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.
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.
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.
/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.DocumentOpenPage.razor (@page "/document-open",
fileId) — inline iframe/blob for pdf/htm/html via authenticated download
(POST /api/doc/downloadfile → DocumentDownload { fileName, fileBytes }); "webpage"/unknown
hand off to native SharedActionsService.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/.
| Parameter | Values / default | Behavior |
|---|---|---|
Mode | CategoryGrid · DocumentList (enum, D18) |
What data renders: the scope's categories vs a document set. |
Presentation | nullable 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 / Grouping | string 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. |
SortOrder | PublishDateDescending (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. |
MaxItems | int?, default 5 (per Figma template) in DocumentList | Row cap. "Latest Documents" = DocumentList + Scope-only keys + defaults; not a special mode. |
Tile items + ReferentType | curated 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). |
Figma 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.
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).
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.
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).
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.
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).
GET /api/doc/getdocuments returns a 3-level hierarchy List<ScopeItem> → Categories → Documents — not flat. The client flattens: ProcessDocumentListResponse stamps parent keys down onto each document.scope/category/grouping (camelCase, AlpaWireJson/JsonSerializerDefaults.Web); DTO Scope/Category/Grouping. There is no documentGrouping key on either branch.CompositeId = {Grouping}-{Scope}-{Category}(-{FileID}); dedup by CompositeId (BUG-1995/BUG-2208 — the API legitimately repeats top-level scopes).IDocumentsQueries — GetDocumentListAsync, GetDocumentsForScopeAsync (scope-only feeds), GetDocumentCategoriesForScopeAsync, GetDocumentsForScopeCategoryAsync, …GroupingAsync, GetDocumentByFileId, SaveDocument. Downloads via POST /api/doc/downloadfile body {"Source": Source, "Path": FileID}.MockDocumentsApiService with a MEC scope, real grouping values, and multi-category data so the hub is exercisable logged-out (real values from live config/data, not invented placeholders).feature/aa/2368-…)The in-flight branch's documents pieces are the right shapes in the wrong plumbing — dispositions, so nothing is silently invalidated:
DocCard.razor → promoted & renamed CardIcon (+ CardIconViewModel, ComponentView dispatch, factory-fed) — becomes the IconCards/Tiles building block.LatestDocumentRow.razor → promoted & renamed DocumentRow — gains the red left-edge unread dot and real favorites wiring (raw id = FileID via FavoriteToggleContext; the local HashSet-by-label favorites must be replaced).MecDocumentsPage.razor → superseded by the hub composition on /documents?scope=MEC: hardcoded section titles/category keys/icons and fabricated Latest-Documents rows are replaced by data-driven groupings + page-markup curation + real IDocumentsQueries data.DocumentCenterPage until AB#2191; follow-up tracked in AB#2400 (the route string DocumentCenterPage?Scope= is burned into fielded server payloads/push notifications — redirect, never remove). Native documents pages are FROZEN as of AB#2368 — fixes land Blazor-side only. Blazor route contract pinned: /documents?scope[&category][&grouping] (hub = no-category behavior for MEC/LEC) + /document-open?fileId.CardIcon joins the card family; (3) DocumentRow domain row (D7 untouched on shared List).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).