anthill-client-web-ui-parity
Use when AntHill UI looks wrong only in the WPF client (oversized Lucide icons, unstyled panels), a feature is blank only there (bell: 'Nothing yet.'), or an added PageChrome service breaks bUnit page
SKILL.mdskills/managed/anthill-client-web-ui-parity/SKILL.md
GitHubAntHill: when the client looks or behaves differently from the dashboard
src/AntHill.App is one component library and one app.css, rendered by both the web
dashboard and the WPF client (host wiring: anthill-shared-css-verify). Two failure classes
look host-specific and are not. Diagnose before blaming caching.
1. Icons render huge (~24px): the selector never matched
Blazicons renders an attribute, not a class:
<svg blazicon="" width="24" height="24" viewBox="0 0 24 24" ...>
So every rule written as svg.blazicon { ... } is dead on both hosts. With no CSS
width, the SVG falls back to its intrinsic width="24".
The dashboard can still look fine: a flex parent shrinks the 24px SVG to roughly the heading's font-size, which is why this reads as "client-only". It is not.
Fix is one mechanical swap across app.css:
svg.blazicon → svg[blazicon]
Then the intended sizes apply by construction (.facet h2 svg[blazicon] { font-size: 13px }).
Project rule: the client's side-panel icons must match the web's size, and the web is the
reference, so fix the selector rather than inventing a new px value.
Confirm what Blazicon actually emits before swapping:
document.querySelector('.facet h2 svg').outerHTML.slice(0, 120)
2. Prove the CSS in the dashboard, not the client
The dashboard serves the identical stylesheet. Verify there with anthill-shared-css-verify:
the --no-build empty-sheet trap, headless measurement, and markup injection for states a
signed-out dev dashboard never renders (the bell).
3. A feature that is blank only in the client is usually missing data plumbing
SyncSnapshot carries projects, assets, files, revisions, dependencies, comments, claims:
one answer shared by every machine, cached as bytes. Anything addressed to one person
was never in it. The notification bell read the client's local Notifications table, which
nothing ever wrote to, so it showed "Nothing yet." to every artist since it shipped.
Pattern for per-artist data:
- Its own endpoint, not the snapshot (
GET /api/sync/notifications?for=<name>), because folding per-person data into the shared snapshot breaks its cache or leaks it. - Identity is the Windows user the client already runs as (
ICurrentUser→DesktopCurrentUser), sent by name and canonicalised server-side withIdentityName.Canonical, the same trust modelEditPush.MadeByalready uses. - Never send the server's row ids as if they mean something locally. The two catalogues number independently; carry the asset path and resolve it against the local catalogue.
- Write-back state (read/seen) belongs to the library, or the next sync resurrects it.
- Refresh on the
CatalogueWatchertick; a once-at-startup read means an artist who leaves the window open all day never sees anything arrive.
4. Adding a service the page chrome injects breaks every page test
PageChrome renders the bell, so every bUnit harness that renders a page needs the new
service registered exactly like ICurrentUser. Symptoms: Cannot provide a value for property 'Feed', and in one harness a stack overflow inside Bunit.Htmlizer that
aborts the whole run mid-suite ("Passed!" with a short count + Test Run Aborted).
Register a quiet INotificationFeed double in every harness that renders a page; find them
with grep -rn INotificationFeed tests/. Web.Tests harnesses use
AntHill.TestSupport.QuietNotificationFeed. Client.Tests uses a local double (QuietBell in
SourceControlPageTests): that project does not reference TestSupport and should not pull
Testcontainers in. Verified 2026-09-24: WorkflowPageTests, PageChromeTests,
GalleryComponentTests, ReleaseNotesTests, CardActionsTests, GalleryBulkSyncTests
(Web.Tests) and SourceControlPageTests (Client.Tests).
Bisect an aborted run by filtering per test class; the summary line lies about totals.