MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-2373

(motir-core) The authed top bar OVERFLOWS below md — the right cluster is 290–409px in a 375px viewport, and it covers the hamburger

Done
Description

Repo: motir-core. One PR. Surfaced while building MOTIR-1150 (the brand slot in TopNav), which deliberately scoped the sweep out and shipped a md: guard around its own addition instead. Logged as its own card rather than absorbed (notes.html #27 / the drive-by-fix rule).

⚠️ AMENDED 2026-08-08 by its design blocker, MOTIR-2374 (notes.html #242 — a design that lands after the card it feeds amends that card, rather than leaving it to build the thing the design replaced). The design's render found a band this card did not know about and settled the shape of the fix; the acceptance criteria below are the design's, not the original improvisation menu. Read design/shell/design-notes.md § The top bar's control budget and design/shell/top-bar.png before starting. Re-estimated 3 → 5 points as a result.

The defect

app/(authed)/_components/TopNav.tsx renders <nav className="flex h-14 items-center justify-between gap-2 px-4"> with a left cluster (min-w-0) and a right cluster that carries up to eight controls. The right cluster cannot shrink — its children are fixed-size icon buttons plus two LABELLED buttons ("Build in public", "Plan with AI") — so the left cluster is squeezed to zero width and its children spill out underneath the right cluster, which paints over them.

The visible consequence is that the mobile hamburger stops being clickable: elementFromPoint at its centre returns whatever right-cluster control happens to sit there.

Measured — by MOTIR-2374's render, on origin/main

Right-cluster width, by viewport and by which optional slots are live:

viewportprivate project + AIpublic project + AIself-hosted, no AI, private
375px350px — hamburger covered409px — the <nav> scrolls to 433px, hamburger covered208px — fits, with 0px to spare
700px656px — hamburger covered670px — scrolls to 702px383px

⚠️ The worst band is smmd (640–767px), not < md. At 640px every label switches on at once (hidden sm:inline) and the cluster jumps 350 → 656px inside a 640px viewport — while the hamburger is still mounted, because it is md:hidden and lives to 767px. A fix scoped to 375px leaves that band broken, and a hit-test asserted only at 375×812 passes while it is. That is why the criteria below name two viewports.

Two more measured facts the fix has to carry:

  • BuildingInPublicHeaderLink is the one control whose label was never breakpoint-gated — 117px at 375px against the 38px CTA it replaces. That asymmetry, not the control count, is what makes the public state the widest surface in the product.
  • The cluster already squeezes its own controls below their box: in the public state at 375px the theme toggle and the avatar measure 28px wide against a 36px height (the avatar's rounded-full renders as an ellipse). Three box sizes coexist — 28 / 36 / 38.

Why MOTIR-1150 did not fix it here

That card added a 24px brand mark + divider (57px) at the head of the left cluster. That pushed the hamburger from x=16 to x=73 — into the right cluster's span — and turned the pre-existing overflow into two red E2E specs (shell-flows "mobile drawer opens…", settings-area "the settings nav collapses into the mobile drawer at narrow width"). It shipped hidden md:flex on the brand slot so its own diff is byte-identical to main below md, and left the underlying overflow to this card.

Acceptance criteria

  • The four-slot budget below md. The right cluster carries exactly four controls below mdCommandPaletteTrigger, CreateIssueButton, NotificationBell, UserMenu — each one --height-control square, gap-2, 168px total at the default style. The right cluster is flex-none, so it can no longer take width from the left one.
  • The label breakpoint moves from sm to lg — labels AND their <kbd> hint chips together. Gating one without the other leaves an icon beside a bare ⌘K chip and overflows the square box. This is the change that closes the 640–767px band.
  • PlanWithAILauncher, ReportButton, ThemeToggle and the build-in-public slot are hidden md:inline-flex — they REPLACE their base display utility, since .hidden and .inline-flex have equal specificity and the one that wins is whichever Tailwind emits last.
  • SidebarDrawer gains the footer utility strip that receives them (design Panel D): flex h-14 shrink-0 items-center gap-2 border-t border-(--el-sidebar-border) px-3, holding the build-in-public slot (labelled, in a min-w-0 flex-1 wrapper) then ReportButton then ThemeToggle. No new component — each is the element that left the bar. The Plan-with-AI pill is NOT in the strip: it is dropped, because PlanWithAIFab already ships on every authed screen under the same gate.
  • The six controls that hard-code their own box take --height-control instead of h-9 / w-9 / p-(--spacing-icon-btn) — that is what makes the four slots one square, removes the 28/36/38 mixture, and lets the budget survive a data-style swap.
  • At 375×812 AND at 700×812, with every optional slot live (showPlanWithAi, buildingInPublic, buildInPublicProjectKey, a non-null initialUnreadCount), document.elementFromPoint at the centre of [aria-label="Open navigation"] returns that button or a descendant of it. Assert it at BOTH widths, do not eyeball it.
  • No control silently disappears: every displaced control is reachable at phone width, and the test names where — the orb for planning, the drawer strip for theme / report / build-in-public.
  • A Playwright case at 375px opens the drawer with the crowded state seeded — the state the current specs do not cover, which is why this shipped — and reaches the theme + report controls inside it.
  • TopNav's docstring records the budget rule verbatim from design/shell/design-notes.md § The rule for the ninth control: the below-md bar is closed at four slots, a new control is md-and-up by default, and anything it displaces must land in the drawer strip.
  • tests/components/top-nav-brand-slot.test.tsx pins MOTIR-1150's md: guard. The guard STAYS (the brand is still a md+ slot in the design), but its comment describing the overflow as unfixed is now stale — update it to cite the budget.

Context refs

  • app/(authed)/_components/TopNav.tsx — the bar; its left cluster is min-w-0, the right cluster is not.
  • design/shell/design-notes.md + design/shell/top-bar.mock.html / .png (MOTIR-2374) — the design of record. Panels C–E carry the target markup with the exact class strings.
  • components/ui/SidebarDrawer.tsx — gains the footer strip.
  • components/planning/PlanWithAIFab.tsx, app/(authed)/layout.tsx:279 — the second door that lets the pill be dropped.
  • tests/e2e/shell-flows.spec.ts · tests/e2e/settings-area.spec.ts — the two specs that exercise this width today, neither with the crowded state.
  • tests/components/top-nav-brand-slot.test.tsx — pins the md: guard MOTIR-1150 shipped.
Status
Done
Type
Bug