Skip to content

feat(oc-docs): add sticky top navigation bar (BRU-3572)#44

Open
sundram-bruno wants to merge 14 commits into
opencollection-dev:mainfrom
sundram-bruno:feat/bru-3572-top-navigation-bar
Open

feat(oc-docs): add sticky top navigation bar (BRU-3572)#44
sundram-bruno wants to merge 14 commits into
opencollection-dev:mainfrom
sundram-bruno:feat/bru-3572-top-navigation-bar

Conversation

@sundram-bruno

@sundram-bruno sundram-bruno commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

BRU-3572 — Top navigation bar

Adds a sticky, purely-presentational Topbar mounted into the OpenCollection desktop shell. Built as small reusable subcomponents so downstream tickets plug into the slots.

What's in it

  • src/components/Topbar/Topbar.tsx — the cross-lane contract surface. Pure presentational (no routing/data/store): everything via props.
    • Props: collectionName, version, logo, searchSlot, envSwitcherSlot, onOpenInBruno, onToggleSidebar + optional openInBrunoHref.
  • Brand — logo + collection name (truncates) + version.
  • OpenInBrunoButton — reusable CTA, restyled from FetchInBrunoButton. Renders a real bruno://… anchor when openInBrunoHref is set, else a <button> calling onOpenInBruno. Icon-only below desktop.
  • SlotssearchSlot (BRU-3573) and envSwitcherSlot (BRU-3186) render whatever node is passed and degrade gracefully when empty.
  • Removed the now-duplicate in-content "Fetch in Bruno" button from Docs — the Topbar owns the CTA.

Header related screenshots :
image

image

sundram-bruno and others added 10 commits June 17, 2026 03:09
Add a reusable, purely-presentational Topbar mounted into the
OpenCollection desktop shell. Composed of small subcomponents:
Brand (logo + collection name + version), an Open-in-Bruno CTA
(restyled from FetchInBrunoButton, renders a real bruno:// deep
link), and two slots — searchSlot (BRU-3573) and envSwitcherSlot
(BRU-3186) — that render whatever node is passed and degrade
gracefully when empty.

Cross-lane contract (src/components/Topbar/Topbar.tsx) kept stable
for BRU-3188: collectionName, version, logo, searchSlot,
envSwitcherSlot, onOpenInBruno, onToggleSidebar — plus an optional
openInBrunoHref so the CTA can render as an anchor with a real
bruno:// href (backward-compatible additive prop).

Responsive (no mobile/tablet Figma — designed here):
- desktop (>=1024): full bar, centered search, inline env + CTA.
- tablet (768-1023): sidebar inline (no hamburger), CTA icon-only,
  env switcher stays inline.
- mobile (<768): hamburger + brand + spacer + search icon + overflow
  + CTA icon. Search icon expands a full-width search row; the
  overflow popover relocates the same envSwitcherSlot node.

Styling maps every value to --oc-* theme tokens (no hardcoded hex):
square bar with bottom border only, 6px rounded inner controls,
Inter, honors light/dark. Remove the now-duplicate in-content
"Fetch in Bruno" button from Docs (Topbar owns the CTA).

Tests: vitest for buildBrunoDeepLink + layoutModeForWidth; Playwright
for sticky behavior, the bruno:// CTA href, mobile condense/overflow,
and search-expand (via a dev-only topbar harness page).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…U-3572)

With searchSlot/envSwitcherSlot undefined there was no flex-1 element
between the brand and the right cluster, so the CTA collapsed next to
the brand on the left. Always render a flex-1 middle (inline search when
present, else a spacer) so the right-hand controls + CTA stay pinned to
the right edge regardless of slot contents. Add a desktop e2e asserting
the CTA hugs the right edge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…572)

Default brand mark is now an initials badge derived from the collection
name (Brand renders it when no explicit logo is passed; a logo prop still
overrides). Matches the Figma brand mark: 26x26, 6px radius, white
initials over the Bruno amber linear gradient.

Initials: first letter of the first two words ("Hotel Booking API" -> HB,
"Bruno Testbench" -> BT); a single word uses just its first letter
("Echo" -> E). Pure getInitials() with unit tests.

