Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
85 changes: 85 additions & 0 deletions sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -2209,3 +2209,88 @@ module.exports = {
'tctl-v1/workflow',
],
};

// ---------------------------------------------------------------------------
// Secondary navigation bar (proof of concept)
//
// Moves four sections out of the single `documentation` sidebar and gives each
// one its own sidebar, so a reader inside a section sees only that section's
// tree. src/components/SecondaryNav renders the tabs and works out which
// section the reader is in from the URL.
//
// Every other section stays in `documentation` and keeps today's sidebar.
// To undo the proof of concept, delete this block and the SecondaryNav import
// in src/theme/Navbar/index.js.
// ---------------------------------------------------------------------------

const NAV_TAB_SECTIONS = [
{ sidebar: 'develop', category: 'Develop' },
{ sidebar: 'cloud', category: 'Temporal Cloud' },
{ sidebar: 'guides', category: 'Guides' },
// The AI tab carries the Durable AI tree plus the standalone "Develop with
// AI" page, which lives at the top level of the documentation sidebar today.
{ sidebar: 'ai', category: 'Durable AI', extraDocs: ['with-ai'] },
];

// The tab label already names the section, so drop the wrapping category and
// promote its overview page to the first sidebar entry.
function flattenSection(category) {
const overview = category.link && category.link.type === 'doc' ? [category.link.id] : [];
return [...overview, ...category.items];
}

(function buildSectionSidebars(sidebars) {
const mainSidebar = sidebars.documentation;
const moved = new Set();

for (const section of NAV_TAB_SECTIONS) {
const category = mainSidebar.find((item) => item && item.type === 'category' && item.label === section.category);

if (!category) {
throw new Error(
`Secondary nav: no "${section.category}" category found in the documentation sidebar. ` +
`Update NAV_TAB_SECTIONS in sidebars.js if the section was renamed or removed.`
);
}

const extraDocs = section.extraDocs || [];
moved.add(category);
extraDocs.forEach((id) => moved.add(id));

sidebars[section.sidebar] = [...flattenSection(category), ...extraDocs];
}

sidebars.documentation = mainSidebar.filter((item) => !moved.has(item));
})(module.exports);

// Develop splits one level further: each SDK gets its own sidebar, so a reader
// on /develop/go sees the Go tree instead of all eight. src/components/SdkNav
// renders the SDKs as a third row, shown only inside Develop. What is left in
// `develop` is the language-agnostic material.
(function buildSdkSidebars(sidebars) {
const developSidebar = sidebars.develop;
const moved = new Set();
const sdkLinks = [];

for (const { id } of SDKS) {
const category = developSidebar.find(
(item) => item && item.type === 'category' && item.link && item.link.id === `develop/${id}/index`
);

if (!category) {
throw new Error(`Secondary nav: no sidebar category found for the ${id} SDK (expected link develop/${id}/index).`);
}

moved.add(category);
sidebars[`develop-${id}`] = flattenSection(category);

// The SDKs stay listed in Develop as plain links. A doc can belong to only
// one sidebar, and its pages now live in the per-SDK sidebar above.
sdkLinks.push({ type: 'link', label: category.label, href: `/develop/${id}` });
}

const remaining = developSidebar.filter((item) => !moved.has(item));
const afterOverview = remaining.indexOf('develop/index') + 1;

sidebars.develop = [...remaining.slice(0, afterOverview), ...sdkLinks, ...remaining.slice(afterOverview)];
})(module.exports);
17 changes: 17 additions & 0 deletions src/components/SdkNav/activeSdk.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SDKS } from '@site/src/constants/sdks';

export const DEVELOP_SECTION = '/develop';

export function normalizePath(pathname) {
return pathname.replace(/\/+$/, '') || '/';
}

// The SDK row and the section tabs are mutually exclusive, so both rows read
// this to decide which one renders.
export function findActiveSdk(pathname) {
const path = normalizePath(pathname);
return SDKS.find(({ id }) => {
const base = `${DEVELOP_SECTION}/${id}`;
return path === base || path.startsWith(`${base}/`);
});
}
45 changes: 45 additions & 0 deletions src/components/SdkNav/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from 'react';
import Link from '@docusaurus/Link';
import { useLocation } from '@docusaurus/router';
import { SDKS } from '@site/src/constants/sdks';
import { StylePickers, useNavStyle } from '@site/src/components/navStyle';
import styles from '@site/src/components/SecondaryNav/styles.module.css';
import { DEVELOP_SECTION, findActiveSdk } from './activeSdk';
import sdkStyles from './styles.module.css';

export default function SdkNav() {
const activeSdk = findActiveSdk(useLocation().pathname);
const navStyle = useNavStyle();

if (!activeSdk) return null;

const tabs = SDKS.map(({ id, label, icon: Icon }) => {
const active = id === activeSdk.id;
return (
<Link
key={id}
to={`${DEVELOP_SECTION}/${id}`}
className={active ? `${styles.tab} ${styles.tabActive}` : styles.tab}
aria-current={active ? 'page' : undefined}
>
<Icon className={sdkStyles.icon} aria-hidden="true" />
{label}
</Link>
);
});

return (
<nav
className={`${styles.secondaryNav} ${styles[navStyle.variant]} ${styles[navStyle.surface]}`}
aria-label="Temporal SDKs"
>
<div className={styles.inner}>
<div className={styles.tabs}>
{navStyle.variant === 'segmented' ? <div className={styles.group}>{tabs}</div> : tabs}
</div>

<StylePickers styles={styles} {...navStyle} />
</div>
</nav>
);
}
5 changes: 5 additions & 0 deletions src/components/SdkNav/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.icon {
width: 1.125rem;
height: 1.125rem;
flex-shrink: 0;
}
54 changes: 54 additions & 0 deletions src/components/SecondaryNav/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import Link from '@docusaurus/Link';
import { useLocation } from '@docusaurus/router';
import { findActiveSdk } from '@site/src/components/SdkNav/activeSdk';
import { StylePickers, useNavStyle } from '@site/src/components/navStyle';
import styles from './styles.module.css';

const TABS = [
{ label: 'Develop', to: '/develop', match: ['/develop'] },
{ label: 'Cloud', to: '/cloud', match: ['/cloud'] },
{ label: 'Guides', to: '/guides', match: ['/guides'] },
{ label: 'AI agents', to: '/ai', match: ['/ai', '/with-ai'] },
];

function isActive(pathname, prefixes) {
const path = pathname.replace(/\/+$/, '') || '/';
return prefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
}

export default function SecondaryNav() {
const { pathname } = useLocation();
const navStyle = useNavStyle();

// Inside an SDK guide the SDK row takes this row's place.
if (findActiveSdk(pathname)) return null;

const current = TABS.find((tab) => isActive(pathname, tab.match));

const tabs = TABS.map((tab) => (
<Link
key={tab.to}
to={tab.to}
className={tab === current ? `${styles.tab} ${styles.tabActive}` : styles.tab}
aria-current={tab === current ? 'page' : undefined}
>
{tab.label}
</Link>
));

return (
<nav
className={`${styles.secondaryNav} ${styles[navStyle.variant]} ${styles[navStyle.surface]}`}
aria-label="Documentation sections"
>
<div className={styles.inner}>
<div className={styles.tabs}>
{navStyle.variant === 'segmented' ? <div className={styles.group}>{tabs}</div> : tabs}
</div>

<StylePickers styles={styles} {...navStyle} />
</div>
</nav>
);
}
Loading