- {leftSidebar ?
: null}
+
+
+ {leftSidebar ?
:
}
- {leftSidebar ? : null}
+ {leftSidebar ? : }
{children}
- {rightSidebar ?
: null}
+ {rightSidebar ?
:
}
);
diff --git a/src/components/Layout/LeftSidebar.test.tsx b/src/components/Layout/LeftSidebar.test.tsx
index a9148d1bbb..edd8d07563 100644
--- a/src/components/Layout/LeftSidebar.test.tsx
+++ b/src/components/Layout/LeftSidebar.test.tsx
@@ -1,6 +1,7 @@
import React from 'react';
import { useLocation } from '@reach/router';
-import { render, screen } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom/extend-expect';
import LeftSidebar from './LeftSidebar';
import { useLayoutContext } from 'src/contexts/layout-context';
@@ -23,56 +24,6 @@ jest.mock('../Link', () => {
return MockLink;
});
-// Mock productData
-jest.mock('src/data', () => ({
- productData: {
- platform: {
- nav: {
- name: 'Platform',
- icon: { open: 'icon-gui-chevron-up-micro', closed: 'icon-gui-chevron-down-micro' },
- content: [
- {
- name: 'Overview',
- pages: [
- { name: 'Introduction', link: '/platform/intro' },
- { name: 'Getting Started', link: '/platform/getting-started' },
- ],
- },
- ],
- api: [
- {
- name: 'API Overview',
- pages: [
- { name: 'API Introduction', link: '/platform/api-intro' },
- { name: 'API Reference', link: '/platform/api-reference' },
- ],
- },
- ],
- link: '/platform',
- showJumpLink: true,
- },
- },
- pubsub: {
- nav: {
- name: 'Pub/Sub',
- icon: { open: 'icon-gui-chevron-up-outline', closed: 'icon-gui-chevron-down-outline' },
- content: [
- {
- name: 'Overview',
- pages: [
- { name: 'Introduction', link: '/pubsub/intro' },
- { name: 'Getting Started', link: '/pubsub/getting-started' },
- ],
- },
- ],
- api: [],
- link: '/pubsub',
- showJumpLink: false,
- },
- },
- },
-}));
-
const mockUseLayoutContext = useLayoutContext as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
@@ -80,10 +31,8 @@ describe('LeftSidebar', () => {
beforeEach(() => {
mockUseLayoutContext.mockReturnValue({
activePage: {
- tree: [
- { index: 0, page: { name: 'Link 1', link: '/link-1' } },
- { index: 1, page: { name: 'Link 2', link: '/link-2' } },
- ],
+ page: { name: 'Test Page', link: '/platform/intro' },
+ tree: [{ index: 0, page: { name: 'Link 1', link: '/link-1' } }],
},
});
@@ -109,20 +58,86 @@ describe('LeftSidebar', () => {
it('renders the sidebar with products', () => {
render(
);
expect(screen.getByRole('button', { name: 'Platform' })).toBeInTheDocument();
- expect(screen.getByRole('button', { name: 'Pub/Sub' })).toBeInTheDocument();
+ expect(screen.getByRole('button', { name: 'Ably Pub/Sub' })).toBeInTheDocument();
});
- it('renders product content and API sections', () => {
+ it('shows Platform accordion expanded with first three child items when active page is under Platform', async () => {
render(
);
- expect(screen.getByText('Overview')).toBeInTheDocument();
- expect(screen.getByText('API Overview')).toBeInTheDocument();
+
+ // Since activePage.page.link is '/platform/intro', the Platform accordion should be open by default
+ // Verify the labels of the first three child accordion items are visible
+ await waitFor(() => {
+ expect(screen.getByText('Introduction')).toBeInTheDocument();
+ expect(screen.getByText('Architecture')).toBeInTheDocument();
+ expect(screen.getByText('Products and SDKs')).toBeInTheDocument();
+ });
+
+ // Verify these are clickable accordion triggers
+ const introButton = screen.getByText('Introduction').closest('button');
+ const archButton = screen.getByText('Architecture').closest('button');
+ const productsButton = screen.getByText('Products and SDKs').closest('button');
+
+ expect(introButton).toBeInTheDocument();
+ expect(archButton).toBeInTheDocument();
+ expect(productsButton).toBeInTheDocument();
});
- it('renders links for pages and API pages', () => {
+ it('expands Platform/Architecture accordion and shows first three child items', async () => {
+ const user = userEvent.setup();
render(
);
- expect(screen.getByText('Introduction')).toBeInTheDocument();
- expect(screen.getByText('Getting Started')).toBeInTheDocument();
- expect(screen.getByText('API Introduction')).toBeInTheDocument();
- expect(screen.getByText('API Reference')).toBeInTheDocument();
+
+ // Platform is already expanded since activePage is /platform/intro
+ expect(screen.queryByText('Overview')).not.toBeInTheDocument();
+ expect(screen.queryByText('Edge network')).not.toBeInTheDocument();
+ expect(screen.queryByText('Infrastructure operations')).not.toBeInTheDocument();
+
+ // Find and click Architecture button to expand it
+ const architectureButton = screen.getByText('Architecture').closest('button');
+ if (!architectureButton) {
+ throw new Error('Architecture button not found');
+ }
+ await user.click(architectureButton);
+
+ // After clicking, verify the first three child items are visible
+ await waitFor(() => {
+ expect(screen.getByText('Overview')).toBeInTheDocument();
+ expect(screen.getByText('Edge network')).toBeInTheDocument();
+ expect(screen.getByText('Infrastructure operations')).toBeInTheDocument();
+ });
+
+ // Verify these are links (leaf nodes) not accordion triggers
+ const overviewLink = screen.getByText('Overview').closest('a');
+ const edgeNetworkLink = screen.getByText('Edge network').closest('a');
+ const infrastructureLink = screen.getByText('Infrastructure operations').closest('a');
+
+ expect(overviewLink).toBeInTheDocument();
+ expect(edgeNetworkLink).toBeInTheDocument();
+ expect(infrastructureLink).toBeInTheDocument();
+ });
+
+ it('clicks Ably Pub/Sub to close Platform and expand Pub/Sub showing first three child items', async () => {
+ const user = userEvent.setup();
+ render(
);
+
+ // Initially on Platform page, so Platform accordion is open
+ expect(screen.getByText('Architecture')).toBeInTheDocument();
+
+ // Pub/Sub children should not be visible
+ expect(screen.queryByText('Authentication')).not.toBeInTheDocument();
+ expect(screen.queryByText('Connections')).not.toBeInTheDocument();
+
+ // Click on Ably Pub/Sub button to expand it (this should close Platform since type="single")
+ const pubsubButton = screen.getByRole('button', { name: 'Ably Pub/Sub' });
+ await user.click(pubsubButton);
+
+ // After clicking, verify the first three Pub/Sub child accordion items appear
+ await waitFor(() => {
+ expect(screen.getByText('Introduction')).toBeInTheDocument();
+ expect(screen.getByText('Authentication')).toBeInTheDocument();
+ expect(screen.getByText('Connections')).toBeInTheDocument();
+ });
+
+ // Platform's Architecture should no longer be visible since accordion closed
+ expect(screen.queryByText('Architecture')).not.toBeInTheDocument();
});
});
diff --git a/src/components/Layout/LeftSidebar.tsx b/src/components/Layout/LeftSidebar.tsx
index 3b6582cbca..854bbb060f 100644
--- a/src/components/Layout/LeftSidebar.tsx
+++ b/src/components/Layout/LeftSidebar.tsx
@@ -1,238 +1,189 @@
-import { useMemo, useState, useEffect, useRef } from 'react';
-import { navigate, useLocation } from '@reach/router';
+import { useEffect, useRef, useState } from 'react';
+import * as Accordion from '@radix-ui/react-accordion';
import cn from '@ably/ui/core/utils/cn';
-import Accordion from '@ably/ui/core/Accordion';
-import { AccordionData } from '@ably/ui/core/Accordion/types';
import Icon from '@ably/ui/core/Icon';
-import { throttle } from 'lodash';
import { productData } from 'src/data';
-import { NavProduct, NavProductContent, NavProductPages } from 'src/data/nav/types';
-import {
- commonAccordionOptions,
- composeNavLinkId,
- hierarchicalKey,
- PageTreeNode,
- sidebarAlignmentClasses,
- sidebarAlignmentStyles,
-} from './utils/nav';
+import { NavProductContent, NavProductPage } from 'src/data/nav/types';
import Link from '../Link';
import { useLayoutContext } from 'src/contexts/layout-context';
-type ContentType = 'content' | 'api';
-
type LeftSidebarProps = {
inHeader?: boolean;
};
-const NavPage = ({
- depth,
- page,
- indices,
- type,
- indentLinks,
- inHeader,
+const accordionContentClassName =
+ 'overflow-hidden data-[state=open]:animate-accordion-down data-[state=closed]:animation-accordion-up';
+
+const accordionTriggerClassName = cn(
+ // Layout & display
+ 'flex items-center justify-between gap-2 p-0 pr-2 w-full text-left ui-text-label3',
+ // Background color states
+ 'bg-neutral-000 dark:bg-neutral-1300 hover:bg-neutral-100 dark:hover:bg-neutral-1200 active:bg-neutral-200 dark:active:bg-neutral-1100',
+ // Text color states
+ 'text-neutral-900 dark:text-neutral-400 hover:text-neutral-1300 dark:hover:text-neutral-000',
+ // State styles
+ 'data-[state=open]:text-neutral-1300 dark:data-[state=open]:text-neutral-000 data-[state=open]:font-bold',
+ // Icon animation
+ '[&[data-state=open]>svg]:rotate-90',
+ // Misc
+ 'focus-base transition-colors',
+);
+
+const accordionLinkClassName = 'pl-3 py-[6px]';
+
+const iconClassName = 'text-neutral-1300 dark:text-neutral-000 transition-transform';
+
+const ChildAccordion = ({
+ content,
+ layer,
+ tree,
}: {
- depth: number;
- page: NavProductPages;
- indices: number[];
- type: ContentType;
- inHeader: boolean;
- indentLinks?: boolean;
+ content: (NavProductPage | NavProductContent)[];
+ layer: number;
+ tree: number[];
}) => {
- const location = useLocation();
- const linkId = 'link' in page ? composeNavLinkId(page.link) : undefined;
const { activePage } = useLayoutContext();
- const treeMatch = indices.every((value, index) => value === activePage.tree[index]?.index);
-
- if ('link' in page) {
- const language = new URLSearchParams(location.search).get('lang');
- const pageActive = treeMatch && page.link === activePage.page.link;
-
- return (
-
- {page.name}
- {page.external ?
: null}
-
- );
- } else {
- return (
-
(
-
-
-
- )),
- },
- ]}
- {...commonAccordionOptions(page, treeMatch ? 0 : undefined, false, inHeader)}
- />
- );
- }
-};
+ const activeTriggerRef = useRef(null);
+ const previousTree = activePage.tree.map(({ index }) => index).slice(0, layer + 2);
-const renderProductContent = (
- content: NavProductContent[],
- type: ContentType,
- inHeader: boolean,
- productIndex: number,
-) =>
- content.map((productContent, productContentIndex) => (
-
-
{productContent.name}
- {productContent.pages.map((page, pageIndex) => (
-
- ))}
-
- ));
-
-const constructProductNavData = (activePageTree: PageTreeNode[], inHeader: boolean): AccordionData[] => {
- const navData: AccordionData[] = Object.entries(productData).map(([productKey, productObj], index) => {
- const product = productObj.nav as NavProduct;
- const apiReferencesId = `${productKey}-api-references`;
-
- return {
- name: product.name,
- icon:
- activePageTree[0]?.page.name === product.name
- ? { name: product.icon.open, css: 'text-orange-600' }
- : { name: product.icon.closed },
- onClick: () => {
- // When a product is clicked, find and scroll to any open accordion element
- if (typeof document !== 'undefined') {
- // Use setTimeout to ensure the DOM has updated after the click and animation has completed
- setTimeout(() => {
- const targetAccordion = window.innerWidth >= 1040 ? 'left-nav' : 'mobile-nav';
- const menuContainer = document.getElementById(targetAccordion);
- const openAccordion: HTMLElement | null = menuContainer
- ? menuContainer.querySelector('[data-state="open"] > button')
- : null;
+ useEffect(() => {
+ if (activeTriggerRef.current) {
+ setTimeout(() => {
+ activeTriggerRef.current?.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ });
+ }, 200);
+ }
+ }, []);
- if (openAccordion) {
- menuContainer?.scrollTo({
- top: openAccordion.offsetTop,
- behavior: 'smooth',
- });
- }
- }, 200);
- }
- },
- content: (
-
- {product.showJumpLink ? (
-
{
- e.preventDefault();
- if (typeof document !== 'undefined') {
- const element = document.getElementById(apiReferencesId);
- if (element) {
- element.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
- }
- }
- }}
- >
- Jump to API references
-
- ) : null}
- {renderProductContent(product.content, 'content', inHeader, index)}
- {product.api.length > 0 ? (
-
0 && 'pl-3')}
+ defaultValue={[`item-${previousTree.join('-')}`]}
+ >
+ {content.map((page, index) => {
+ const hasDeeperLayer = 'pages' in page && page.pages;
+ const isActiveLink = 'link' in page && page.link === activePage.page.link;
+
+ return (
+
+ 0,
+ 'border-orange-600 bg-orange-100 hover:bg-orange-100': isActiveLink,
+ })}
>
- {renderProductContent(product.api, 'api', inHeader, index)}
-
- ) : null}
-
- ),
- };
- });
-
- // Add a Home entry at the start of navData if inHeader is true
- if (inHeader) {
- navData.unshift({
- name: 'Home',
- content: null,
- onClick: () => {
- navigate('/docs');
- },
- interactive: false,
- });
- }
-
- return navData;
+ {hasDeeperLayer ? (
+
+ {page.name}
+
+ ) : (
+ 'link' in page && (
+
+ {page.name}
+
+ )
+ )}
+ {hasDeeperLayer ? (
+
+ ) : null}
+
+ {hasDeeperLayer && (
+
+
+
+ )}
+
+ );
+ })}
+
+ );
};
const LeftSidebar = ({ inHeader = false }: LeftSidebarProps) => {
const { activePage } = useLayoutContext();
- const [hasScrollbar, setHasScrollbar] = useState(false);
- const sidebarRef = useRef(null);
-
- useEffect(() => {
- const checkScrollbar = throttle(() => {
- if (sidebarRef.current) {
- setHasScrollbar(sidebarRef.current.offsetWidth > sidebarRef.current.clientWidth);
- }
- }, 150);
-
- checkScrollbar();
- window.addEventListener('resize', checkScrollbar);
-
- return () => {
- window.removeEventListener('resize', checkScrollbar);
- };
- }, []);
-
- const productNavData = useMemo(() => constructProductNavData(activePage.tree, inHeader), [activePage.tree, inHeader]);
+ const [openProduct, setOpenProduct] = useState(`item-${activePage.tree[0]?.index}`);
return (
-
+
+
+
{
+ setOpenProduct(value);
+ }}
+ >
+ {Object.entries(productData).map(([productKey, productObj], index) => {
+ const isActive = openProduct === `item-${index}`;
+
+ return (
+
+
+
+
+ {productObj.nav.name}
+
+
+
+
+
+
+
+ );
+ })}
+
+
);
};
diff --git a/src/components/Layout/RightSidebar.test.tsx b/src/components/Layout/RightSidebar.test.tsx
index 10f33e66cc..25659bfae9 100644
--- a/src/components/Layout/RightSidebar.test.tsx
+++ b/src/components/Layout/RightSidebar.test.tsx
@@ -13,10 +13,6 @@ jest.mock('@reach/router', () => ({
useLocation: jest.fn(),
}));
-jest.mock('./LanguageSelector', () => ({
- LanguageSelector: jest.fn(() => LanguageSelector
),
-}));
-
const mockUseLayoutContext = useLayoutContext as jest.Mock;
const mockUseLocation = useLocation as jest.Mock;
@@ -62,83 +58,29 @@ describe('RightSidebar', () => {
document.body.innerHTML = '';
});
- it('does not render the LanguageSelector component when activePage.languages is empty', () => {
- render();
- expect(screen.queryByText('LanguageSelector')).not.toBeInTheDocument();
- });
-
- it('renders the LanguageSelector component when activePage.languages is not empty', () => {
- mockUseLayoutContext.mockReturnValue({
- activePage: {
- page: {
- name: 'Test Page',
- link: '/test-path',
- },
- tree: [0],
- languages: ['javascript'],
- },
- products: [['pubsub']],
- });
- render();
- expect(screen.getByText('LanguageSelector')).toBeInTheDocument();
- });
-
it('renders headers from the article', () => {
render();
expect(screen.getByRole('heading', { level: 2, name: 'Header 1' })).toBeInTheDocument();
expect(screen.getByRole('heading', { level: 3, name: 'Header 2' })).toBeInTheDocument();
});
- it('sets active header on click', async () => {
- render();
- const headerLink = await screen.findByRole('link', { name: 'Header 1' });
- fireEvent.click(headerLink);
- expect(headerLink).toHaveClass('text-neutral-1300');
- });
+ it('renders sidebar links for article headers', () => {
+ const { container } = render();
- it('renders external links', () => {
- render();
- expect(screen.getByText('Edit on GitHub')).toBeInTheDocument();
- expect(screen.getByText('Request changes')).toBeInTheDocument();
- });
+ // Verify sidebar links are created with correct IDs
+ const header1Link = container.querySelector('#sidebar-header1');
+ const header2Link = container.querySelector('#sidebar-header2');
- it('renders a textile Github link when the page is textile', () => {
- mockUseLayoutContext.mockReturnValue({
- activePage: {
- page: {
- name: 'Test Page',
- link: '/test-path',
- },
- template: 'textile',
- tree: [0],
- languages: [],
- },
- products: [['pubsub']],
- });
- render();
-
- const githubLink = screen.getByTestId('external-github-link');
- expect(githubLink).toBeInTheDocument();
- expect(githubLink).toHaveAttribute('href', 'https://github.com/ably/docs/blob/main/content/test-path.textile');
+ expect(header1Link).toBeInTheDocument();
+ expect(header2Link).toBeInTheDocument();
+ expect(header1Link).toHaveAttribute('href', '#header1');
+ expect(header2Link).toHaveAttribute('href', '#header2');
});
- it('renders an MDX Github link when the page is MDX', () => {
- mockUseLayoutContext.mockReturnValue({
- activePage: {
- page: {
- name: 'Test Page',
- link: '/test-path',
- },
- template: 'mdx',
- tree: [0],
- languages: [],
- },
- products: [['pubsub']],
- });
+ it('sets active header on click', async () => {
render();
-
- const githubLink = screen.getByTestId('external-github-link');
- expect(githubLink).toBeInTheDocument();
- expect(githubLink).toHaveAttribute('href', 'https://github.com/ably/docs/blob/main/src/pages/docs/test-path.mdx');
+ const headerLink = await screen.findByRole('link', { name: 'Header 1' });
+ fireEvent.click(headerLink);
+ expect(headerLink).toHaveClass('text-neutral-1300');
});
});
diff --git a/src/components/Layout/RightSidebar.tsx b/src/components/Layout/RightSidebar.tsx
index 824f580fc8..70a14a57de 100644
--- a/src/components/Layout/RightSidebar.tsx
+++ b/src/components/Layout/RightSidebar.tsx
@@ -1,256 +1,298 @@
-import { useEffect, useMemo, useRef, useState, useCallback } from 'react';
-import { useLocation, WindowLocation } from '@reach/router';
+import { useEffect, useRef, useState, useCallback } from 'react';
+import { useLocation } from '@reach/router';
import cn from '@ably/ui/core/utils/cn';
-import Icon from '@ably/ui/core/Icon';
-import { IconName } from '@ably/ui/core/Icon/types';
import { componentMaxHeight, HEADER_HEIGHT, HEADER_BOTTOM_MARGIN } from '@ably/ui/core/utils/heights';
-import Tooltip from '@ably/ui/core/Tooltip';
-import { track } from '@ably/ui/core/insights';
-
-import { LanguageSelector } from './LanguageSelector';
-import { useLayoutContext } from 'src/contexts/layout-context';
-import { productData } from 'src/data';
-import { LanguageKey } from 'src/data/languages/types';
-import { languageInfo } from 'src/data/languages';
-import { ActivePage, sidebarAlignmentClasses, sidebarAlignmentStyles } from './utils/nav';
import { INKEEP_ASK_BUTTON_HEIGHT } from './utils/heights';
type SidebarHeader = {
id: string;
type: string;
label: string;
+ stepNumber?: string;
};
-const githubBasePathTextile = 'https://github.com/ably/docs/blob/main/content';
-const githubBasePathMDX = 'https://github.com/ably/docs/blob/main/src/pages/docs';
-const requestBasePath = 'https://github.com/ably/docs/issues/new';
-
-const customGithubPaths = {
- '/how-to/pub-sub': 'https://github.com/ably/docs/blob/main/how-tos/pub-sub/how-to.mdx',
-} as Record;
-
-const externalLinks = (
- activePage: ActivePage,
- location: WindowLocation,
-): { label: string; icon: IconName; link: string; type: string }[] => {
- if (!activePage) {
- return [];
+const getHeaderId = (element: Element) =>
+ element.querySelector('a')?.getAttribute('id') ?? element.querySelector('a')?.getAttribute('name') ?? element.id;
+
+// Paddings for various indentations of headers, 12px apart
+const INDENT_MAP: Record = {
+ H2: 'pl-4',
+ H3: 'pl-7',
+ H4: 'pl-10',
+ H5: 'pl-[52px]', // there is no pl-13
+ H6: 'pl-16',
+} as const;
+
+// Paddings for various indentations of stepped headers, 12px apart with a 1rem buffer for the step indicators
+const STEPPED_INDENT_MAP: Record = {
+ H2: 'pl-8',
+ H3: 'pl-11',
+ H4: 'pl-14',
+ H5: 'pl-[68px]', // there is no pl-17
+ H6: 'pl-20',
+} as const;
+
+// The height of a single line row
+const FALLBACK_HEADER_HEIGHT = 28;
+
+const getElementIndent = (type: string, isStepped: boolean) => {
+ if (isStepped && STEPPED_INDENT_MAP[type]) {
+ return STEPPED_INDENT_MAP[type];
}
- let githubEditPath = '#';
- const githubPathName = location.pathname.replace('docs/', '');
-
- if (customGithubPaths[githubPathName]) {
- githubEditPath = customGithubPaths[githubPathName];
- } else if (activePage.template === 'mdx') {
- githubEditPath =
- githubBasePathMDX + (activePage.page.index ? `${githubPathName}/index.mdx` : `${githubPathName}.mdx`);
- } else {
- githubEditPath =
- githubBasePathTextile + (activePage.page.index ? `${githubPathName}/index.textile` : `${githubPathName}.textile`);
+ if (INDENT_MAP[type]) {
+ return INDENT_MAP[type];
}
- const language = activePage.languages.length > 0 ? activePage.language : null;
- const requestTitle = `Change request for: ${activePage.page.link}`;
- const requestBody = encodeURIComponent(`
- **Page name**: ${activePage.page.name}
- **URL**: [${activePage.page.link}](https://ably.com${activePage.page.link})
- ${language && languageInfo[language] ? `Language: **${languageInfo[language].label}**` : ''}
-
- **Requested change or enhancement**:
-`);
-
- return [
- {
- label: 'Edit on GitHub',
- icon: 'icon-social-github-mono',
- link: githubEditPath,
- type: 'github',
- },
- {
- label: 'Request changes',
- icon: 'icon-gui-hand-raised-outline',
- link: `${requestBasePath}?title=${requestTitle}&body=${requestBody}`,
- type: 'request',
- },
- ];
-};
-
-const llmLinks = (
- activePage: ActivePage,
- language: LanguageKey,
-): { model: string; label: string; icon: IconName; link: string }[] => {
- const prompt = `Tell me more about ${activePage.product ? productData[activePage.product]?.nav.name : 'Ably'}'s '${activePage.page.name}' feature from https://ably.com${activePage.page.link}${language ? ` for ${languageInfo[language]?.label}` : ''}`;
- const gptPath = `https://chatgpt.com/?q=${encodeURIComponent(prompt)}`;
- const claudePath = `https://claude.ai/new?q=${encodeURIComponent(prompt)}`;
-
- return [
- { model: 'gpt', label: 'ChatGPT', icon: 'icon-tech-openai', link: gptPath },
- { model: 'claude', label: 'Claude (must be logged in)', icon: 'icon-tech-claude-mono', link: claudePath },
- ];
-};
-
-const getHeaderId = (element: Element) => {
- const customId = element.querySelector('a')?.getAttribute('id') ?? element.querySelector('a')?.getAttribute('name');
- return customId ?? element.id;
+ return 'pl-0';
};
const RightSidebar = () => {
const location = useLocation();
- const { activePage } = useLayoutContext();
const [headers, setHeaders] = useState([]);
const [activeHeader, setActiveHeader] = useState>({
id: location.hash ? location.hash.slice(1) : '#',
});
+ const [isStepped, setIsStepped] = useState(false);
+ const [sidebarDimensions, setSidebarDimensions] = useState<{
+ indicatorHeights: number[];
+ indicatorPosition: { yOffset: number; height: number };
+ }>({
+ indicatorHeights: [],
+ indicatorPosition: { yOffset: 0, height: 28 },
+ });
const intersectionObserver = useRef(undefined);
const manualSelection = useRef(false);
- const showLanguageSelector = activePage?.languages.length > 0;
- const language = new URLSearchParams(location.search).get('lang') as LanguageKey;
-
- const handleHeaderClick = useCallback((headerId: string) => {
- // Set manual selection flag to prevent intersection observer updates
- manualSelection.current = true;
- setActiveHeader({ id: headerId });
-
- // Reset the flag after scroll animation completes
- setTimeout(() => {
- manualSelection.current = false;
- }, 1000);
+
+ // Extract headers from article element
+ const extractHeaders = useCallback((articleElement: Element): { headers: SidebarHeader[]; hasSteps: boolean } => {
+ const headerElements = articleElement.querySelectorAll('h2, h3, h4, h5, h6');
+ let hasSteps = false;
+
+ const headers = Array.from(headerElements)
+ .filter((element) => element.id && element.textContent)
+ .map((header) => {
+ const stepNumber = header.querySelector('[data-step]')?.getAttribute('data-step');
+ if (stepNumber) {
+ hasSteps = true;
+ }
+
+ return {
+ type: header.tagName,
+ label: header.textContent ?? '',
+ id: getHeaderId(header),
+ stepNumber: stepNumber || undefined,
+ };
+ });
+
+ return { headers, hasSteps };
}, []);
+ // Callback that fires when header intersects with the viewport
+ const handleIntersect = useCallback(
+ (
+ entries: {
+ target: Element;
+ isIntersecting: boolean;
+ boundingClientRect: DOMRect;
+ }[],
+ ) => {
+ // Skip updates if the header was manually selected
+ if (manualSelection.current) {
+ return;
+ }
+
+ // Get all currently intersecting headers
+ const intersectingEntries = entries.filter((entry) => entry.isIntersecting);
+
+ if (intersectingEntries.length > 0) {
+ // Find the entry nearest to the top of the viewport
+ const topEntry = intersectingEntries.reduce((nearest, current) => {
+ return current.boundingClientRect.top < nearest.boundingClientRect.top ? current : nearest;
+ }, intersectingEntries[0]);
+
+ if (topEntry.target.id) {
+ setActiveHeader({
+ id: getHeaderId(topEntry.target),
+ });
+ }
+ }
+ },
+ [],
+ );
+
+ // Setup intersection observer and update headers when page changes
useEffect(() => {
const articleElement = document.querySelector('article');
if (!articleElement) {
return;
}
- const updateHeaders = () => {
- const headerElements = articleElement.querySelectorAll('h2, h3, h6') ?? [];
- const headerData = Array.from(headerElements)
- .filter((element) => element.id && element.textContent)
- .map((header) => {
- const customId =
- header.querySelector('a')?.getAttribute('id') ?? header.querySelector('a')?.getAttribute('name');
-
- return {
- type: header.tagName,
- label: header.textContent ?? '',
- id: customId ?? header.id,
- height: header.getBoundingClientRect().height,
- };
- });
+ // Extract and set headers
+ const { headers: headerData, hasSteps } = extractHeaders(articleElement);
+ setHeaders(headerData);
+ setIsStepped(hasSteps);
- setHeaders(headerData);
+ // Set the first header as active when page changes
+ if (headerData.length > 0) {
+ setActiveHeader({ id: headerData[0].id });
+ }
- // Set the first header as active when page changes
- if (headerData.length > 0) {
- setActiveHeader({ id: headerData[0].id });
- }
+ // Create intersection observer
+ const observer = new IntersectionObserver(handleIntersect, {
+ root: null,
+ threshold: 0,
+ rootMargin: '-64px 0px -80% 0px', // Account for header and focus on top of viewport
+ });
- const handleIntersect = (
- entries: {
- target: Element;
- isIntersecting: boolean;
- boundingClientRect: DOMRect;
- }[],
- ) => {
- // Skip updates if manual selection is active
- if (manualSelection.current) {
- return;
- }
+ // Observe all headers
+ const headerElements = articleElement.querySelectorAll('h2, h3, h4, h5, h6');
+ headerElements.forEach((header) => observer.observe(header));
- // Get all currently intersecting headers
- const intersectingEntries = entries.filter((entry) => entry.isIntersecting);
+ // Store observer reference for cleanup
+ intersectionObserver.current = observer;
- if (intersectingEntries.length > 0) {
- // Find the entry nearest to the top of the viewport
- const topEntry = intersectingEntries.reduce((nearest, current) => {
- return current.boundingClientRect.top < nearest.boundingClientRect.top ? current : nearest;
- }, intersectingEntries[0]);
+ // Cleanup
+ return () => {
+ observer.disconnect();
+ };
+ }, [location.pathname, location.search, extractHeaders, handleIntersect]);
- if (topEntry.target.id) {
- setActiveHeader({
- id: getHeaderId(topEntry.target),
- });
- }
- }
- };
-
- // Create a new observer with configuration focused on the top of the viewport
- intersectionObserver.current = new IntersectionObserver(handleIntersect, {
- root: null,
- // Using a small threshold to detect partial visibility
- threshold: 0,
- // Account for 64px header bar at top and focus on top portion of viewport
- rootMargin: '-64px 0px -80% 0px',
- });
+ // Calculate sidebar dimensions after DOM is ready
+ useEffect(() => {
+ if (headers.length === 0) {
+ return;
+ }
- // Observe each header
- headerElements.forEach((header) => {
- intersectionObserver.current?.observe(header);
+ const calculateDimensions = () => {
+ // Calculate step indicator heights (for stepped mode)
+ const indicatorHeights = isStepped
+ ? headers.map((header) => {
+ const sidebarElement = document.getElementById(`sidebar-${header.id}`);
+ const sidebarElementDimensions = sidebarElement?.getBoundingClientRect();
+ return sidebarElementDimensions?.height ?? FALLBACK_HEADER_HEIGHT;
+ })
+ : [];
+
+ // Calculate indicator position (for non-stepped mode)
+ const sidebarElement = document.getElementById(`sidebar-${activeHeader?.id}`);
+ const sidebarParentElement = sidebarElement?.parentElement;
+ const sidebarElementDimensions = sidebarElement?.getBoundingClientRect();
+
+ const indicatorPosition =
+ sidebarParentElement && sidebarElementDimensions
+ ? {
+ yOffset: Math.abs(sidebarParentElement.getBoundingClientRect().top - sidebarElementDimensions.top),
+ height: sidebarElementDimensions.height,
+ }
+ : { yOffset: 0, height: FALLBACK_HEADER_HEIGHT };
+
+ setSidebarDimensions({
+ indicatorHeights,
+ indicatorPosition,
});
-
- return () => {
- intersectionObserver.current?.disconnect();
- };
};
- updateHeaders();
- }, [location.pathname, location.search]);
+ calculateDimensions();
- const highlightPosition = useMemo(() => {
- const sidebarElement =
- typeof document !== 'undefined' ? document.getElementById(`sidebar-${activeHeader?.id}`) : null;
- const sidebarParentElement = sidebarElement?.parentElement;
- const sidebarElementDimensions = sidebarElement?.getBoundingClientRect();
+ window.addEventListener('resize', calculateDimensions);
- if (!sidebarParentElement || !sidebarElementDimensions) {
- return {
- yOffset: 0,
- height: 21,
- };
- }
+ // Watch each individual sidebar item for dimension changes (catches async updates, i.e. font pop-in)
+ const resizeObservers: ResizeObserver[] = [];
+ headers.forEach((header) => {
+ const sidebarElement = document.getElementById(`sidebar-${header.id}`);
+ if (sidebarElement) {
+ const observer = new ResizeObserver(() => {
+ calculateDimensions();
+ });
+ observer.observe(sidebarElement);
+ resizeObservers.push(observer);
+ }
+ });
- return {
- yOffset: Math.abs(sidebarParentElement.getBoundingClientRect().top - sidebarElementDimensions?.top),
- height: sidebarElementDimensions?.height,
+ return () => {
+ window.removeEventListener('resize', calculateDimensions);
+ resizeObservers.forEach((observer) => observer.disconnect());
};
- }, [activeHeader]);
+ }, [headers, isStepped, activeHeader]);
+
+ const { indicatorHeights, indicatorPosition } = sidebarDimensions;
+
+ const steppedHeader = useCallback(
+ (header: SidebarHeader, index: number) => (
+
+
+
+ {header.stepNumber ?? ''}
+
+
+
+ ),
+ [indicatorHeights, headers.length, activeHeader?.id],
+ );
return (
- {showLanguageSelector ?
: null}
-
+
{headers.length > 0 ? (
<>
-
On this page
-
-
- {/* 18px derives from the 2px width of the grey tracker bar plus the 16px between it and the menu items */}
-
+
On this page
+
+ {isStepped ? (
+
{headers.map((header, index) => steppedHeader(header, index))}
+ ) : (
+
+ )}
+
{headers.map((header, index) => (
@@ -259,70 +301,6 @@ const RightSidebar = () => {
>
) : null}
-
- {externalLinks(activePage, location).map(({ label, icon, link, type }) => (
-
-
-
- ))}
-
-
);
diff --git a/src/components/Layout/utils/nav.ts b/src/components/Layout/utils/nav.ts
index 739b098cdd..23bd91c288 100644
--- a/src/components/Layout/utils/nav.ts
+++ b/src/components/Layout/utils/nav.ts
@@ -1,6 +1,3 @@
-import cn from '@ably/ui/core/utils/cn';
-import { AccordionProps } from '@ably/ui/core/Accordion';
-import { HEADER_HEIGHT, componentMaxHeight } from '@ably/ui/core/utils/heights';
import { ProductData, ProductKey } from 'src/data/types';
import { NavProductContent, NavProductPage, NavProductPages } from 'src/data/nav/types';
import { LanguageKey } from 'src/data/languages/types';
@@ -137,40 +134,5 @@ export const formatNavLink = (link: string) => {
return link.replace(/\/$/, '');
};
-export const commonAccordionOptions = (
- currentPage: NavProductContent | null,
- openIndex: number | undefined,
- topLevel: boolean,
- inHeader: boolean,
-): Omit
=> ({
- icons: { open: { name: 'icon-gui-chevron-up-micro' }, closed: { name: 'icon-gui-chevron-down-micro' } },
- options: {
- autoClose: topLevel,
- headerCSS: cn(
- 'text-neutral-1000 dark:text-neutral-300 md:text-neutral-900 dark:md:text-neutral-400 hover:text-neutral-1100 active:text-neutral-1000 !py-0 pl-0 !mb-0 transition-colors [&_svg]:!w-6 [&_svg]:!h-6 md:[&_svg]:!w-5 md:[&_svg]:!h-5',
- {
- 'my-3': topLevel && inHeader,
- 'h-10 ui-text-label1 !font-bold md:ui-text-label4 px-4': topLevel,
- 'min-h-[1.625em] md:min-h-[1.375em] ui-text-label2 !font-semibold md:ui-text-label4': !topLevel,
- },
- ),
- selectedHeaderCSS: '!text-neutral-1300 mb-2',
- contentCSS: '[&>div]:pb-0',
- rowIconSize: '20px',
- defaultOpenIndexes: !inHeader && openIndex !== undefined ? [openIndex] : [],
- hideBorders: true,
- fullyOpen: !topLevel && currentPage?.expand,
- },
-});
-
-export const sidebarAlignmentClasses = 'absolute md:sticky w-60 md:pb-32 pt-6';
-
-export const sidebarAlignmentStyles: React.CSSProperties = {
- top: HEADER_HEIGHT,
- height: componentMaxHeight(HEADER_HEIGHT),
-};
-
-export const composeNavLinkId = (link: string) => `nav-link-${formatNavLink(link).replaceAll('/', '-')}`;
-
export const hierarchicalKey = (id: string, depth: number, tree?: PageTreeNode[]) =>
[...(tree ? tree.slice(0, depth).map((node) => node.index) : []), id].join('-');
diff --git a/src/components/SearchBar/InkeepSearchBar.tsx b/src/components/SearchBar/InkeepSearchBar.tsx
index fe228d382d..ad480272af 100644
--- a/src/components/SearchBar/InkeepSearchBar.tsx
+++ b/src/components/SearchBar/InkeepSearchBar.tsx
@@ -1,11 +1,9 @@
-import { CSSProperties } from 'react';
+import { CSSProperties, forwardRef } from 'react';
-export const InkeepSearchBar = ({
- className,
- extraInputStyle,
-}: {
- className: string;
- extraInputStyle: CSSProperties;
-}) => {
- return ;
-};
+export const InkeepSearchBar = forwardRef(
+ ({ className, extraInputStyle = {} }, ref) => {
+ return ;
+ },
+);
+
+InkeepSearchBar.displayName = 'InkeepSearchBar';
diff --git a/src/pages/docs/test-stepped-sidebar.mdx b/src/pages/docs/test-stepped-sidebar.mdx
new file mode 100644
index 0000000000..367e5e813f
--- /dev/null
+++ b/src/pages/docs/test-stepped-sidebar.mdx
@@ -0,0 +1,156 @@
+---
+title: Getting Started with Chat (Stepped Tutorial)
+meta_description: "A step-by-step guide to setting up and using Ably Chat."
+---
+
+This is a test page to demonstrate the stepped sidebar functionality. Follow these numbered steps to get started with Ably Chat.
+
+## Introduction
+
+Before diving into the setup process, let's understand what Ably Chat provides and why you might want to use it.
+
+## Create a client
+
+
+First, you need to create an Ably Chat client instance. This client will be used to interact with the Ably Chat API.
+
+```javascript
+import * as Ably from 'ably';
+import { ChatClient } from '@ably/chat';
+
+const realtimeClient = new Ably.Realtime({
+ key: 'your-api-key',
+ clientId: 'unique-client-id'
+});
+
+const chatClient = new ChatClient(realtimeClient);
+```
+
+The client ID should be a unique identifier for the user. This is used to track which messages belong to which users.
+
+## Connect to a room
+
+Once you have a client, you can connect to a chat room. Rooms are the main way to organize conversations in Ably Chat.
+
+```javascript
+const room = await chatClient.rooms.get('my-chat-room', {
+ presence: { enableEvents: true },
+ typing: { heartbeatThrottleMs: 5000 }
+});
+
+await room.attach();
+```
+
+The room ID can be any string that uniquely identifies your room. If the room doesn't exist, it will be created automatically.
+
+### Room configuration options Room configuration options Room configuration options Room configuration options
+
+You can configure various features when creating a room:
+
+- **Presence**: Track who is currently in the room
+- **Typing indicators**: Show when users are typing
+- **Occupancy**: Track the number of users in the room
+- **Reactions**: Enable emoji reactions to messages
+
+## Send messages
+
+Now that you're connected to a room, you can start sending messages:
+
+```javascript
+await room.messages.send({ text: 'Hello, world!' });
+```
+
+You can also send messages with additional metadata:
+
+```javascript
+await room.messages.send({
+ text: 'Hello!',
+ metadata: {
+ type: 'greeting',
+ timestamp: Date.now()
+ }
+});
+```
+
+## Subscribe to messages
+
+To receive messages from other users, you need to subscribe to the room's message events:
+
+```javascript
+const { unsubscribe } = room.messages.subscribe((message) => {
+ console.log('Received message:', message.text);
+ console.log('From:', message.clientId);
+ console.log('At:', message.timestamp);
+});
+
+// Later, when you want to stop receiving messages
+unsubscribe();
+```
+
+The subscription will continue to receive messages until you call the `unsubscribe` function.
+
+## Add presence tracking
+
+Presence tracking allows you to see who is currently in the room and track their status:
+
+```javascript
+// Enter the room's presence set
+await room.presence.enter({ status: 'online' });
+
+// Subscribe to presence updates
+room.presence.subscribe((presenceEvent) => {
+ console.log('Presence update:', presenceEvent.action);
+ console.log('User:', presenceEvent.clientId);
+ console.log('Data:', presenceEvent.data);
+});
+
+// Update your presence data
+await room.presence.update({ status: 'away' });
+
+// Leave the room's presence set
+await room.presence.leave();
+```
+
+## Additional features
+
+Beyond the core functionality, Ably Chat provides several additional features to enhance your chat experience.
+
+### Typing indicators
+
+Show when users are currently typing in the room:
+
+```javascript
+// Start typing
+await room.typing.start();
+
+// Subscribe to typing events
+room.typing.subscribe((event) => {
+ console.log(`${event.clientId} is typing`);
+});
+
+// Stop typing
+await room.typing.stop();
+```
+
+### Room reactions
+
+Allow users to send quick emoji reactions:
+
+```javascript
+// Send a reaction
+await room.reactions.send({ type: 'like' });
+
+// Subscribe to reactions
+room.reactions.subscribe((reaction) => {
+ console.log(`${reaction.clientId} sent: ${reaction.type}`);
+});
+```
+
+## Next steps
+
+Now that you've completed the basic setup, you can explore more advanced features:
+
+- Learn about [message history and pagination](/docs/chat/rooms/messages)
+- Implement [occupancy tracking](/docs/chat/rooms/occupancy)
+- Set up [webhooks for chat events](/docs/chat/webhooks)
+- Configure [push notifications](/docs/chat/push-notifications)
diff --git a/src/types/non-javascript-assets/svg-loading.d.ts b/src/types/non-javascript-assets/svg-loading.d.ts
index dda274a423..cdb2b1a9a2 100644
--- a/src/types/non-javascript-assets/svg-loading.d.ts
+++ b/src/types/non-javascript-assets/svg-loading.d.ts
@@ -1,4 +1,4 @@
declare module '*.svg' {
- const content: unknown;
+ const content: string;
export default content;
}
diff --git a/tailwind.config.js b/tailwind.config.js
index 6ce2650021..66041d7af5 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -15,6 +15,22 @@ module.exports = extendConfig((ablyUIConfig) => ({
gridTemplateColumns: {
'header-layout': '173px minmax(200px, 400px) 1fr',
},
+ keyframes: {
+ ...ablyUIConfig.theme.extend.keyframes,
+ 'accordion-down': {
+ from: { height: '0' },
+ to: { height: 'var(--radix-accordion-content-height)' },
+ },
+ 'accordion-up': {
+ from: { height: 'var(--radix-accordion-content-height)' },
+ to: { height: '0' },
+ },
+ },
+ animation: {
+ ...ablyUIConfig.theme.extend.animation,
+ 'accordion-down': 'accordion-down 0.2s ease-out',
+ 'accordion-up': 'accordion-up 0.2s ease-out',
+ },
},
},
}));
diff --git a/yarn.lock b/yarn.lock
index 6c0e6d9821..0ff086ad80 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3088,6 +3088,21 @@
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
+"@radix-ui/react-accordion@^1.2.12":
+ version "1.2.12"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz#1fd70d4ef36018012b9e03324ff186de7a29c13f"
+ integrity sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==
+ dependencies:
+ "@radix-ui/primitive" "1.1.3"
+ "@radix-ui/react-collapsible" "1.1.12"
+ "@radix-ui/react-collection" "1.1.7"
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-context" "1.1.2"
+ "@radix-ui/react-direction" "1.1.1"
+ "@radix-ui/react-id" "1.1.1"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-use-controllable-state" "1.2.2"
+
"@radix-ui/react-arrow@1.1.4":
version "1.1.4"
resolved "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz"
@@ -3116,7 +3131,7 @@
"@radix-ui/react-use-controllable-state" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
-"@radix-ui/react-collapsible@^1.1.12":
+"@radix-ui/react-collapsible@1.1.12", "@radix-ui/react-collapsible@^1.1.12":
version "1.1.12"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz#e2cc69a4490a2920f97c3c3150b0bf21281e3c49"
integrity sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==
@@ -3160,6 +3175,16 @@
"@radix-ui/react-primitive" "2.1.0"
"@radix-ui/react-slot" "1.2.0"
+"@radix-ui/react-collection@1.1.7":
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz#d05c25ca9ac4695cc19ba91f42f686e3ea2d9aec"
+ integrity sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==
+ dependencies:
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-context" "1.1.2"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-slot" "1.2.3"
+
"@radix-ui/react-compose-refs@1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz"
@@ -3233,11 +3258,29 @@
"@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-use-escape-keydown" "1.1.1"
+"@radix-ui/react-dropdown-menu@^2.1.16":
+ version "2.1.16"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz#5ee045c62bad8122347981c479d92b1ff24c7254"
+ integrity sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==
+ dependencies:
+ "@radix-ui/primitive" "1.1.3"
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-context" "1.1.2"
+ "@radix-ui/react-id" "1.1.1"
+ "@radix-ui/react-menu" "2.1.16"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-use-controllable-state" "1.2.2"
+
"@radix-ui/react-focus-guards@1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz"
integrity sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==
+"@radix-ui/react-focus-guards@1.1.3":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz#2a5669e464ad5fde9f86d22f7fdc17781a4dfa7f"
+ integrity sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==
+
"@radix-ui/react-focus-scope@1.1.4":
version "1.1.4"
resolved "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.4.tgz"
@@ -3247,6 +3290,15 @@
"@radix-ui/react-primitive" "2.1.0"
"@radix-ui/react-use-callback-ref" "1.1.1"
+"@radix-ui/react-focus-scope@1.1.7":
+ version "1.1.7"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz#dfe76fc103537d80bf42723a183773fd07bfb58d"
+ integrity sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==
+ dependencies:
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-use-callback-ref" "1.1.1"
+
"@radix-ui/react-id@1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz"
@@ -3261,6 +3313,30 @@
dependencies:
"@radix-ui/react-use-layout-effect" "1.1.1"
+"@radix-ui/react-menu@2.1.16":
+ version "2.1.16"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.16.tgz#528a5a973c3a7413d3d49eb9ccd229aa52402911"
+ integrity sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==
+ dependencies:
+ "@radix-ui/primitive" "1.1.3"
+ "@radix-ui/react-collection" "1.1.7"
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-context" "1.1.2"
+ "@radix-ui/react-direction" "1.1.1"
+ "@radix-ui/react-dismissable-layer" "1.1.11"
+ "@radix-ui/react-focus-guards" "1.1.3"
+ "@radix-ui/react-focus-scope" "1.1.7"
+ "@radix-ui/react-id" "1.1.1"
+ "@radix-ui/react-popper" "1.2.8"
+ "@radix-ui/react-portal" "1.1.9"
+ "@radix-ui/react-presence" "1.1.5"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-roving-focus" "1.1.11"
+ "@radix-ui/react-slot" "1.2.3"
+ "@radix-ui/react-use-callback-ref" "1.1.1"
+ aria-hidden "^1.2.4"
+ react-remove-scroll "^2.6.3"
+
"@radix-ui/react-navigation-menu@^1.2.4":
version "1.2.5"
resolved "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.5.tgz"
@@ -3396,6 +3472,21 @@
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
+"@radix-ui/react-roving-focus@1.1.11":
+ version "1.1.11"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz#ef54384b7361afc6480dcf9907ef2fedb5080fd9"
+ integrity sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==
+ dependencies:
+ "@radix-ui/primitive" "1.1.3"
+ "@radix-ui/react-collection" "1.1.7"
+ "@radix-ui/react-compose-refs" "1.1.2"
+ "@radix-ui/react-context" "1.1.2"
+ "@radix-ui/react-direction" "1.1.1"
+ "@radix-ui/react-id" "1.1.1"
+ "@radix-ui/react-primitive" "2.1.3"
+ "@radix-ui/react-use-callback-ref" "1.1.1"
+ "@radix-ui/react-use-controllable-state" "1.2.2"
+
"@radix-ui/react-select@^2.2.2":
version "2.2.2"
resolved "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.2.tgz"