Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
021044a
feat(oc-docs): add sticky top navigation bar (BRU-3572)
sundram-bruno Jun 16, 2026
f349f9e
fix(oc-docs): pin Open-in-Bruno CTA to the right with empty slots (BR…
sundram-bruno Jun 16, 2026
7642b9d
feat(oc-docs): initials brand avatar with Bruno amber gradient (BRU-3…
sundram-bruno Jun 16, 2026
e8cb550
feat(oc-docs): display collection version with a leading "v" (BRU-3572)
sundram-bruno Jun 16, 2026
4c9f8aa
refactor(oc-docs): address self-review on topbar (BRU-3572)
sundram-bruno Jun 17, 2026
3795aa7
feat(oc-docs): Docs brand label, desktop-only CTA, hamburger below de…
sundram-bruno Jun 17, 2026
52e9fc4
style(oc-docs): borderless ghost icon-buttons in topbar (BRU-3572)
sundram-bruno Jun 17, 2026
64d5d58
feat(oc-docs): gate Open-in-Bruno on device capability, not width (BR…
sundram-bruno Jun 17, 2026
aeb4132
refactor(oc-docs): align Topbar to per-component folder convention (B…
sundram-bruno Jun 17, 2026
5070942
feat(oc-docs): compact mobile brand — avatar + "Docs" only (BRU-3572)
sundram-bruno Jun 17, 2026
b5a7334
fix(oc-docs): address self-review on topbar capability + shadow (BRU-…
sundram-bruno Jun 17, 2026
2d538b1
fix(oc-docs): align topbar header to design — hamburger, spacing, ava…
sundram-bruno Jun 18, 2026
8e0675f
test(oc-docs): use data-testid selectors in topbar e2e (BRU-3572)
sundram-bruno Jun 19, 2026
dfeded3
refactor(oc-docs): drop per-component index.ts barrels per review (BR…
sundram-bruno Jun 20, 2026
41c8711
test(oc-docs): scope header e2e to shipped behavior + adopt e2e struc…
sundram-bruno Jun 24, 2026
a91a588
refactor(oc-docs): eager capability check + note slot/toggle gaps (BR…
sundram-bruno Jun 24, 2026
c8c20b6
Merge remote-tracking branch 'upstream/main' into feat/bru-3572-top-n…
sundram-bruno Jun 24, 2026
b3bd08a
test(oc-docs): migrate request-errors e2e to the page-object structur…
sundram-bruno Jun 24, 2026
dbb954f
refactor(oc-docs): align topbar to the team folder conventions (BRU-3…
sundram-bruno Jun 24, 2026
e1125b0
Merge main into topbar branch
sundram-bruno Jun 25, 2026
960e2d0
Rewrite pages.fixture comment to document present fixtures
sundram-bruno Jun 25, 2026
d2f7ed8
Assert the header bounding box exists before checking its position
sundram-bruno Jun 25, 2026
d32de6f
Guard the header bounding box with an explicit null check
sundram-bruno Jun 25, 2026
ba50c29
Move getInitials into utils/common.ts
sundram-bruno Jun 25, 2026
d8e9b69
Drop the oc- prefix from topbar class names
sundram-bruno Jun 25, 2026
f5bf208
Move Topbar into src/ui and strip stylesheet comments
sundram-bruno Jun 25, 2026
ee6027f
Select the sidebar toggle by test id
sundram-bruno Jun 25, 2026
2a72ce2
Add a 2px gap between the brand name and version
sundram-bruno Jun 25, 2026
4558718
Move Topbar back to components/
sundram-bruno Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/oc-docs/e2e/components/layout/page-header.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Page } from '@playwright/test';
import { BaseComponent } from '../base.component';

/**
* The page header (sticky top navigation bar). A layout component — present on
* every screen — so tests get it handed over directly (`pageHeader.brandName`),
* the same way the sidebar is.
*
* Scoped to the `topbar` test id. Exposes the header's own shipped chrome:
* brand cluster, the Open-in-Bruno CTA, and the menu (hamburger) trigger.
* Parts are found by test id or accessible name, never by class.
*/
export class PageHeaderComponent extends BaseComponent {
constructor(page: Page) {
super(page, page.getByTestId('topbar'));
}

// Brand cluster
readonly brand = this.root.getByTestId('brand');
readonly brandName = this.root.getByTestId('brand-name');
readonly brandVersion = this.root.getByTestId('brand-version');
readonly brandInitials = this.root.getByTestId('brand-initials');

// Open-in-Bruno CTA
readonly openInBruno = this.root.getByTestId('open-in-bruno');

// Sidebar (hamburger) trigger — shown below desktop. The header only renders
// the button; the drawer it opens lives elsewhere.
readonly menuButton = this.root.getByTestId('topbar-menu');
}
10 changes: 8 additions & 2 deletions packages/oc-docs/e2e/playwright/pages.fixture.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import { test as base } from '@playwright/test';
import { OverviewPage } from '../pages/overview.page';
import { PageHeaderComponent } from '../components/layout/page-header.component';
import { ThemeToggleComponent } from '../components/theme-toggle.component';

/**
* Each page object gets a fixture, and so do the common components specs drive
* directly — the theme switch today, the sidebar and page header when they're added.
* Registers the page objects and shared components as Playwright fixtures, so a
* spec receives a ready instance by destructuring (e.g. `{ pageHeader }`) and
* calls `pageHeader.brandName` directly instead of constructing it.
*/
type Fixtures = {
overviewPage: OverviewPage;
pageHeader: PageHeaderComponent;
themeToggle: ThemeToggleComponent;
};

export const test = base.extend<Fixtures>({
overviewPage: async ({ page }, use) => {
await use(new OverviewPage(page));
},
pageHeader: async ({ page }, use) => {
await use(new PageHeaderComponent(page));
},
themeToggle: async ({ page }, use) => {
await use(new ThemeToggleComponent(page));
}
Expand Down
72 changes: 72 additions & 0 deletions packages/oc-docs/e2e/tests/layout/page-header.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { test, expect } from '../../playwright';

/**
* The page header (sticky top navigation bar): brand cluster + Open-in-Bruno
* CTA. These tests cover what the mounted app actually renders — the search and
* env-switcher slots ship empty here, so they aren't exercised in this suite.
*/
test.use({ colorScheme: 'light' });

const DESKTOP = { width: 1280, height: 900 };
const MOBILE = { width: 390, height: 800 };

test.describe('Page header', () => {
test('shows brand (name + version) and a pinned bar', async ({ page, pageHeader }) => {
await page.setViewportSize(DESKTOP);
await page.goto('/');

await expect(pageHeader.root).toBeVisible();
await expect(pageHeader.brandName).toContainText('Bruno Testbench');
await expect(pageHeader.brandVersion).toHaveText('v1.0.0');

// Sticky: header stays at the top after the page scrolls.
await page.mouse.wheel(0, 600);
const box = await pageHeader.root.boundingBox();
if (!box) throw new Error('header has no bounding box');
expect(box.y).toBeLessThanOrEqual(1);
});

test('shows the initials avatar derived from the collection name', async ({ page, pageHeader }) => {
await page.setViewportSize(DESKTOP);
await page.goto('/');

// sampleCollection name is "Bruno Testbench" → "BT".
await expect(pageHeader.brandInitials).toBeVisible();
await expect(pageHeader.brandInitials).toHaveText('BT');
});

test('Open-in-Bruno CTA deep-links via bruno:// and is pinned right', async ({ page, pageHeader }) => {
await page.setViewportSize(DESKTOP);
await page.goto('/');

await expect(pageHeader.openInBruno).toBeVisible();
const href = await pageHeader.openInBruno.getAttribute('href');
expect(href).toMatch(/^bruno:\/\/app\/collection\/import\/git\?url=/);

// CTA hugs the right edge (within the 20px bar padding), not the brand.
const headerBox = await pageHeader.root.boundingBox();
const ctaBox = await pageHeader.openInBruno.boundingBox();
const brandBox = await pageHeader.brand.boundingBox();
expect((headerBox!.x + headerBox!.width) - (ctaBox!.x + ctaBox!.width)).toBeLessThanOrEqual(24);
expect(ctaBox!.x).toBeGreaterThan(brandBox!.x + brandBox!.width + 100);
});

test('mobile condenses: hamburger shows, CTA hidden, brand compact', async ({ page, pageHeader }) => {
await page.setViewportSize(MOBILE);
await page.goto('/');

// Below desktop the sidebar trigger appears.
await expect(pageHeader.menuButton).toBeVisible();
// Open-in-Bruno is desktop-only (no Bruno desktop app on mobile).
await expect(pageHeader.openInBruno).toHaveCount(0);

// Compact brand: avatar + "Docs" only — no full name, no version.
await expect(pageHeader.brandName).toHaveText('Docs');
await expect(pageHeader.brandVersion).toHaveCount(0);
await expect(pageHeader.root).not.toContainText('Bruno Testbench');

// No horizontal overflow.
const scrollW = await page.evaluate(() => document.documentElement.scrollWidth);
expect(scrollW).toBeLessThanOrEqual(MOBILE.width + 1);
});
});
107 changes: 107 additions & 0 deletions packages/oc-docs/src/assets/icons/BrunoGlyph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from 'react';

/** Bruno mascot — fixed brand colours (a brand mark, not a themeable surface).
* Used inside the Open-in-Bruno CTA. */
export const BrunoGlyph: React.FC = () => (
Comment thread
sundram-bruno marked this conversation as resolved.
<svg viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<g>
<path
fill="#F4AA41"
d="M23.5,14.5855l-4.5,1.75l-7.25,8.5l-4.5,10.75l2,5.25c1.2554,3.7911,3.5231,7.1832,7.25,10l2.5-3.3333 c0,0,3.8218,7.7098,10.7384,8.9598c0,0,10.2616,1.936,15.5949-0.8765c3.4203-1.8037,4.4167-4.4167,4.4167-4.4167l3.4167-3.4167 l1.5833,2.3333l2.0833-0.0833l5.4167-7.25L64,37.3355l-0.1667-4.5l-2.3333-5.5l-4.8333-7.4167c0,0-2.6667-4.9167-8.1667-3.9167 c0,0-6.5-4.8333-11.8333-4.0833S32.0833,10.6688,23.5,14.5855z"
/>
<polygon
fill="#EA5A47"
points="36,47.2521 32.9167,49.6688 30.4167,49.6688 30.3333,53.5021 31.0833,57.0021 32.1667,58.9188 35,60.4188 39.5833,59.8355 41.1667,58.0855 42.1667,53.8355 41.9167,49.8355 39.9167,50.0855"
/>
<polygon
fill="#3F3F3F"
points="32.5,36.9188 30.9167,40.6688 33.0833,41.9188 34.3333,42.4188 38.6667,42.5855 41.5833,40.3355 39.8333,37.0855"
/>
</g>
<g>
<path
fill="#000000"
d="M29.5059,30.1088c0,0-1.8051,1.2424-2.7484,0.6679c-0.9434-0.5745-1.2424-1.8051-0.6679-2.7484 s1.805-1.2424,2.7484-0.6679S29.5059,30.1088,29.5059,30.1088z"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M33.1089,37.006h6.1457c0.4011,0,0.7634,0.2397,0.9203,0.6089l1.1579,2.7245l-2.1792,1.1456 c-0.6156,0.3236-1.3654-0.0645-1.4567-0.754"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M34.7606,40.763c-0.1132,0.6268-0.7757,0.9895-1.3647,0.7471l-2.3132-0.952l1.0899-2.9035 c0.1465-0.3901,0.5195-0.6486,0.9362-0.6486"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M30.4364,50.0268c0,0-0.7187,8.7934,3.0072,9.9375c2.6459,0.8125,5.1497,0.5324,6.0625-0.25 c0.875-0.75,2.6323-4.4741,1.8267-9.6875"
/>
<path
fill="#000000"
d="M44.2636,30.1088c0,0,1.805,1.2424,2.7484,0.6679c0.9434-0.5745,1.2424-1.8051,0.6679-2.7484 c-0.5745-0.9434-1.805-1.2424-2.7484-0.6679C43.9881,27.9349,44.2636,30.1088,44.2636,30.1088z"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M25.6245,42.8393c-0.475,3.6024,2.2343,5.7505,4.2847,6.8414c1.1968,0.6367,2.6508,0.5182,3.7176-0.3181l2.581-2.0233l2.581,2.0233 c1.0669,0.8363,2.5209,0.9548,3.7176,0.3181c2.0504-1.0909,4.7597-3.239,4.2847-6.8414"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M19.9509,28.3572c-2.3166,5.1597-0.5084,13.0249,0.119,15.3759c0.122,0.4571,0.0755,0.9355-0.1271,1.3631l-1.9874,4.1937 c-0.623,1.3146-2.3934,1.5533-3.331,0.4409c-3.1921-3.7871-8.5584-11.3899-6.5486-16.686 c7.0625-18.6104,15.8677-18.1429,15.8677-18.1429c2.8453-1.9336,13.1042-6.9375,24.8125,0.875c0,0,8.6323-1.7175,14.9375,16.9375 c1.8036,5.3362-3.4297,12.8668-6.5506,16.6442c-0.9312,1.127-2.7162,0.8939-3.3423-0.4272l-1.9741-4.1656 c-0.2026-0.4275-0.2491-0.906-0.1271-1.3631c0.6275-2.3509,2.4356-10.2161,0.119-15.3759"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M52.6309,46.4628c0,0-3.0781,6.7216-7.8049,8.2712"
/>
<path
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
d="M19.437,46.969c0,0,3.0781,6.0823,7.8049,7.632"
/>
<line
x1="36.2078"
x2="36.2078"
y1="47.3393"
y2="44.3093"
fill="none"
stroke="#000000"
strokeLinecap="round"
strokeLinejoin="round"
strokeMiterlimit="10"
strokeWidth="2"
/>
</g>
</svg>
);
9 changes: 9 additions & 0 deletions packages/oc-docs/src/assets/icons/HamburgerIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';
import { baseIconProps } from './baseIconProps';

/** Three bars — Topbar sidebar (hamburger) toggle. */
export const HamburgerIcon: React.FC = () => (
<svg {...baseIconProps}>
<path d="M3 6h18M3 12h18M3 18h18" />
</svg>
);
10 changes: 10 additions & 0 deletions packages/oc-docs/src/assets/icons/OverflowIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';

/** Vertical ellipsis — Topbar mobile overflow trigger. */
export const OverflowIcon: React.FC = () => (
<svg width={20} height={20} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<circle cx="12" cy="5" r="1.6" />
<circle cx="12" cy="12" r="1.6" />
<circle cx="12" cy="19" r="1.6" />
</svg>
);
10 changes: 10 additions & 0 deletions packages/oc-docs/src/assets/icons/SearchIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';
import { baseIconProps } from './baseIconProps';

/** Magnifying glass — Topbar search toggle. */
export const SearchIcon: React.FC = () => (
<svg {...baseIconProps}>
<circle cx="11" cy="11" r="7" />
<path d="m21 21-4.3-4.3" />
</svg>
);
4 changes: 2 additions & 2 deletions packages/oc-docs/src/assets/icons/baseIconProps.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SVGProps } from 'react';

/** Shared stroke styling for the empty-state icons. `currentColor` lets the icon
* inherit the surrounding theme colour, so it adapts when the theme changes. */
/** Shared stroke styling. `currentColor` lets the icon inherit the surrounding
* theme colour, so it adapts when the theme changes. */
export const baseIconProps: SVGProps<SVGSVGElement> = {
width: 20,
height: 20,
Expand Down
4 changes: 4 additions & 0 deletions packages/oc-docs/src/assets/icons/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
export * from './SearchIcon';
export * from './HamburgerIcon';
export * from './OverflowIcon';
export * from './BrunoGlyph';
export * from './GlobeIcon';
export * from './BookIcon';

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, it, expect } from 'vitest';
import InitialsAvatar from './InitialsAvatar';

describe('InitialsAvatar', () => {
it('renders the initials for a multi-word collection name', () => {
const html = renderToStaticMarkup(<InitialsAvatar collectionName="Hotel Booking API" />);
expect(html).toContain('HB');
});

it('renders a single letter for a one-word name', () => {
const html = renderToStaticMarkup(<InitialsAvatar collectionName="Echo" />);
expect(html).toContain('>E<');
});
});
23 changes: 23 additions & 0 deletions packages/oc-docs/src/components/InitialsAvatar/InitialsAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { getInitials } from '../../utils/common';
import { StyledWrapper } from './StyledWrapper';

export interface InitialsAvatarProps {
collectionName: string;
testId?: string;
}

/**
* Default brand mark: a rounded badge showing the collection initials over the
* Bruno amber gradient.
*/
const InitialsAvatar: React.FC<InitialsAvatarProps> = ({
collectionName,
testId = 'brand-initials',
}) => (
<StyledWrapper aria-hidden="true" data-testid={testId}>
{getInitials(collectionName)}
</StyledWrapper>
);

export default InitialsAvatar;
19 changes: 19 additions & 0 deletions packages/oc-docs/src/components/InitialsAvatar/StyledWrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import styled from '@emotion/styled';

export const StyledWrapper = styled.span`
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
flex: none;
border-radius: var(--oc-border-radius-base);
background: linear-gradient(135deg, #d37f17 0%, #dc9741 100%);
color: #fff;
font-family: var(--font-mono);
font-size: var(--oc-font-size-xs);
font-weight: 700;
line-height: 1;
letter-spacing: -0.02em;
user-select: none;
`;
Loading
Loading