Surface design pass: what to review โ
| Field | Value |
|---|---|
| Branch | feat/admin-and-merchant-portals, draft PR #976: feat(admin): portal navigation, financials and monitoring, plus the agent-process work behind them, not merged |
| Project | README.md |
| Issue | #293: Need to refine the design |
| Servers you need | Admin 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 origins | Not 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
PageHeadersubtitle, 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. โ
| Meaning | Glyph | Component |
|---|---|---|
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 empty | Hyphen | <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-hiddenbeside atitleand ansr-onlyphrase. 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.jscarried the whole ruling in a comment, on a privateconst. 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 indecisions.md.
The empty-value components themselves have no Storybook story. โ
EmptyValue.jsxis covered by five render tests that assert the glyph by codepoint, thetitle, thesr-onlyphrase, the caller-supplied reason, and thatNotSetdoes not claim acursor: helpaffordance 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.mdhas the measurements andscreens/has the captures, so the survey does not need re-running.- Two routes are findings rather than gaps:
#/activepaints a blank page, and#/wavesredirects 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/adminhas 15 more across 9 files, three of them in shared components includingPageTabs.jsx, which renders on every tabbed admin page. - Step 4 is one status vocabulary.
The public site is out of this pass, by decision. โ
apps/sitehas its own plan row and its own issues,#848: feat(site): show a phone with a lit lantern beacon on the splash cover, flame becomes background and#593: Site: "Finding each other" needs a see-it-in-the-app screenshot strip (+ in-app flow to capture). It shares the fonts but not the token stylesheet, so it is a genuinely separate visual system today.
Twenty of the survey's thirty items are deliberately out, each with what would promote it, in backlog.md. โ
- The rule that splits them: this pass owns a defect that appears on more than one surface, lives in a primitive more than one surface consumes, or breaks a grammar the other surfaces follow. It does not touch a screen
#320: Phase 3: Venue analytics, aggregate interest and vibe insights for merchants is about to redesign.
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.mdwith 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.
Scaffold,EmptyValueandPageHeaderwere checked and found clean. They are in the review's "what was checked and found clean" list.- No open finding lands on this project's surfaces. It is the only surface project on this branch that is clear.
- One adjacent thing worth knowing:
PageTabsdeclaresrole="tablist"over anchors, which axe reports as a critical ARIA violation on every tabbed admin page. That is#1000: bug(admin): PageTabs declares role=tablist over anchors, which axe reports as a critical ARIA violation on every tabbed page, pre-existing, and it sits in a shared primitive this pass's step 3 will touch.
What issue does each piece close? โ
| Piece | Issue | State |
|---|---|---|
| The whole pass. It is the CATCH-ALL for design refinement, by your intent | #293: Need to refine the design | Open 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 page | Open, pre-existing, not fixed here |
| The dashboard screens this pass stays off | #320: Phase 3: Venue analytics, aggregate interest and vibe insights for merchants | Open, 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 phone | Open, backlog |