Gradient stops map to existing theme tokens — no hardcoded hex:
  --oc-brand (#D37F17 light) -> --oc-primary-subtle (#DC9741 light),
text via --oc-button2-color-primary-text (white light / black dark), so
the badge is fully theme-aware (verified light + dark).

Drop the hardcoded opencollection logo from the dev harness so the live
app shows the initials avatar by default. Add an e2e asserting the BT
avatar renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brand now formats the version for display as "v1.0.0" instead of raw
"1.0.0", matching Figma. Idempotent — a version already prefixed
("v2") is left unchanged. The Topbar `version` prop stays the raw
string; only the rendered label is formatted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes from code review:

Bugs:
- Render the Open-in-Bruno CTA only when a deep link or onClick is
  provided, instead of a visible-but-inert button when neither is set.
- Reset the mobile search row when leaving the mobile layout so it
  doesn't reappear with stale aria state on the next mobile resize.
- MobileOverflow outside-dismiss now listens on pointerdown (covers
  touch) instead of mousedown.

Optimization:
- useTopbarLayout only setState on an actual breakpoint-band crossing
  (updater bail-out), and drop the redundant on-mount resize call.

Dead code / clarity:
- Delete the now-orphaned FetchInBrunoButton (superseded by
  OpenInBrunoButton).
- Correct the StyledWrapper comment: layout switching is in JSX, not
  CSS; data-mode is for debugging/e2e. Token claim scoped to colors.
- Use --oc-font-size-xs for the initials avatar instead of 11px.

Reusability:
- Extract a shared IconButton (hamburger, search toggle, overflow).
- Deduplicate the search-inner wrapper in Topbar.
- Collapse OpenInBrunoButton's anchor/button branches into one render.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sktop (BRU-3572)

Per updated mobile/tablet design:
- Header brand now shows the fixed "Docs" label + initials avatar (HB),
  not the collection name. Version is no longer rendered in the header
  (it remains in the page body). collectionName still drives the avatar
  initials and the accessible title.
- Open-in-Bruno is desktop-only — the Bruno desktop app isn't available
  on tablet/mobile, so the CTA is hidden below the desktop breakpoint.
- Hamburger now shows on tablet too (sidebar is a drawer below desktop,
  persistent only on desktop).
- Search collapses to an icon below desktop (tablet + mobile), expanding
  a full-width row; inline search input is desktop-only. Env switcher
  stays inline on tablet, in the overflow popover on mobile.

`version` is kept on TopbarProps for cross-lane contract stability but is
intentionally not rendered. Tests + harness updated for the new model.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the 1px border on the shared topbar icon-button (hamburger, search
toggle, overflow trigger) to match the design — ghost style with a
hover/active background for affordance instead of a border.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…U-3572)

iPad Pro is 1024–1366px wide, so the width-only desktop breakpoint showed
the Open-in-Bruno CTA on it. Split "layout mode" (width) from "can run the
Bruno desktop app" (capability): the CTA now needs the desktop layout AND a
capable device, so the iPad Pro gets the desktop layout with no CTA.

Capability (computeCanRunBrunoApp, pure + unit-tested):
- Require any fine, hovering pointer — `(any-hover: hover) and
  (any-pointer: fine)` — so touchscreen laptops / 2-in-1s (touch is the
  primary pointer but a trackpad exists) still qualify.
- Hard-exclude mobile/tablet OSes: UA match, plus maxTouchPoints > 1 on
  MacIntel to unmask iPadOS 13+ (which reports as Mac). A real Mac
  (maxTouchPoints === 0) is kept. The exclusion — not the media query — is
  what reliably catches an iPad with a trackpad folio.

Also revert the header brand to collection name + version (per design); the
"Docs"/no-version variant is dropped. Layout breakpoints unchanged: large
tablets (>=1024) keep the desktop layout, 768–1023 keep hamburger + search
icon. Add a dev-only topbar-device-check.html that prints the raw signals +
the computed decision for verifying the device matrix (not shipped).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…RU-3572)

Structure + naming only — no behavior, props, or visual change. Adopts the
house convention from PR opencollection-dev#45 (each component a folder with <Name>.tsx +
<Name>.spec.tsx + StyledWrapper.ts + index.ts; pure utils in src/utils;
hooks in src/hooks; shared glyphs in src/assets/icons.tsx).

Moves:
- Promote shared primitives to top-level src/components/: IconButton/,
  InitialsAvatar/, OpenInBrunoButton/ (reusable across lanes).
