Standard controls: the audit and the target shape

"activities display not very pretty. are we using standard controls library? check all user interfaces and all apps if they use standard user interface components" — maintainer, 2026-09-16

The Governance activity page rendered its whole record — state, inputs, a gates table, the signatures, the log — as one markdown string built with a StringBuilder, wrapped the state in a hand-styled <span>-like pill with literal hex colours, and used ✅ ❌ ⏳ as the gate indicator. That is the shape the maintainer called not very pretty, and the audit below found it is the majority shape in this repo, not the exception.

What the audit measured

Count
Source/*.cs files that build UiControls (layout areas) 116
…that hit at least one of the four anti-patterns below ~101
Files using Skins.Card 0
Files using Controls.Badge 4
Files using Controls.DataGrid / ToDataGrid 15
Files using Controls.Html(...) 25
Files with a literal #rrggbb colour in a style 44

The four anti-patterns, each measured against the platform's own guidance (Badges, Icons & Status, DataGrid, Layout Areas, the /layout-area skill):

The target shape

Every page is composed from the platform's controls and nothing else:

Governance/Activity and Governance/Standard are the first two pages in this shape (#1981). Read them as the reference.

PR Module What moved
#1981 Governance Activity + Standard: badges, cards, grids, toolbar, texts per language
#1983 ten gallery pages the shared hand-rolled card line → Skins.Card; a Card example in DefaultViews
#1985 Essentials OperationRequest: state badge, facts, cards, toolbar, plain progress line
#1986 Hosting Issue, DeploymentStatus, TriageStatus (counts as an int-typed grid), TriageItem (facts, links, code-view logs)
#1987 Signature signers grid, card thumbnail, facts for the credential summaries, token colours, desk cards
#1988 Approvals token colours, toolbars, badge for the desk error
#1989 Providers the setup page as a card, an Invalid badge
#1990 MyAi source rows as cards
#1991 Training the tour's prose panel as a card
#1992 Feedback cards, category badge, status badges
core #6920 + this repo (2026-10-17) Settings app core's SettingsLayoutArea and the person-app tabs (Profile, Account, Preferences, Notifications, API tokens, Security, Connected instances, Sharing, Storage): every Controls.Html and Controls.Title (which emits an HtmlControl) replaced — banners and errors as Controls.MessageBar, sections and the API-token / instance forms on Skins.Card with an H4 heading (SettingsLayoutArea.Section), Metadata facts as a LayoutGrid of Subject/Body with a Controls.Link for the node type, the icon preview as an IconControl bound to the node, Check access as a DataGrid of rows; the menu search box lost its settings-menu-search class and this repo's stylesheet dropped that rule and the .settings-section* card system. Pinned by core's SettingsUseStandardControlsTest (no HtmlControl, no retired class, in any settled frame of those tabs)
this change (2026-10-18) Store, RolePlay, Collaboration the easy Controls.Html sites, where a platform control already exists: the Plugin cover's explainer video as Controls.Video with its conventional poster (VideoEmbed.Control, PluginLayoutAreas.CoverMedia) and its poster as Controls.Image; the Character sheet's picture as an ImageControl; the Scenery and Story hero subtitles as Subject labels in the hero's colour, and the Story cards' description/mood lines as hint-coloured Body labels; the Review page's empty-list hints and bylines as labels, an unreadable satellite as a Warning MessageBar, the action outcome as a Success/Error/Info MessageBar, and "resolved" as a Badge. Pinned by in-mesh render tests that build each area on its own hub and walk the tree (PluginCoverMediaTests, CharacterRenderTests, SceneryRenderTests, StoryRenderTests, ReviewRenderTests). Also fixed on the way: the Story cover read a scenery's mood with a bare is JsonElement test, which read nothing once the content arrived typed

| this repo, src/ half (2026-10-18) | Graph.Views, AI, ContentCollections.Indexing.Graph, Markdown.Collaboration, Blazor.Views, Northwind | the compiled modules the audit above never counted. One shared helper per module routes every repeated shape — ViewNotices (Graph.Views: Prose/Code/CodeList, Banner, SectionHeading, Fact, IconFact, SuccessBadge), IndexText (Indexing.Graph: Prose, CodeBlock, Fact, Banner), CommentText (Collaboration). Delete / Copy / Move / Import / Pin / StopSync / Versions: hints as hint-coloured Body labels, the delete warnings and statuses as MessageBars, paths and queries as markdown code spans; the Space header: H1 title, Verified badge, location / website / email as icon + Link, section headings as H2 + Link; data sources: provider badge, Subject/Body facts, the installed-to note as a Warning bar; the content-index tab: Active / Inactive as a Success / Warning bar, section titles as H3, tool calls and chunk text as fenced markdown, errors as Error bars; the Document page: H1, Subject/Body facts, the source as a Link; the render-error fallback of the default view pack as an Error MessageBar; Northwind's growth label as a status-token Label. The unreachable message-bubble builders in ThreadMessageLayoutAreas (never called) were deleted rather than converted. Pinned by mesh-booting render tests that also drive the interactive path (StandardControlsRenderTest — the confirmed Delete click deletes, an unconfirmed one does not; AiStandardControlsRenderTest; IndexPagesUseStandardControlsTest; RenderingErrorIsAMessageBarTest; CommentViewsAreTemplatesTest; GrowthPercentageTest) | | this repo, src/ half, round 2 (2026-10-19) | AI, Graph.Views, ContentCollections.Indexing.Graph, Markdown.Collaboration | the four module Hint helpers (ViewNotices, ChatText — deleted —, IndexText, CommentText) replaced by core's Controls.Hint / Controls.HintColor; the agent Details badge row as three bound Badges in status tokens (was an Html string of inline-styled spans); the thread message's metadata line as bound hint Labels, the model / tool timing sentences as their tooltips (was an Html string); the content-index activity transcript as a status Subject in a status token plus a fenced markdown block of the last messages (was an Html string), its error slot as ShowNotice / NoticeView; the Delete outcome banners as ActionNotice. Left: the default view pack's escaped-text fallback (BlazorViewRegistry.FallbackHtml — the renderer's own last resort), the Space logo tile and header divider (need platform controls). Pinned by AiStandardControlsRenderTest, ChatSurfacesAreTemplatesTest, AgentAndAdminViewsAreTemplatesTest, ActivityPanelIsATemplateTest, IndexPagesUseStandardControlsTest | | this repo, round 2 (2026-10-19) | Store, Edu, Cornerstone, Collaboration | the pointer-bound HTML the first pass left: the Edu/Exercise card (CardTemplate — Image icon, Subject name, difficulty / time / module as Badges, a hint blurb, on Skins.Card, every value a pointer into CardView; BuildCardHtml retired for ProjectCard), the CourseInvite exercises card (difficulty and Started Badges, Subject title, hint description), the Edu catalog card's picture and explainer (a bound ImageControl and VideoControl with its conventional poster — VideoEmbed.Control now exists in Edu's copy too, and BOTH copies' <video> HTML path is deleted, having no caller), the Store catalog's category tiles (white Body labels on the accent, the reserved blank tier line kept as a non-breaking-space label under the same style) and every card's icon band (IconView — an ImageControl, which draws an inline SVG icon inert), Cornerstone/Article's header (H1, a hint meta label, the tags as Badges through BindMany, the thumbnail as an ImageControl; TitleHtml / TagBadges retired for Title / Tags), Cornerstone/Pricing's Submission heading (H1) and its catalog card (Skins.Card, Subject title, a status Badge in the status colour, hint facts, click to open), and the Review comment's anchored passage (a quoted hint label, QuoteLine). Every authored value is now TEXT handed to a control, so the hand-escaping those sites carried is gone with them. Pinned by the types' own tests, which walk the composed templates and the areas built on their hubs (ExerciseTemplateTests, ExercisesGridTemplateTests, CourseCatalogTemplateTests.Card_MediaAreControlsBoundToTheRow, StoreCatalogTests.IconView_IsAnImageControlInEveryForm + the tile test, ArticleTemplateTests.TheHeader_IsPlatformControls / TheOverviewArea_RendersControls, PricingTemplateTests.TheCardAndTitle_ArePlatformControls, ReviewRenderTests) | | this repo (2026-10-25) | Hosting | the Deployment record page and its Key Vault surface (DeploymentRecordPage, SecretDialog, DeploymentActions, VaultSecretSteward): the four Controls.Title headings (each an HtmlControl) are H1 / H5 labels; the page's max-width wrapper and every inline flex/gap style are gone (Stack gaps, Controls.Toolbar for button rows); the header facts are a LayoutGrid of Subject/Body; the stale reading, the sampler's warnings and "no sample yet" are Warning / Info MessageBars (the ⏳ and ⚠️ badges are words); ✓ / ✗ cells are yes / no; the extra-config keys a one-column grid; history entries are split by Controls.Divider. The Key Vault entry moved out of a COLLAPSED expander at the bottom into an always-open card under the actions (Manage Key Vault secrets…, global administrators only), which leads to the record's own KeyVault page — a page rather than a modal, because the platform inventory's delete confirmation opens in the dialog area and must return to it. That page is now the platform's SecretInventorySection over every declared object but the platform master key, plus a write-only entry for values-half-only keys and for a vault object the record does not declare YET; the entry's and the action dialog's phase is a typed FilingState shown as a MessageBar with the filed node as a link — the emoji-prefix parse (anti-pattern D's worked example) and the bordered status box are gone. Pinned by DeploymentRecordPageRenderTests (builds the page and the Key Vault page on the type's hub and walks the tree: no HtmlControl, no retired inline style, no emoji state, the Key Vault button present for an admin, never under a collapsed expander, absent otherwise) |

Three lessons from the first pass, so the next one does not repeat them: a skin and the stack it wraps both bind the control's style — padding goes on an inner stack, never on the skinned one; a count column is PropertyColumnControl<int> — the grid deserializes each cell as the column's type and a string column over numbers throws per row; and a page whose tests read the direct area ids of a card (the Signature desk) needs those ids on the card's direct children, not on a nested content stack.

Left from those passes on purpose, each for a reason a swap cannot answer: the Review suggestion body, the hunk body and the pointer-bound body / redline views (<del>/<ins> redline markup — no platform control expresses an inline redline yet); the Store catalog HERO band (HeroMarkup — a gradient band with decorative circles, a container query that hides its icon showcase on a narrow hero, and a pill CTA: no platform hero-band control exists) and with it the showcase's IconMarkup tiles; and the generated course landing in Edu/CourseCatalog/Source/CourseManifestSource.cs, which is the BODY of a generated Markdown node (a cover hero, pre-rendered rich text by design), not a layout area. The duplicate Edu/CourseInvite/Source/VideoEmbed.cs stays because consolidating onto Store/Core's copy needs Edu to compile against Store's source; both copies are now the same control-only shape.

Round 2 — the app modules (2026-10-19)

AppleMusic, ICloud, Google, RemoteControl, Chess, Expenses and RolePlay, one shared helper per module:

Pinned by AmViewsTests, IcViewsTests, GoViewsTests, ChessChipsTests, the follow-chip assertions in ScreenTemplateTests, CharacterRenderTests.Memory_StampIsEmphasisNotMarkup and the line assertions in the Expenses and Playroom suites — each walks the rendered control tree (LayoutTemplate.Descendants) and fails on an HtmlControl where a control belongs.

The remaining debt, per module

Letters are the anti-patterns above. A module is done when its line can be deleted.

Module Files Anti-patterns
AppleMaps Maps/Source/MapsAreas.cs, AmapsConnect.cs A, D
AppleMusic Music/Source/MusicAreas.cs, AmConnect.cs A, B, D
AppleWeather Weather/Source/WeatherAreas.cs, AwConnect.cs A, B, D
Chess GambitHunt/Source/GambitHuntLayoutAreas.cs, Game/Source/ChessGameLayoutAreas.cs, ChessBoardView.cs, ChessNativeBoard.cs B, C, D
Collaboration Review/Source/ReviewLayoutAreas.cs (the redline bodies; the pointer-bound body / redline views — need an inline-redline control) B, C
DataModelling DataModelExplorer/Source/DataModelExplorerLayoutAreas.cs, DataModelExplorerLogic.cs A
Edu CourseCatalog, LearningJourney, CourseInvite, Exercise, Quiz, Workbook, AnswerSheet layout areas A, B, C, D
Essentials Email/Source/EmailLayoutAreas.cs A, C, D
Google Account/Source/AccountAreas.cs, GoConnect.cs A, B, D
HomeAssistant Home/Source/HomeAreas.cs, HaConnect.cs A, B, D
Hosting 16 files — Admin, Backup, BackupStore, Build (2), ConfigAudit, Deployment, FleetConsole (3), InstanceAction (2), LogEntry, ModuleInventory, PlatformBuildInbox, RepoHealth A, C, D
ICloud Calendar/Source/CalendarAreas.cs, IcConnect.cs A, B, D
MyAi Panel/Source/AiSettingsAreas.cs — the ✅/❌ status messages D
Providers ProvidersApp/Source/ProviderSetupAreas.cs (status messages), ProviderSetup.cs (models as markdown rows) A, D
RemoteControl Console/Source/ConsoleAreas.cs, Screen/Source/ScreenLayoutAreas.cs A, B, D
RolePlay Character, Playroom (2), Scenery, Story layout areas A, B, C, D
Signature the transient action messages (RefreshMessage, the login status lines) D
Store Catalog (4), Plugin (3), Core (2), Coupon, Publishing, Installer (2), Maintenance (3) A, B, C, D

Left as HtmlControl in the src/ half, each for a reason a swap cannot answer: the bound HTML a view model pre-renders (AgentView's badge row, the thread message Meta line, the content-index activity transcript), which needs its projection rewritten into rows rather than markup; and the stand-alone portal pages outside the layout system.

Round 2 (2026-10-19, core MeshWeaver#6986 + this repo): the platform gained Controls.Divider, Controls.Embed (a Document frame of this portal, or an always-sandboxed App frame on another host, rules in core's EmbedSources) and ImageControl.MaxWidth/MaxHeight (a picture keeps its natural aspect). Their Blazor and React renderers live here (DividerView, EmbedView, frame.tsx). The Space header's logo is now SpaceLayoutAreas.BuildLogo (a typed ImageControl chosen by shape, initials as the fallback) and its rule a Controls.Divider, so the header carries no HtmlControl; Home Assistant's instance frame is HaConnect.Frame (an App embed).

Not counted: the canvas and SVG renderers (DoublePendulum, FractalStars, ThreeBody), where Controls.Html carries a drawing rather than a layout; the Tests areas, whose pass/fail table is read by CI; and the package covers' hero blocks, which are pre-rendered rich text by design.

The clean references to align against, from the same audit: DefaultViews/Gallery, Analysis/Gallery (the only KpiStrip), Approvals/Approval, Publish/Deck, Edu/Module, Edu/Lesson, Essentials/Mailbox, Hosting/LogEntry.

How to re-run the audit

for p in 'Skins.Card' 'Controls.Badge' 'Controls.DataGrid' 'Controls.Html(' 'StringBuilder' \
         'border-radius' '#[0-9a-fA-F]\{6\}'; do
  printf '%4s  %s\n' "$(grep -rl --include='*.cs' -e "$p" --exclude-dir=src --exclude-dir=.worktrees \
      --exclude-dir=clients --exclude-dir=app --exclude-dir=e2e . | grep '/Source/' | wc -l)" "$p"
done

The per-file classification was a read of every one of the 116 layout-area files; re-read a module before deleting its line, the counts above only say where to look.