diff --git a/src/components/LibraryLayout.tsx b/src/components/LibraryLayout.tsx
index 40eba3fb7..ddcc0cfd7 100644
--- a/src/components/LibraryLayout.tsx
+++ b/src/components/LibraryLayout.tsx
@@ -44,8 +44,11 @@ import {
getLibraryLayoutVersion,
getLibraryTabLinkOptions,
getMenuGroupInitialOpenState,
- isChartsCatalogTarget,
} from './library-layout-navigation'
+import {
+ customToolTabs,
+ isCustomToolTarget,
+} from '~/libraries/custom-tool-tabs'
// Number of days a doc page is flagged as "New"/"Updated" in the sidebar.
const RECENCY_WINDOW_DAYS = 7
@@ -675,18 +678,6 @@ const useMenuConfig = ({
const currentFramework = useCurrentFramework(frameworks)
const statsAvailable =
getLibrary(libraryId as LibraryId).statsAvailable !== false
- const chartsExamplesMenuItems: MenuItem['children'] = [
- {
- label: 'Examples',
- to: '/charts/catalog',
- tab: 'home',
- },
- {
- label: 'Examples',
- to: '/charts/catalog',
- tab: 'examples',
- },
- ]
const localMenu: MenuItem = {
label: 'Menu',
@@ -695,7 +686,6 @@ const useMenuConfig = ({
label: 'Home',
to: '..',
},
- ...(libraryId === 'charts' ? chartsExamplesMenuItems : []),
{
label: 'Blog',
to: '/$libraryId/$version/docs/blog',
@@ -830,17 +820,7 @@ export function LibraryLayout({
const menuConfig = useMenuConfig({ config, frameworks, repo, libraryId })
const libraryGroupColor = categoryTextColor[categoryOf(libraryId)]
- const isExample = matches.some(
- (d) =>
- d.pathname.includes('/examples/') ||
- d.routeId.startsWith('/_library/charts/catalog'),
- )
-
- const isThemeEditor = matches.some((d) =>
- d.routeId.startsWith('/_library/highlight/$version/theme-editor'),
- )
-
- const isNpmStats = matches.some((d) => d.pathname.includes('/docs/npm-stats'))
+ const docsTab = matches.find((d) => d.staticData.docsTab)?.staticData.docsTab
// The library blog already lists posts, so the "Latest Posts" rail widget is
// redundant there — hide it on the blog while keeping it on other docs pages.
@@ -872,60 +852,17 @@ export function LibraryLayout({
}, [closeMobileMenu, mobileMenuOpen])
const tabbedMenuConfig = React.useMemo(() => {
- const tabs = getTabbedMenuConfig(menuConfig)
-
- if (libraryId === 'charts') {
- return tabs.map((tab) =>
- tab.id === 'examples'
- ? {
- ...tab,
- firstItem: {
- label: 'Examples',
- to: '/charts/catalog',
- tab: 'examples',
- },
- }
- : tab,
- )
- }
-
- if (libraryId === 'highlight') {
- return [
- ...tabs,
- {
- id: 'theme-editor' as const,
- label: 'Theme Editor',
- groups: [],
- firstItem: {
- label: 'Theme Editor',
- to: '/highlight/$version/theme-editor',
- tab: 'theme-editor' as const,
- },
- },
- ]
- }
-
- return tabs
+ return getTabbedMenuConfig(menuConfig, customToolTabs[libraryId])
}, [libraryId, menuConfig])
const activeTabId = React.useMemo(() => {
- if (isThemeEditor) {
- return 'theme-editor' as const
- }
-
return getActiveDocsNavTabId({
- isExample,
+ tabIdOverride: docsTab?.id,
menuConfig,
pathname: lastMatch.pathname,
relativePathname,
})
- }, [
- isExample,
- isThemeEditor,
- lastMatch.pathname,
- menuConfig,
- relativePathname,
- ])
+ }, [docsTab?.id, lastMatch.pathname, menuConfig, relativePathname])
const visibleMenuConfig = React.useMemo(() => {
return (
@@ -964,11 +901,12 @@ export function LibraryLayout({
const groupInitialOpenState = React.useMemo(() => {
return getMenuGroupInitialOpenState(
+ libraryId,
visibleMenuConfig,
_splat,
lastMatch.pathname,
)
- }, [lastMatch.pathname, visibleMenuConfig, _splat])
+ }, [libraryId, lastMatch.pathname, visibleMenuConfig, _splat])
const [openGroups, setOpenGroups] = React.useState(groupInitialOpenState)
@@ -1042,7 +980,7 @@ export function LibraryLayout({
? ({ libraryId, version } as never)
: undefined
const isHomeLink = child.to === '..'
- const isChartsExamplesLink = isChartsCatalogTarget(child.to)
+ const isCustomToolLink = isCustomToolTarget(libraryId, child.to)
const frameworkDocsTarget = getFrameworkDocsLinkTarget(child.to)
const recency = getDocRecency(child.addedAt, child.updatedAt)
@@ -1130,14 +1068,12 @@ export function LibraryLayout({
) : (
diff --git a/src/components/library-layout-navigation.ts b/src/components/library-layout-navigation.ts
index 3adbfc764..f5c2fe3cf 100644
--- a/src/components/library-layout-navigation.ts
+++ b/src/components/library-layout-navigation.ts
@@ -1,3 +1,5 @@
+import { isCustomToolTarget } from '~/libraries/custom-tool-tabs'
+import type { LibraryId } from '~/libraries/ids'
import type { MenuItem } from '~/utils/config'
export function isChartsCatalogTarget(to: string) {
@@ -22,7 +24,7 @@ export function getLibraryTabLinkOptions({
version,
to,
}: {
- libraryId: string
+ libraryId: LibraryId
version: string
to: string
}) {
@@ -34,7 +36,7 @@ export function getLibraryTabLinkOptions({
return {
from:
- isHomeTarget || isChartsCatalogTarget(to)
+ isHomeTarget || isCustomToolTarget(libraryId, to)
? undefined
: '/$libraryId/$version/docs',
to: isHomeTarget ? `/${libraryId}/${version}` : to,
@@ -50,6 +52,7 @@ function normalizeMenuPath(path: string) {
}
export function isMenuTargetActive(
+ libraryId: LibraryId,
to: string,
relativePathname: string | undefined,
pathname: string,
@@ -59,12 +62,13 @@ export function isMenuTargetActive(
}
return (
- isChartsCatalogTarget(to) &&
+ isCustomToolTarget(libraryId, to) &&
normalizeMenuPath(to) === normalizeMenuPath(pathname)
)
}
export function getMenuGroupInitialOpenState(
+ libraryId: LibraryId,
groups: MenuItem[],
relativePathname: string | undefined,
pathname: string,
@@ -73,7 +77,7 @@ export function getMenuGroupInitialOpenState(
groups.forEach((group, index) => {
const isChildActive = group.children.some((child) =>
- isMenuTargetActive(child.to, relativePathname, pathname),
+ isMenuTargetActive(libraryId, child.to, relativePathname, pathname),
)
const key = `${index}:${String(group.label)}`
diff --git a/src/libraries/custom-tool-tabs.ts b/src/libraries/custom-tool-tabs.ts
new file mode 100644
index 000000000..fdd82f042
--- /dev/null
+++ b/src/libraries/custom-tool-tabs.ts
@@ -0,0 +1,29 @@
+import type { DocsNavTabId } from '~/utils/docsNavTabs'
+import type { LibraryId } from './ids'
+import type { FileRouteTypes } from '~/routeTree.gen'
+
+// A "custom tool tab" is a page bolted onto a library's docs nav bar that isn't sourced from the library's docs/config.json
+export type CustomToolTabId = DocsNavTabId
+
+export type CustomToolTab = {
+ id: CustomToolTabId
+ label: string
+ to: FileRouteTypes['fullPaths']
+}
+
+export const customToolTabs: Partial> = {
+ charts: [{ id: 'examples', label: 'Examples', to: '/charts/catalog' }],
+ highlight: [
+ {
+ id: 'theme-editor',
+ label: 'Theme Editor',
+ to: '/highlight/$version/theme-editor',
+ },
+ ],
+}
+
+export function isCustomToolTarget(libraryId: LibraryId, to: string) {
+ return (customToolTabs[libraryId] ?? []).some(
+ (tab) => to === tab.to || to.startsWith(`${tab.to}/`),
+ )
+}
diff --git a/src/router.tsx b/src/router.tsx
index d62b954ba..0362d4998 100644
--- a/src/router.tsx
+++ b/src/router.tsx
@@ -10,6 +10,8 @@ import {
consumeStaleAppRecovery,
} from './utils/stale-app-reload'
import { redactByokRequestHeaders } from './utils/sentry-redaction'
+import type { DocsNavTabId } from './utils/docsNavTabs'
+import type { CustomToolTabId } from './libraries/custom-tool-tabs'
if (typeof document !== 'undefined') {
installStaleAppReloadHandlers()
@@ -97,6 +99,13 @@ declare module '@tanstack/react-router' {
showNavbar?: boolean
includeSearchInCanonical?: boolean
ownsCanonicalLink?: boolean
+
+ // allows for the setting of custom routes to show up in the page tab
+ docsTab?: {
+ id: DocsNavTabId | CustomToolTabId
+ // config to disable clamping of page
+ fullBleed?: boolean
+ }
}
}
diff --git a/src/routes/_library/$libraryId/$version.docs.framework.$framework.examples.$.tsx b/src/routes/_library/$libraryId/$version.docs.framework.$framework.examples.$.tsx
index c00b040ab..39cd83e03 100644
--- a/src/routes/_library/$libraryId/$version.docs.framework.$framework.examples.$.tsx
+++ b/src/routes/_library/$libraryId/$version.docs.framework.$framework.examples.$.tsx
@@ -122,6 +122,7 @@ export const Route = createFileRoute(
// /latest equivalent), so the root route must not emit its own.
staticData: {
ownsCanonicalLink: true,
+ docsTab: { id: 'examples', fullBleed: true },
},
validateSearch: v.object({
path: v.optional(v.string()),
diff --git a/src/routes/_library/$libraryId/$version.docs.npm-stats.tsx b/src/routes/_library/$libraryId/$version.docs.npm-stats.tsx
index 4df97a637..108698a58 100644
--- a/src/routes/_library/$libraryId/$version.docs.npm-stats.tsx
+++ b/src/routes/_library/$libraryId/$version.docs.npm-stats.tsx
@@ -142,6 +142,7 @@ export const Route = createFileRoute(
component: RouteComponent,
staticData: {
includeSearchInCanonical: true,
+ docsTab: { id: 'home', fullBleed: true },
},
})
diff --git a/src/routes/_library/charts.catalog.tsx b/src/routes/_library/charts.catalog.tsx
index 051a8c099..040b4e305 100644
--- a/src/routes/_library/charts.catalog.tsx
+++ b/src/routes/_library/charts.catalog.tsx
@@ -10,6 +10,9 @@ export const Route = createFileRoute('/_library/charts/catalog')({
version: 'latest',
}),
component: ChartsCatalogLayout,
+ staticData: {
+ docsTab: { id: 'examples', fullBleed: true },
+ },
})
function ChartsCatalogLayout() {
diff --git a/src/routes/_library/highlight.$version.theme-editor.tsx b/src/routes/_library/highlight.$version.theme-editor.tsx
index 141c9a046..dad771ba7 100644
--- a/src/routes/_library/highlight.$version.theme-editor.tsx
+++ b/src/routes/_library/highlight.$version.theme-editor.tsx
@@ -25,5 +25,6 @@ export const Route = createFileRoute(
}
},
component: ThemeEditorPage,
+ staticData: { docsTab: { id: 'theme-editor', fullBleed: true } },
ssr: false,
})
diff --git a/src/utils/docsNavTabs.ts b/src/utils/docsNavTabs.ts
index fa68bb317..480fb9d48 100644
--- a/src/utils/docsNavTabs.ts
+++ b/src/utils/docsNavTabs.ts
@@ -114,8 +114,11 @@ function isDocsTabTarget(to: string) {
return true
}
-export function getTabbedMenuConfig(menuConfig: MenuItem[]) {
- return docsNavTabs
+export function getTabbedMenuConfig(
+ menuConfig: MenuItem[],
+ tools: Array<{ id: DocsNavTabId; label: string; to: string }> = [],
+) {
+ const tabs = docsNavTabs
.map((tab) => {
const groups = menuConfig
.map((group) => {
@@ -148,6 +151,17 @@ export function getTabbedMenuConfig(menuConfig: MenuItem[]) {
}
})
.filter((tab) => tab.groups.length)
+
+ for (const tool of tools) {
+ const existing = tabs.find((tab) => tab.id === tool.id)
+ const firstItem = { label: tool.label, to: tool.to }
+ if (existing) {
+ existing.firstItem = firstItem
+ } else {
+ tabs.push({ id: tool.id, label: tool.label, groups: [], firstItem })
+ }
+ }
+ return tabs
}
// Matches a menu child's `to` against the current pathname, handling the
@@ -184,19 +198,19 @@ function isChildPathMatch({
return false
}
-export function getActiveDocsNavTabId({
- isExample,
+export function getActiveDocsNavTabId({
+ tabIdOverride,
menuConfig,
pathname,
relativePathname,
}: {
- isExample: boolean
+ tabIdOverride?: TTabIdOverride
menuConfig: MenuItem[]
pathname: string
relativePathname: string
}) {
- if (isExample) {
- return 'examples'
+ if (tabIdOverride) {
+ return tabIdOverride
}
const activeGroup = menuConfig.find((group) =>
diff --git a/tests/charts-sidebar-navigation.test.ts b/tests/charts-sidebar-navigation.test.ts
index bde42c1e6..9d0274c2c 100644
--- a/tests/charts-sidebar-navigation.test.ts
+++ b/tests/charts-sidebar-navigation.test.ts
@@ -1,3 +1,8 @@
+import {
+ getTabbedMenuConfig,
+ getActiveDocsNavTabId,
+} from '../src/utils/docsNavTabs'
+import { customToolTabs } from '../src/libraries/custom-tool-tabs'
import assert from 'node:assert/strict'
import {
getLibraryLayoutVersion,
@@ -83,6 +88,7 @@ const groups = [
assert.deepEqual(
getMenuGroupInitialOpenState(
+ 'charts',
groups,
undefined,
'/charts/catalog/charts/01-line-gaps/',
@@ -92,13 +98,11 @@ assert.deepEqual(
)
assert.deepEqual(
- getMenuGroupInitialOpenState(groups, undefined, '/charts/catalog'),
+ getMenuGroupInitialOpenState('charts', groups, undefined, '/charts/catalog'),
{ '0:Individual Charts': false },
'an inactive catalog group keeps its configured collapsed state',
)
-console.log('charts sidebar navigation tests passed')
-
assert.deepEqual(
getLibraryTabLinkOptions({
libraryId: 'highlight',
@@ -112,3 +116,53 @@ assert.deepEqual(
},
'the theme editor uses its own versioned route without a docs route prefix',
)
+
+const catalogGroups = groups.map((group) => ({
+ ...group,
+ children: group.children.map((child) => ({
+ ...child,
+ label: 'Example chart',
+ })),
+}))
+const chartsTabs = getTabbedMenuConfig(catalogGroups, customToolTabs.charts)
+assert.equal(chartsTabs.filter((tab) => tab.label === 'Examples').length, 1)
+const examplesTab = chartsTabs.find((tab) => tab.id === 'examples')
+assert.equal(examplesTab?.firstItem?.to, '/charts/catalog')
+assert.equal(
+ examplesTab?.groups[0]?.children.length,
+ groups[0]?.children.length,
+)
+assert.equal(
+ getActiveDocsNavTabId({
+ tabIdOverride: 'examples',
+ menuConfig: catalogGroups,
+ pathname: '/charts/catalog/charts/01-line-gaps/',
+ relativePathname: '',
+ }),
+ examplesTab?.id,
+)
+assert.equal(
+ getTabbedMenuConfig([], customToolTabs.charts)[0]?.firstItem?.to,
+ '/charts/catalog',
+)
+const highlightTabs = getTabbedMenuConfig([], customToolTabs.highlight)
+assert.equal(highlightTabs[0]?.id, 'theme-editor')
+assert.equal(
+ highlightTabs[0]?.firstItem?.to,
+ '/highlight/$version/theme-editor',
+)
+assert.ok(highlightTabs[0]?.firstItem)
+assert.deepEqual(
+ getLibraryTabLinkOptions({
+ libraryId: 'highlight',
+ version: 'v1',
+ to: highlightTabs[0].firstItem.to,
+ }),
+ {
+ from: undefined,
+ to: '/highlight/$version/theme-editor',
+ params: { version: 'v1' },
+ },
+)
+
+console.log('charts sidebar navigation tests passed')