Navbar: single create (+) button left of the avatar with New repo / Import / New org dropdown; drop redundant CTAs #466

Closed
opened 2026-09-13 15:28:46 +00:00 by crueber · 4 comments
Owner

Navbar create-button: one "+" entry point for repository/org creation

What's requested

Add a create ("+" / plus) button to the site navbar, positioned immediately left of the identity menu (avatar/initials), opening a dropdown with exactly three items:

  • New repository → /new
  • Import repository → /import
  • New organization → /orgs/new

With that entry point in place, remove the now-redundant buttons:

  • the navbar import link (App.jsx site nav),
  • the New repository button on the explore page toolbar (Repos.jsx Repositories toolbar),
  • the New repository / Import repository buttons on the profile (Owners.jsx) header — note Owners.jsx also currently carries the New organization button; that one moves into the dropdown too.

The provided plus icon SVG (referenced in the parent task; implementer may inline it) is the button glyph.

Evidence (current tree)

  • web/src/App.jsx — site nav renders <A href="/import">import</A> as a primary nav item; the right cluster ends with NotificationTray → theme toggle → IdentityMenu (signed-in only, gated nav().showIdentity).
  • web/src/pages/Repos.jsx (~line 552) — Repositories toolbar right-anchors a btn primary "New repository" CTA, gated on canWrite().
  • web/src/pages/Owners.jsx (~line 148) — Owners page header carries "New repository" (canWrite-gated), "New organization" (#348 gate), and "Import repository" buttons.
  • The dropdown/popover machinery to copy already exists: web/src/components/IdentityMenu.jsx implements the full popover contract (document-level outside-click close removed in onCleanup, Escape + focus return, arrow-key item walk, Tab-out dismiss, role menu/menuitem, no toggle-fight because the trigger lives inside the root). Reuse that pattern — either extract a shared menu primitive or clone the component, implementer's call.

Architecture notes

  • Gate the whole button on the same condition as the identity menu (nav().showIdentity, i.e. signed-in): an anonymous visitor has nothing to create and the three targets all assume auth. Keep the signed-out navbar unchanged.
  • navModel lives in web/src/lib/identity.js — extend the model (e.g. a showCreate flag + hrefs) there rather than branching inline in App.jsx, matching the existing convention.
  • Dropdown item permissions differ per target today (Repos.jsx gates New repository on canWrite(); Owners.jsx gates New org on logged-in writer). The navbar dropdown should show all three for any signed-in user; the target pages already enforce permission server-side and render their own errors — matching the destination page's own gate client-side in the menu is acceptable if trivial, implementer's call, but do not invent a new permission probe (law 6: no new requests).
  • The button joins the shrink-0 right cluster (ml-auto flex items-center gap-2); check the 390px header budget (#273): the cluster currently fits because the avatar dropped its btn box — the new trigger should be a compact icon button (plus glyph only, no text label, with aria-label="Create new" and a title) so it adds ~32px at most.
  • Removing /import from the primary nav does NOT remove the import page or route — /import stays reachable from the dropdown.
  • Fixed-width right-anchored dropdown panels must carry max-width: calc(100vw - 1rem) per the mobile-popover rule.

Acceptance criteria

  • Signed-in navbar shows a compact plus-icon button immediately left of the identity menu (avatar/initials), using the provided SVG glyph, aria-label + tooltip set.
  • Clicking it opens a dropdown with New repository (/new), Import repository (/import), New organization (/orgs/new); selecting an item navigates and closes the menu.
  • Popover contract matches IdentityMenu: outside-click close, Escape + focus return, arrow-key walk, Tab-out dismiss; no toggle-fight on trigger clicks; dropdown bounded on phone widths.
  • Button hidden for signed-out visitors / none mode; navbar otherwise unchanged.
  • import link removed from the primary site nav; /import page and route untouched.
  • Explore page (Repos.jsx) Repositories toolbar no longer renders the New repository CTA (heading remains; listing behavior unchanged).
  • Owners.jsx header buttons (New repository / Import repository / New organization) removed; page otherwise unchanged.
  • Header still fits 390px viewport with no page-level horizontal scroll (per the #273 sweep pattern).
  • No new network requests introduced.
# Navbar create-button: one "+" entry point for repository/org creation ## What's requested Add a create ("+" / plus) button to the site navbar, positioned immediately **left of the identity menu (avatar/initials)**, opening a dropdown with exactly three items: - **New repository** → `/new` - **Import repository** → `/import` - **New organization** → `/orgs/new` With that entry point in place, remove the now-redundant buttons: - the navbar `import` link (App.jsx site nav), - the `New repository` button on the explore page toolbar (Repos.jsx Repositories toolbar), - the `New repository` / `Import repository` buttons on the profile (Owners.jsx) header — note Owners.jsx also currently carries the `New organization` button; that one moves into the dropdown too. The provided plus icon SVG (referenced in the parent task; implementer may inline it) is the button glyph. ## Evidence (current tree) - `web/src/App.jsx` — site nav renders `<A href="/import">import</A>` as a primary nav item; the right cluster ends with `NotificationTray` → theme toggle → `IdentityMenu` (signed-in only, gated `nav().showIdentity`). - `web/src/pages/Repos.jsx` (~line 552) — Repositories toolbar right-anchors a `btn primary` "New repository" CTA, gated on `canWrite()`. - `web/src/pages/Owners.jsx` (~line 148) — Owners page header carries "New repository" (canWrite-gated), "New organization" (#348 gate), and "Import repository" buttons. - The dropdown/popover machinery to copy already exists: `web/src/components/IdentityMenu.jsx` implements the full popover contract (document-level outside-click close removed in `onCleanup`, Escape + focus return, arrow-key item walk, Tab-out dismiss, role menu/menuitem, no toggle-fight because the trigger lives inside the root). Reuse that pattern — either extract a shared menu primitive or clone the component, implementer's call. ## Architecture notes - **Gate the whole button on the same condition as the identity menu** (`nav().showIdentity`, i.e. signed-in): an anonymous visitor has nothing to create and the three targets all assume auth. Keep the signed-out navbar unchanged. - `navModel` lives in `web/src/lib/identity.js` — extend the model (e.g. a `showCreate` flag + hrefs) there rather than branching inline in App.jsx, matching the existing convention. - Dropdown item permissions differ per target today (Repos.jsx gates New repository on `canWrite()`; Owners.jsx gates New org on logged-in writer). The navbar dropdown should show all three for any signed-in user; the target pages already enforce permission server-side and render their own errors — matching the destination page's own gate client-side in the menu is acceptable if trivial, implementer's call, but do not invent a new permission probe (law 6: no new requests). - The button joins the shrink-0 right cluster (`ml-auto flex items-center gap-2`); check the 390px header budget (#273): the cluster currently fits because the avatar dropped its btn box — the new trigger should be a compact icon button (plus glyph only, no text label, with `aria-label="Create new"` and a `title`) so it adds ~32px at most. - Removing `/import` from the primary nav does NOT remove the import page or route — `/import` stays reachable from the dropdown. - Fixed-width right-anchored dropdown panels must carry `max-width: calc(100vw - 1rem)` per the mobile-popover rule. ## Acceptance criteria - [ ] Signed-in navbar shows a compact plus-icon button immediately left of the identity menu (avatar/initials), using the provided SVG glyph, `aria-label` + tooltip set. - [ ] Clicking it opens a dropdown with New repository (`/new`), Import repository (`/import`), New organization (`/orgs/new`); selecting an item navigates and closes the menu. - [ ] Popover contract matches IdentityMenu: outside-click close, Escape + focus return, arrow-key walk, Tab-out dismiss; no toggle-fight on trigger clicks; dropdown bounded on phone widths. - [ ] Button hidden for signed-out visitors / none mode; navbar otherwise unchanged. - [ ] `import` link removed from the primary site nav; `/import` page and route untouched. - [ ] Explore page (Repos.jsx) Repositories toolbar no longer renders the New repository CTA (heading remains; listing behavior unchanged). - [ ] Owners.jsx header buttons (New repository / Import repository / New organization) removed; page otherwise unchanged. - [ ] Header still fits 390px viewport with no page-level horizontal scroll (per the #273 sweep pattern). - [ ] No new network requests introduced.
crueber added this to the v1 milestone 2026-09-13 15:29:01 +00:00
Author
Owner

Icon source (verbatim from the user) — save as the + trigger icon, 1em/currentColor per #465's mechanism:

<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
	<path fill="currentColor" fill-rule="evenodd" d="M7.345 4.017a42.3 42.3 0 0 1 9.31 0c1.713.192 3.095 1.541 3.296 3.26a40.7 40.7 0 0 1 0 9.446c-.201 1.719-1.583 3.068-3.296 3.26a42.3 42.3 0 0 1-9.31 0c-1.713-.192-3.095-1.541-3.296-3.26a40.7 40.7 0 0 1 0-9.445a3.734 3.734 0 0 1 3.295-3.26M12 7.007a.75.75 0 0 1 .75.75v3.493h3.493a.75.75 0 1 1 0 1.5H12.75v3.493a.75.75 0 0 1-1.5 0V12.75H7.757a.75.75 0 0 1 0-1.5h3.493V7.757a.75.75 0 0 1 .75-.75" clip-rule="evenodd" />
</svg>
Icon source (verbatim from the user) — save as the + trigger icon, `1em`/`currentColor` per #465's mechanism: ```svg <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> <path fill="currentColor" fill-rule="evenodd" d="M7.345 4.017a42.3 42.3 0 0 1 9.31 0c1.713.192 3.095 1.541 3.296 3.26a40.7 40.7 0 0 1 0 9.446c-.201 1.719-1.583 3.068-3.296 3.26a42.3 42.3 0 0 1-9.31 0c-1.713-.192-3.095-1.541-3.296-3.26a40.7 40.7 0 0 1 0-9.445a3.734 3.734 0 0 1 3.295-3.26M12 7.007a.75.75 0 0 1 .75.75v3.493h3.493a.75.75 0 1 1 0 1.5H12.75v3.493a.75.75 0 0 1-1.5 0V12.75H7.757a.75.75 0 0 1 0-1.5h3.493V7.757a.75.75 0 0 1 .75-.75" clip-rule="evenodd" /> </svg> ```
Author
Owner

Fix ready for review: #476 (branch fix/issue-466). Create (+) button left of the identity menu, showCreate-gated, three router links; import link / toolbar CTA / Owners CTAs removed; /import route kept. node --test 1009 pass / 0 fail; vite + esbuild green.

Fix ready for review: https://git.packden.us/crueber/walhub/pulls/476 (branch fix/issue-466). Create (+) button left of the identity menu, showCreate-gated, three router links; import link / toolbar CTA / Owners CTAs removed; /import route kept. node --test 1009 pass / 0 fail; vite + esbuild green.
Author
Owner

REVIEW: PR #476 (fix/issue-466) — verified in scratch worktree /tmp/pr476 (removed afterward); main worktree untouched (still clean).

FINDINGS — all 9 checks pass:

  1. Placement: App.jsx CreateMenu renders inside the ml-auto right cluster, immediately left of IdentityMenu (after theme toggle); trigger is btn px-2 py-1 compact plus-only with aria-label+title 'Create new'. 390px budget test updated (pinned 288px < 390px, create ~32px signed-in-only). PASS
  2. Gating: identity.js showCreate: signedIn — byte-identical condition to showIdentity; anon/none/discovery-fail all false; signed-out navbar unchanged. PASS
  3. Items: createItems() = New repository /new, Import repository /import, New organization /orgs/new, rendered as router links that navigate+close. PASS
  4. Popover: verbatim IdentityMenu contract — document outside-click w/ root.contains + onCleanup removal, Escape+focus-return, ArrowUp/Down walk, Tab-out dismiss, role menu/menuitem, trigger inside root (no toggle-fight), max-w-[calc(100vw-1rem)] bound. PASS
  5. Removals: navbar import link gone; /import route intact (index.jsx:60); Repos toolbar CTA + canWrite gate gone (h3 kept, import footer /import?owner= intact); Owners 3 CTAs + dead getMe/canWrite gone (heading + intro card kept). PASS
  6. Plus icon: via shared Icon mechanism (no at call site), 16-unit viewBox, currentColor, no hex literals. PASS
  7. navModel: showCreate + createItems() live in identity.js; App.jsx has no inline branching; no permission probe, no fetches (law 6). PASS
  8. No orphans: old CTA pins rewritten to post-#466 state (repos-toolbar-413 SUPERSEDED header, others updated), none deleted; no dead /new/orgs/new/import links in removed spots. PASS
  9. No backend change (files: web/ + DEVIATIONS.md only), no new deps (package.json/lock untouched), D-WEB-13 appended (law 12). Laws 1/7/8 hold. PASS

TESTS (scratch, node_modules symlinked from main): node --test web/test/unit/*.test.js = 1012 tests, 1009 pass / 0 fail / 3 skipped (server-smoke skips, no server up) — matches PR claim. vite build + esbuild SDK bundle green. Note: default :8080 run shows 2 smoke failures because a foreign live 'walhub serve' answers :8080 /healthz 200 but / 401 — left untouched per instructions; environmental, unrelated to PR. No browser check performed (per review instructions; popover is a line-level clone of the shipped IdentityMenu contract).

NON-BLOCKING OBSERVATION (pre-existing, out of scope — NOT fixed): CreateMenu.jsx toggle focuses the first item under 'if (!getOpen())' after setOpen, which reads the NEW value, so focus-on-open never fires (fires only on close, into an unmounting menu). IdentityMenu.jsx has the identical line, so the 'matches IdentityMenu' criterion holds bug-for-bug; fixing one side would diverge them. Suggest a follow-up issue covering both menus, not this PR.

No code pushed (nothing to fix). Main worktree left clean.

MERGE RECOMMENDATION: ready to merge.

REVIEW: PR #476 (fix/issue-466) — verified in scratch worktree /tmp/pr476 (removed afterward); main worktree untouched (still clean). FINDINGS — all 9 checks pass: 1. Placement: App.jsx CreateMenu renders inside the ml-auto right cluster, immediately left of IdentityMenu (after theme toggle); trigger is btn px-2 py-1 compact plus-only with aria-label+title 'Create new'. 390px budget test updated (pinned 288px < 390px, create ~32px signed-in-only). PASS 2. Gating: identity.js showCreate: signedIn — byte-identical condition to showIdentity; anon/none/discovery-fail all false; signed-out navbar unchanged. PASS 3. Items: createItems() = New repository /new, Import repository /import, New organization /orgs/new, rendered as router links that navigate+close. PASS 4. Popover: verbatim IdentityMenu contract — document outside-click w/ root.contains + onCleanup removal, Escape+focus-return, ArrowUp/Down walk, Tab-out dismiss, role menu/menuitem, trigger inside root (no toggle-fight), max-w-[calc(100vw-1rem)] bound. PASS 5. Removals: navbar import link gone; /import route intact (index.jsx:60); Repos toolbar CTA + canWrite gate gone (h3 kept, import footer /import?owner= intact); Owners 3 CTAs + dead getMe/canWrite gone (heading + intro card kept). PASS 6. Plus icon: via shared Icon mechanism (no <svg> at call site), 16-unit viewBox, currentColor, no hex literals. PASS 7. navModel: showCreate + createItems() live in identity.js; App.jsx has no inline branching; no permission probe, no fetches (law 6). PASS 8. No orphans: old CTA pins rewritten to post-#466 state (repos-toolbar-413 SUPERSEDED header, others updated), none deleted; no dead /new/orgs/new/import links in removed spots. PASS 9. No backend change (files: web/ + DEVIATIONS.md only), no new deps (package.json/lock untouched), D-WEB-13 appended (law 12). Laws 1/7/8 hold. PASS TESTS (scratch, node_modules symlinked from main): node --test web/test/unit/*.test.js = 1012 tests, 1009 pass / 0 fail / 3 skipped (server-smoke skips, no server up) — matches PR claim. vite build + esbuild SDK bundle green. Note: default :8080 run shows 2 smoke failures because a foreign live 'walhub serve' answers :8080 /healthz 200 but / 401 — left untouched per instructions; environmental, unrelated to PR. No browser check performed (per review instructions; popover is a line-level clone of the shipped IdentityMenu contract). NON-BLOCKING OBSERVATION (pre-existing, out of scope — NOT fixed): CreateMenu.jsx toggle focuses the first item under 'if (!getOpen())' after setOpen, which reads the NEW value, so focus-on-open never fires (fires only on close, into an unmounting menu). IdentityMenu.jsx has the identical line, so the 'matches IdentityMenu' criterion holds bug-for-bug; fixing one side would diverge them. Suggest a follow-up issue covering both menus, not this PR. No code pushed (nothing to fix). Main worktree left clean. MERGE RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #476 (review clean — all 9 checks pass; focus follow-up filed as #477), merged. Closing.

Fixed by PR #476 (review clean — all 9 checks pass; focus follow-up filed as #477), merged. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#466
No description provided.