- Keep Topbar/, Topbar/Brand/, Topbar/MobileOverflow/ as their own folders.
- Split the single Topbar/StyledWrapper.ts into per-component StyledWrapper.ts.
- Pure utils -> src/utils: buildBrunoDeepLink, getInitials (+ .spec.ts).
- Hooks -> src/hooks: useTopbarLayout, useCanRunBrunoApp (+ .spec.ts).
- Raw glyphs -> src/assets/icons.tsx (SearchIcon, HamburgerIcon, OverflowIcon,
  BrunoGlyph). Matches PR opencollection-dev#45's icons.tsx style/baseIconProps; see the file's
  reconciliation note for the (mechanical) merge with opencollection-dev#45's glyph set.
- *.test.ts -> *.spec.ts(x); add a colocated render spec per component.

IconButton consolidation:
- Single canonical src/components/IconButton/ (icon-only button primitive,
  aria-label required). Removed the duplicate, unused IconButton from
  ui/MinimalComponents.tsx (the other exports there are untouched) and the
  local copy in Topbar/icons.tsx. All callers import the shared path.

Dev scaffolding removed from shipped src/ + package root: deleted
src/topbarHarness.tsx, src/deviceCheck.tsx, topbar-harness.html,
topbar-device-check.html. The e2e harness is now gated behind dev.tsx
(?view=topbar-harness); e2e navigates there instead of the html.

Public API unchanged: Topbar still exports from src/components/Topbar
(index.ts re-export) for BRU-3188. OpenCollection updated to import
buildBrunoDeepLink from src/utils.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On mobile the brand showed the full collection name + version like
tablet/desktop. Per design, mobile shows just the initials avatar (HB)
and a fixed "Docs" label — no collection name, no version (both remain
in the page body). Add a `compact` prop to Brand and drive it from
`isMobile` in Topbar. Tablet/desktop keep name + version.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…3572)

- useCanRunBrunoApp: re-detect pointer capability via the media query's
  own `change` event instead of window `resize` (a resize event does not
  fire when a trackpad / external mouse is attached, so the documented
  dynamic re-detection never ran). Initial-load detection is unchanged.
- MobileOverflow popover: use the --oc-shadow-md theme token instead of a
  hardcoded rgba shadow, so the elevation adapts in dark mode.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sundram-bruno and others added 3 commits June 19, 2026 01:37
…tar (BRU-3572)

Matched the Claude design reference (chrome.jsx):
- IconButton glyph now uses the base text token (--oc-text, = design's
  #343434 fg-base in light) instead of the muted token, and renders at
  18px — the hamburger/search/overflow glyphs read solid, not faded.
- Mobile bar tightened to match: padding 0 12px + gap 8 (from 20/12), and
  the hamburger gets margin-left -4 so the brand isn't over-indented.
- Brand logo↔text gap 10 -> 8.
- Initials avatar: gradient is now a fixed brand mark (#d37f17 -> #dc9741,
  identical in light/dark like the Bruno mascot), white text, JetBrains
  Mono with -0.02em tracking — matches the design letterforms (was Inter,
  theme-tokened, +0.02em).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per team convention, drop tag/class-based Playwright selectors
(page.locator('header.oc-topbar'), '.oc-topbar__brand-name', etc.) in
favor of data-testid. Adds data-testid to the Topbar root ("topbar"),
Brand ("brand"), brand name ("brand-name") and version ("brand-version")
and switches the e2e to getByTestId. Also replaces the emotion-class
assertion on the CTA (toHaveClass(/is-full/)) with a visibility check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…U-3572)

Team standard (reversed from the earlier 4-file pattern): component folders
have no index.ts barrel; consumers import the concrete file directly.

- Delete the 6 barrels this PR added: Topbar/, Topbar/Brand/,
  Topbar/MobileOverflow/, IconButton/, InitialsAvatar/, OpenInBrunoButton/.
- Repoint every importer at the concrete module (e.g. '../Topbar' ->
  '../Topbar/Topbar', '../IconButton' -> '../IconButton/IconButton'),
  switching the barrel's named re-exports back to the components' default
  exports. Touches Topbar.tsx, Brand.tsx, MobileOverflow.tsx,
  OpenCollection.tsx (mount) and dev.tsx (harness).

Behavior/visual unchanged. icons split + e2e locators restructure deferred
to mirror PR opencollection-dev#45 once it lands (icons.tsx already notes the reconciliation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants