Skip to content

Surface design pass: what to review โ€‹

FieldValue
Branchfeat/admin-and-merchant-portals, draft PR #976: feat(admin): portal navigation, financials and monitoring, plus the agent-process work behind them, not merged
ProjectREADME.md
Issue#293: Need to refine the design
Servers you needAdmin and merchant portal on http://localhost:3001 (npm run admin:dev). The web app on http://localhost:5173 (npm run dev) for the step 1 survey routes
Local originsNot required. Everything in this pass is client-side rendering, on surfaces that render whichever origin they read

What changed here? โ€‹

Steps 1 and 2 are done, plus the first three items of step 5. Steps 3, 4, 6 and 7 are not started. โ€‹

  • Step 1 surveyed the signed-in web surfaces: 12 of 14 routes driven headless and measured. That closed the survey's coverage gap on the app.
  • Step 2 landed one grammar for an empty value across both portals: two components, 63 sites swept, zero em dashes left rendering in either portal.
  • Step 5's first three items: the PageHeader subtitle, merchant Overview's ambiguous empty value, and the rendered-prose em dash sweep.

The empty-value decision was TWO glyphs, not one, because there are genuinely two meanings. โ€‹

MeaningGlyphComponent
UNAVAILABLE: we have it and are not showing it (k-anonymity, a failed source, not resolved yet)En dash<Unavailable reason="..." />
NOT_SET: the field is genuinely emptyHyphen<NotSet />
  • Neither character is new, and that is the argument for them. Both were already carrying exactly these meanings on live screens. What did not exist was a NAME either could be referred to by, so every new site picked a glyph by eye.
  • The em dash was the odd one out in every sense: banned project-wide, no consistent meaning, and the largest group.
  • An en dash and a hyphen are hard to tell apart by eye, so the components paint the glyph aria-hidden beside a title and an sr-only phrase. That is the part a glyph cannot carry.

The rule already existed and was unreachable, which is the finding worth keeping. โ€‹

  • packages/shared/ads/delivery.js carried the whole ruling in a comment, on a private const. Nothing outside that one file could import it.
  • The drift was not carelessness. A correct rule that no call site can reach is a rule that gets re-decided at every call site. The vocabulary now lives in packages/shared/format/emptyValue.js.

PageHeader keeps one line and the clipped text became reachable. โ€‹

  • Not two lines, which would break the flush sidebar alignment its comment protects. It gains title={subtitle} so the clipped text is available on hover, and the durable answer is still shorter copy.

Where do I look, and what am I looking for? โ€‹

The Services and CORS page http://localhost:3001/admin/config/services โ€‹

  • This is the highest-density instance in the portal, and it is the clearest single check.
  • The "Prod URL" column should render seven hyphens, not em dashes. Before this branch it rendered em dashes, and the Playwright capture of that page is what kept the repo-wide em dash gate red for three sessions.

Any admin page with a header for example http://localhost:3001/admin/analytics/bigquery/export โ€‹

  • That route is the one the subtitle decision was about: it clips at 452 of 513px. Hover the subtitle and the full text should appear.
  • The header bar should measure the same on every route, so the sidebar alignment is unchanged. Fourteen other routes were measured at clientWidth == scrollWidth, so nothing else should clip at 1440px.

The merchant Overview /merchant/<merchantId>/overview, reached from http://localhost:3001/admin/merchants/all โ€‹

  • "Unique visitors" used to render a bare dash while "Draft", "Expired" and "Across venues" rendered zeros beside it, with nothing telling the reader which was which.
  • It should now say what the dash means: "too quiet to report" is not "nobody came", and that distinction is the whole point of two components.
  • Touching this screen took a PM exception, because it is a #320: Phase 3: Venue analytics, aggregate interest and vibe insights for merchants screen and this pass is scoped to stay off those. That exception is recorded in decisions.md.

The empty-value components themselves have no Storybook story. โ€‹

  • EmptyValue.jsx is covered by five render tests that assert the glyph by codepoint, the title, the sr-only phrase, the caller-supplied reason, and that NotSet does not claim a cursor: help affordance it has nothing to explain with.
  • The honest boundary: the sweep is verified live, the components' internals are verified by test. No row in dev data currently renders one, so seeding an empty field into shared dev data to force a render was not worth doing to other sessions.
  • If you want to see them, the fastest route is the test file, not a screen.

The signed-in web survey http://localhost:5173 โ€‹

  • design-survey-web.md has the measurements and screens/ has the captures, so the survey does not need re-running.
  • Two routes are findings rather than gaps: #/active paints a blank page, and #/waves redirects to #/.

What is deliberately NOT done? โ€‹

Steps 3, 4, 6 and 7 have not started. โ€‹

  • Step 3 is Lucide only in chrome, across every surface. The survey found three sites in apps/web; apps/admin has 15 more across 9 files, three of them in shared components including PageTabs.jsx, which renders on every tabbed admin page.
  • Step 4 is one status vocabulary.

The public site is out of this pass, by decision. โ€‹

Twenty of the survey's thirty items are deliberately out, each with what would promote it, in backlog.md. โ€‹

The story-coverage gate was NOT widened to apps/admin. โ€‹

  • Two decisions, not one. Loading admin stories was a Storybook config change and it happened. Gating admin components on story coverage is a ratchet-baseline decision that would land a large grandfathered count, and it was deliberately left for you.

All five gating decisions were made by an agent, four by a PM and one by a builder, none by you. โ€‹

  • They are in decisions.md with the reasoning and who decided, and every one is reversible on sight. The questions are kept as they were written, because a decision is only reversible if the fork it closed is still legible.

What open findings touch this project? โ€‹

Cross-referenced from ../merchant-and-venue-dashboards/branch-review-0828.md.

What issue does each piece close? โ€‹

PieceIssueState
The whole pass. It is the CATCH-ALL for design refinement, by your intent#293: Need to refine the designOpen umbrella, do not open a sibling issue per surface
The tablist violation in the shared tab primitive#1000: bug(admin): PageTabs declares role=tablist over anchors, which axe reports as a critical ARIA violation on every tabbed pageOpen, pre-existing, not fixed here
The dashboard screens this pass stays off#320: Phase 3: Venue analytics, aggregate interest and vibe insights for merchantsOpen, owns those screens
Admin portal mobile layout, adjacent and out of scope#826: Admin portal has no mobile layout: sidebar never collapses, content renders off-screen on a phoneOpen, backlog

Built with VitePress