From 9b7ed7a11daebf3733b8fefc1c546613aafe9639 Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:34:14 +0100 Subject: [PATCH 1/6] feat(nav): redesign header with Platform/Products/Examples tabs Replace the previous header navigation with three top-level tabs: Platform, Products, and Examples. Add a search button trigger with keyboard shortcut hint, Ask AI button, and help resources dropdown. Restructure the mobile menu with a tab-based layout where the Products tab keeps the ProductBar pinned above the scrollable sidebar. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Examples/ExamplesContent.test.tsx | 4 +- src/components/Examples/ExamplesFilter.tsx | 27 +- src/components/Layout/Header.test.tsx | 3 +- src/components/Layout/Header.tsx | 389 ++++++++++-------- src/components/ui/Input.tsx | 22 + 5 files changed, 269 insertions(+), 176 deletions(-) create mode 100644 src/components/ui/Input.tsx diff --git a/src/components/Examples/ExamplesContent.test.tsx b/src/components/Examples/ExamplesContent.test.tsx index 7d1d86b212..5d3ccef6f7 100644 --- a/src/components/Examples/ExamplesContent.test.tsx +++ b/src/components/Examples/ExamplesContent.test.tsx @@ -58,7 +58,7 @@ describe('ExamplesContent', () => { it('filters examples based on search input (no results)', () => { render(); - const searchInput = screen.getByPlaceholderText('Find an example'); + const searchInput = screen.getByPlaceholderText('Find an example...'); fireEvent.change(searchInput, { target: { value: 'nonexistent' } }); expect(screen.getByText('No results found')).toBeInTheDocument(); expect(screen.queryByText('Member location')).not.toBeInTheDocument(); @@ -67,7 +67,7 @@ describe('ExamplesContent', () => { it('filters examples based on search input (with results)', () => { render(); - const searchInput = screen.getByPlaceholderText('Find an example'); + const searchInput = screen.getByPlaceholderText('Find an example...'); fireEvent.change(searchInput, { target: { value: 'avatar' } }); expect(screen.queryByText('No results found')).not.toBeInTheDocument(); expect(screen.queryByText('Member location')).not.toBeInTheDocument(); diff --git a/src/components/Examples/ExamplesFilter.tsx b/src/components/Examples/ExamplesFilter.tsx index ec22cf980f..0ca712d337 100644 --- a/src/components/Examples/ExamplesFilter.tsx +++ b/src/components/Examples/ExamplesFilter.tsx @@ -1,6 +1,7 @@ import React, { ChangeEvent, Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import Icon from '@ably/ui/core/Icon'; +import { Input } from 'src/components/ui/Input'; import { products } from '../../data/examples'; import Button from '@ably/ui/core/Button'; import cn from '@ably/ui/core/utils/cn'; @@ -111,18 +112,22 @@ const ExamplesFilter = ({ return ( <> -
- +
+ + handleSearch(e)} + />
- handleSearch(e)} - /> + + +
, +
+
+ +
+
+ +
+
, + , + ]} + rootClassName="h-full overflow-y-hidden min-h-[3.1875rem] flex flex-col" + contentClassName="h-full overflow-y-scroll" + tabClassName="ui-text-menu2 !px-4" + options={{ flexibleTabWidth: true }} + /> + )} - - - - - - - - - Help & Resources - - - - +
+ {!externalScriptsData.inkeepSearchEnabled && ( +
+ - - - Open CLI - - + + + Search docs... + + + + ⌘ + + + K + + + +
)} - {sessionState.signedIn ? ( - <> - {sessionState.preferredEmail ? ( - +
+ +
+ {externalScriptsData.inkeepChatEnabled && ( + + )} + + + - - Dashboard - + - - {sessionState.preferredEmail} - - - ) : ( - - Dashboard - - )} + + + Help & Resources + + + + + {helpResourcesItems.map((item) => ( + + +
+ + {item.label} +
+ {item.external && } +
+
+ ))} +
+
+
+ {CLI_ENABLED && ( - - Log out + Open CLI - - ) : ( - <> - - Login - - - Start free - - - )} + )} + {sessionState.signedIn ? ( + <> + {sessionState.preferredEmail ? ( + + + + Dashboard + + + + + {sessionState.preferredEmail} + + + + ) : ( + + Dashboard + + )} + + + + + + Log out + + + + ) : ( + <> + + Login + + + Start free + + + )} +
+
+
+
- -
- -
-
- {externalScriptsData.inkeepSearchEnabled && ( - - )} - {externalScriptsData.inkeepChatEnabled && ( - - )} +
+ {externalScriptsData.inkeepSearchEnabled && ( + + )} + {externalScriptsData.inkeepChatEnabled && ( + + )} +
); diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx new file mode 100644 index 0000000000..06d36a5921 --- /dev/null +++ b/src/components/ui/Input.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import cn from '@ably/ui/core/utils/cn'; + +function Input({ className, type, ...props }: React.ComponentProps<'input'>) { + return ( + + ); +} + +export { Input }; From 9aeae9e2ccd01b3eaaf335884a86096a53b5826d Mon Sep 17 00:00:00 2001 From: Mark Hulbert <39801222+m-hulbert@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:34:21 +0100 Subject: [PATCH 2/6] feat(nav): add ProductBar and restructure layout Add a sticky ProductBar for switching between products (Pub/Sub, Chat, Spaces, etc.) shown below the header on non-platform pages. Restructure the main layout from a centred container to a full-width flex layout with left sidebar, content pane (max 860px for docs, 1024px for examples), and right sidebar. Change button hover style from border colour to background fill. Co-Authored-By: Claude Opus 4.6 (1M context) --- data/onCreatePage.ts | 2 +- src/components/Layout/Breadcrumbs.test.tsx | 16 +-- src/components/Layout/Breadcrumbs.tsx | 28 ++--- src/components/Layout/Layout.tsx | 50 ++++---- src/components/Layout/ProductBar.test.tsx | 97 +++++++++++++++ src/components/Layout/ProductBar.tsx | 131 +++++++++++++++++++++ src/components/Layout/utils/heights.ts | 1 + src/components/Layout/utils/styles.ts | 2 +- 8 files changed, 283 insertions(+), 44 deletions(-) create mode 100644 src/components/Layout/ProductBar.test.tsx create mode 100644 src/components/Layout/ProductBar.tsx diff --git a/data/onCreatePage.ts b/data/onCreatePage.ts index c9e5650b05..a8fbdad732 100644 --- a/data/onCreatePage.ts +++ b/data/onCreatePage.ts @@ -12,7 +12,7 @@ export type LayoutOptions = { const mdxWrapper = path.resolve('src/components/Layout/MDXWrapper.tsx'); const pageLayoutOptions: Record = { - '/docs': { leftSidebar: true, rightSidebar: false, template: 'index', mdx: false }, + '/docs': { leftSidebar: false, rightSidebar: false, template: 'index', mdx: false }, '/docs/api/control-api': { leftSidebar: false, rightSidebar: false, diff --git a/src/components/Layout/Breadcrumbs.test.tsx b/src/components/Layout/Breadcrumbs.test.tsx index 4593ebeb03..e7d42ad8bc 100644 --- a/src/components/Layout/Breadcrumbs.test.tsx +++ b/src/components/Layout/Breadcrumbs.test.tsx @@ -70,25 +70,25 @@ describe('Breadcrumbs', () => { render(); // Current page (last item) should be disabled - expect(screen.getByText('Current Page')).toHaveClass('text-gui-unavailable'); + expect(screen.getByText('Current Page')).toHaveClass('text-neutral-700'); expect(screen.getByText('Current Page')).toHaveClass('pointer-events-none'); // Non-linked nodes (link='#') should be disabled - expect(screen.getByText('Subsection 1')).toHaveClass('text-gui-unavailable'); + expect(screen.getByText('Subsection 1')).toHaveClass('text-neutral-700'); expect(screen.getByText('Subsection 1')).toHaveClass('pointer-events-none'); // Active links should not be disabled - expect(screen.getByText('Section 1')).not.toHaveClass('text-gui-unavailable'); + expect(screen.getByText('Section 1')).not.toHaveClass('text-neutral-700'); expect(screen.getByText('Section 1')).not.toHaveClass('pointer-events-none'); }); it('shows only the last active node in mobile view', () => { render(); - // All items except index 0 should have 'hidden sm:flex' classes + // All items except index 0 should have 'hidden md:flex' classes expect(screen.getByText('Section 1')).not.toHaveClass('hidden'); - expect(screen.getByText('Subsection 1')).toHaveClass('hidden', 'sm:flex'); - expect(screen.getByText('Current Page')).toHaveClass('hidden', 'sm:flex'); + expect(screen.getByText('Subsection 1')).toHaveClass('hidden', 'md:flex'); + expect(screen.getByText('Current Page')).toHaveClass('hidden', 'md:flex'); }); it('correctly identifies last active node when current page is non-linked', () => { @@ -108,8 +108,8 @@ describe('Breadcrumbs', () => { }); render(); - expect(screen.getByText('Section 1')).toHaveClass('hidden', 'sm:flex'); + expect(screen.getByText('Section 1')).toHaveClass('hidden', 'md:flex'); expect(screen.getByText('Subsection 1')).not.toHaveClass('hidden'); - expect(screen.getByText('Current Page')).toHaveClass('hidden', 'sm:flex'); + expect(screen.getByText('Current Page')).toHaveClass('hidden', 'md:flex'); }); }); diff --git a/src/components/Layout/Breadcrumbs.tsx b/src/components/Layout/Breadcrumbs.tsx index ea0a967763..f0c080adab 100644 --- a/src/components/Layout/Breadcrumbs.tsx +++ b/src/components/Layout/Breadcrumbs.tsx @@ -6,7 +6,7 @@ import cn from '@ably/ui/core/utils/cn'; import { hierarchicalKey } from './utils/nav'; const linkStyles = - 'ui-text-label4 font-semibold text-neutral-900 hover:text-neutral-1300 active:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-000 dark:active:text-neutral-500 focus-base transition-colors'; + 'ui-text-label4 font-semibold text-neutral-900 hover:text-neutral-1300 active:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-000 dark:active:text-neutral-500 focus-base transition-colors whitespace-nowrap'; const Breadcrumbs: React.FC = () => { const { activePage } = useLayoutContext(); @@ -35,40 +35,40 @@ const Breadcrumbs: React.FC = () => { })(); return ( -