From 1057984aeb236daff795b6624d5ab887022a5e52 Mon Sep 17 00:00:00 2001 From: Raffaele Farinaro Date: Thu, 11 Jun 2026 13:43:13 +0200 Subject: [PATCH 1/2] Add mobile framework and version selector to doc pages On mobile there was no visible way to switch frameworks: the only path was the easy-to-miss 'Back to main menu' link in the hamburger, behind an entry labeled 'SDKs', and its links dropped users on the add-sdk page instead of keeping the page they were reading. - Add an in-page framework + version selector (homepage pill style) at the top of doc pages, shown below 768px. Switching keeps the current page and version; Xamarin entries only appear on legacy versions. - On tablets (768-996px) show the existing navbar version/framework dropdowns next to the search instead of the in-page selector. - Extract the desktop dropdown's path-preserving link logic into a shared useFrameworkItems hook; the mobile SDKs dropdown now also keeps the current page when switching. - Hide breadcrumbs and the 'Back to main menu' button on mobile; the sidebar already highlights the open page. - Reduce mobile doc content top padding (48px -> 16px). - Fix homepage framework selector dropdown being painted over by the SkillsCallout (z-index). --- .../FrameworkSelectorMobile/index.tsx | 201 ++++++++++++++++ .../FrameworkSelectorMobile/styles.module.css | 119 ++++++++++ .../Frameworks/FrameworksMobile.module.css | 3 +- src/css/components/doc-content.scss | 10 + src/css/components/navbar-sidebar.scss | 6 + src/css/components/navbar.scss | 11 + src/theme/DocItem/Layout/index.js | 63 +++++ src/theme/DocItem/Layout/styles.module.css | 10 + .../NavbarItem/DropdownNavbarItem/index.js | 217 ++---------------- .../DropdownNavbarItem/styles.module.css | 3 +- src/utils/useFrameworkItems.js | 123 ++++++++++ 11 files changed, 572 insertions(+), 194 deletions(-) create mode 100644 src/components/FrameworkSelectorMobile/index.tsx create mode 100644 src/components/FrameworkSelectorMobile/styles.module.css create mode 100644 src/theme/DocItem/Layout/index.js create mode 100644 src/theme/DocItem/Layout/styles.module.css create mode 100644 src/utils/useFrameworkItems.js diff --git a/src/components/FrameworkSelectorMobile/index.tsx b/src/components/FrameworkSelectorMobile/index.tsx new file mode 100644 index 00000000..4ae6e571 --- /dev/null +++ b/src/components/FrameworkSelectorMobile/index.tsx @@ -0,0 +1,201 @@ +import React, { useEffect, useRef, useState } from "react"; +import Link from "@docusaurus/Link"; +import { useLocation } from "@docusaurus/router"; +import { + useVersions, + useActiveDocContext, +} from "@docusaurus/plugin-content-docs/client"; +import { useDocsPreferredVersion } from "@docusaurus/theme-common"; +import { useFrameworkItems } from "@site/src/utils/useFrameworkItems"; +import { frameworkCards } from "@site/src/components/HomePage/data/frameworkCardsArr"; +import { FrameworksName } from "@site/src/components/constants/frameworksName"; +import { ArrowDown } from "@site/src/components/IconComponents"; +import styles from "./styles.module.css"; + +// Reuse the homepage framework icons, keyed by display label ("iOS", ".NET Android", ...) +const iconsByLabel: Record = {}; +frameworkCards.forEach((card) => { + iconsByLabel[FrameworksName[card.framework as keyof typeof FrameworksName]] = + card.icon; + card.additional?.forEach((additionalCard) => { + iconsByLabel[ + FrameworksName[additionalCard.framework as keyof typeof FrameworksName] + ] = additionalCard.icon; + }); +}); + +// Same categorization as the navbar version dropdown +const VERSION_CATEGORY: Record = { + current: "stable", +}; +const TAG_LABEL: Record = { + stable: "Stable", + beta: "Beta", + legacy: "Legacy", +}; + +const getVersionMainDoc = (version) => + version.docs.find((doc) => doc.id === version.mainDocId); + +function useDropdown(close: () => void) { + const fieldRef = useRef(null); + const menuRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event: MouseEvent | TouchEvent) { + if ( + menuRef.current && + !menuRef.current.contains(event.target as Node) && + fieldRef.current && + !fieldRef.current.contains(event.target as Node) + ) { + close(); + } + } + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("touchstart", handleClickOutside); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("touchstart", handleClickOutside); + }; + }, [close]); + + return { fieldRef, menuRef }; +} + +/** + * Homepage-style framework and SDK version selector shown at the top of doc + * pages on mobile, where the desktop navbar dropdowns are not available. + * Switching keeps the current page (and version, for framework switches). + */ +export default function FrameworkSelectorMobile() { + const { currentFramework, frameworkItems } = useFrameworkItems(); + const { search, hash, pathname } = useLocation(); + const activeDocContext = useActiveDocContext(undefined); + const versions = useVersions(undefined); + const { savePreferredVersionName } = useDocsPreferredVersion(); + + const [openSelector, setOpenSelector] = useState< + "framework" | "version" | null + >(null); + const framework = useDropdown(() => + setOpenSelector((open) => (open === "framework" ? null : open)) + ); + const version = useDropdown(() => + setOpenSelector((open) => (open === "version" ? null : open)) + ); + + if (!currentFramework) { + return null; + } + + // Xamarin only exists in the legacy versions + const filteredVersions = pathname.includes("/xamarin/") + ? versions.filter((v) => v.name === "7.6.14" || v.name === "6.28.10") + : versions; + const activeVersion = activeDocContext?.activeVersion; + + const versionLinks = filteredVersions.map((v) => ({ + name: v.name, + label: v.label, + category: VERSION_CATEGORY[v.name] || "legacy", + to: `${ + (activeDocContext?.alternateDocVersions[v.name] ?? getVersionMainDoc(v)) + .path + }${search}${hash}`, + isActive: v === activeVersion, + })); + + const versionTag = (category: string) => ( + + {TAG_LABEL[category]} + + ); + + return ( +
+
+
+ setOpenSelector(openSelector === "version" ? null : "version") + } + > + {activeVersion?.label} + + + +
+
+ setOpenSelector(openSelector === "framework" ? null : "framework") + } + > + + + {iconsByLabel[currentFramework]} + + {currentFramework} + + + + +
+
+ {openSelector === "version" && ( +
+ {versionLinks.map((v) => ( + { + savePreferredVersionName(v.name); + setOpenSelector(null); + }} + > + {v.label} + {versionTag(v.category)} + + ))} +
+ )} + {openSelector === "framework" && ( +
+ {frameworkItems.map((item) => ( + setOpenSelector(null)} + > + + {iconsByLabel[item.label]} + + {item.label} + + ))} +
+ )} +
+ ); +} diff --git a/src/components/FrameworkSelectorMobile/styles.module.css b/src/components/FrameworkSelectorMobile/styles.module.css new file mode 100644 index 00000000..ebe4fb97 --- /dev/null +++ b/src/components/FrameworkSelectorMobile/styles.module.css @@ -0,0 +1,119 @@ +.selectorMobile { + position: relative; + margin-bottom: 20px; + /* Full width on phones, but don't stretch across tablet-width viewports + (Docusaurus treats everything under 997px as mobile) */ + max-width: 420px; +} + +/* From tablet width up, the navbar shows the version + framework dropdowns + (see navbar.scss), so the in-page selector is only for narrow screens */ +@media (min-width: 768px) { + .selectorMobile { + display: none; + } +} + +.fieldsRow { + display: flex; + gap: 8px; +} + +.field { + width: 100%; + border-radius: 100px; + border: 1px solid var(--nav-border); + height: 48px; + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 20px; + color: var(--light-black); + font-size: 16px; + font-weight: 400; + cursor: pointer; +} + +/* The version pill only needs to fit its short label. + Declared after .field so its width override wins. */ +.versionField { + flex: 0 0 auto; + width: auto; + gap: 10px; +} + +.fieldLabel { + display: flex; + align-items: center; + gap: 10px; +} + +.fieldIcon, +.open { + height: 20px; +} + +.open { + transform: rotate(180deg); + transition: transform 0.3s ease; +} + +.optionsMain { + margin-top: 8px; + box-shadow: 0 0 6px #10233223; + border-radius: 10px; + padding: 16px 20px; + position: absolute; + background-color: var(--frameworksBgLight); + width: 100%; + max-height: 440px; + overflow-y: auto; + top: 48px; + /* Above in-page content like the SkillsCallout, whose summary/body are z-index 1 */ + z-index: 20; +} + +.optionsMain::-webkit-scrollbar { + width: 8px; +} + +.optionsMain::-webkit-scrollbar-thumb { + background-color: #888; + border-radius: 10px; +} + +.optionsMain::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +.option { + display: flex; + align-items: center; + gap: 8px; + padding: 10px; + cursor: pointer; + border-radius: 8px; + color: var(--light-black); +} + +.option:hover { + background-color: var(--frameworksBgHover); + text-decoration: none; + color: var(--light-black); +} + +.optionsIcon { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; +} + +/* Same selected style as the homepage framework selector */ +.checkedFramework, +.checkedFramework:hover { + background-color: #121619; + color: #fff; +} diff --git a/src/components/HomePage/Frameworks/FrameworksMobile.module.css b/src/components/HomePage/Frameworks/FrameworksMobile.module.css index 6de58cd2..19802b8a 100644 --- a/src/components/HomePage/Frameworks/FrameworksMobile.module.css +++ b/src/components/HomePage/Frameworks/FrameworksMobile.module.css @@ -46,7 +46,8 @@ max-height: 440px; overflow-y: auto; top: 50px; - z-index: 1; + /* Above in-page content like the SkillsCallout, whose summary/body are z-index 1 */ + z-index: 20; } /* Styles for the scrollbar */ diff --git a/src/css/components/doc-content.scss b/src/css/components/doc-content.scss index 0007c593..fab460df 100644 --- a/src/css/components/doc-content.scss +++ b/src/css/components/doc-content.scss @@ -116,6 +116,16 @@ #__docusaurus [class*="docMainContainer"] { max-width: 100%; overflow: hidden; + + & .container { + padding-top: 16px !important; + } + } + + /* On mobile the breadcrumbs make way for the framework selector; the + sidebar menu still highlights the open page for orientation. */ + #__docusaurus .theme-doc-breadcrumbs { + display: none; } } diff --git a/src/css/components/navbar-sidebar.scss b/src/css/components/navbar-sidebar.scss index 88f8005d..d209ba5a 100644 --- a/src/css/components/navbar-sidebar.scss +++ b/src/css/components/navbar-sidebar.scss @@ -91,3 +91,9 @@ html[data-theme="dark"] .navbar-sidebar { background-color: var(--dark-blue-90); } + +/* The in-page framework/version selector replaced the primary-menu detour + on doc pages, so the "Back to main menu" button only adds noise. */ +#__docusaurus .navbar-sidebar__back { + display: none; +} diff --git a/src/css/components/navbar.scss b/src/css/components/navbar.scss index e296f4c4..c7e2b6dd 100644 --- a/src/css/components/navbar.scss +++ b/src/css/components/navbar.scss @@ -307,6 +307,17 @@ html[data-theme="dark"] .navbar { } } +/* On tablets there is room in the navbar for the version + framework + dropdowns (Infima hides all navbar items below 997px); the in-page + selector takes over only below 768px. */ +@media screen and (min-width: 768px) and (max-width: 996px) { + .navbar { + & .navbar__items:not(.navbar__items--right) .navbar__item.dropdown { + display: block; + } + } +} + @media screen and (max-width: 1120px) { .navbar { & .navbar__title { diff --git a/src/theme/DocItem/Layout/index.js b/src/theme/DocItem/Layout/index.js new file mode 100644 index 00000000..7191647c --- /dev/null +++ b/src/theme/DocItem/Layout/index.js @@ -0,0 +1,63 @@ +import React from "react"; +import clsx from "clsx"; +import { useWindowSize } from "@docusaurus/theme-common"; +import { useDoc } from "@docusaurus/theme-common/internal"; +import DocItemPaginator from "@theme/DocItem/Paginator"; +import DocVersionBanner from "@theme/DocVersionBanner"; +import DocVersionBadge from "@theme/DocVersionBadge"; +import DocItemFooter from "@theme/DocItem/Footer"; +import DocItemTOCMobile from "@theme/DocItem/TOC/Mobile"; +import DocItemTOCDesktop from "@theme/DocItem/TOC/Desktop"; +import DocItemContent from "@theme/DocItem/Content"; +import DocBreadcrumbs from "@theme/DocBreadcrumbs"; +import Unlisted from "@theme/Unlisted"; +import FrameworkSelectorMobile from "@site/src/components/FrameworkSelectorMobile"; +import styles from "./styles.module.css"; + +/** + * Swizzled (copy of the stock component) only to render the mobile + * framework/version selector above the "On this page" mobile TOC. + */ +function useDocTOC() { + const { frontMatter, toc } = useDoc(); + const windowSize = useWindowSize(); + const hidden = frontMatter.hide_table_of_contents; + const canRender = !hidden && toc.length > 0; + const mobile = canRender ? : undefined; + const desktop = + canRender && (windowSize === "desktop" || windowSize === "ssr") ? ( + + ) : undefined; + return { + hidden, + mobile, + desktop, + }; +} + +export default function DocItemLayout({ children }) { + const docTOC = useDocTOC(); + const { + metadata: { unlisted }, + } = useDoc(); + return ( +
+
+ {unlisted && } + +
+
+ + + + {docTOC.mobile} + {children} + +
+ +
+
+ {docTOC.desktop &&
{docTOC.desktop}
} +
+ ); +} diff --git a/src/theme/DocItem/Layout/styles.module.css b/src/theme/DocItem/Layout/styles.module.css new file mode 100644 index 00000000..d5aaec13 --- /dev/null +++ b/src/theme/DocItem/Layout/styles.module.css @@ -0,0 +1,10 @@ +.docItemContainer header + *, +.docItemContainer article > *:first-child { + margin-top: 0; +} + +@media (min-width: 997px) { + .docItemCol { + max-width: 75% !important; + } +} diff --git a/src/theme/NavbarItem/DropdownNavbarItem/index.js b/src/theme/NavbarItem/DropdownNavbarItem/index.js index 70b99cc2..d1172bbc 100644 --- a/src/theme/NavbarItem/DropdownNavbarItem/index.js +++ b/src/theme/NavbarItem/DropdownNavbarItem/index.js @@ -1,4 +1,4 @@ -import React, { useState, useRef, useEffect, useMemo } from "react"; +import React, { useState, useRef, useEffect } from "react"; import clsx from "clsx"; import { isRegexpStringMatch, @@ -13,7 +13,7 @@ import NavbarNavLink from "@theme/NavbarItem/NavbarNavLink"; import NavbarItem from "@theme/NavbarItem"; import styles from "./styles.module.css"; import { useLocation } from "@docusaurus/router"; -import { FrameworksName } from "@site/src/components/constants/frameworksName"; +import { useFrameworkItems } from "@site/src/utils/useFrameworkItems"; function isItemActive(item, localPathname) { if (isSamePath(item.to, localPathname)) { @@ -39,12 +39,11 @@ function DropdownNavbarItemDesktop({ }) { const dropdownRef = useRef(null); const [showDropdown, setShowDropdown] = useState(false); - const [link, setLink] = useState("/add-sdk"); - const [linkVersion, setLinkVersion] = useState("sdks"); const location = useLocation(); const currentPath = location.pathname; const regex = /\/hosted\//; const isHostedPage = regex.test(currentPath); + const { currentFramework, frameworkItems } = useFrameworkItems(); useEffect(() => { const handleClickOutside = (event) => { @@ -63,182 +62,6 @@ function DropdownNavbarItemDesktop({ }; }, [dropdownRef]); - const currentFramework = useMemo(() => { - const regex = /(?<=\/sdks\/)(\w+)(?:\/(\w+))?/; - const match = currentPath.match(regex); - if (match) { - const primaryKey = match[1]; - const secondaryKey = match[2]; - if (primaryKey === "xamarin" || primaryKey === "net") { - const frameworkKey = secondaryKey - ? `${primaryKey}/${secondaryKey}` - : primaryKey; - const frameworksMap = { - "xamarin/ios": "Xamarin iOS", - "xamarin/android": "Xamarin Android", - "xamarin/forms": "Xamarin Forms", - "net/android": ".NET Android", - "net/ios": ".NET iOS", - }; - return frameworksMap[frameworkKey] || null; - } - return FrameworksName[primaryKey] || null; - } - }, [currentPath]); - - // Detect if we're on an older version that still has Xamarin - const isXamarinAvailable = useMemo(() => { - if (!currentPath) return false; - // Xamarin is only available in versions 7.6.x and 6.28.x - return currentPath.includes("/7.6.") || currentPath.includes("/6.28."); - }, [currentPath]); - - // Get the version from the current path for Xamarin links - const xamarinVersion = useMemo(() => { - if (!currentPath) return "/7.6.14"; - if (currentPath.includes("/7.6.14")) return "/7.6.14"; - if (currentPath.includes("/6.28.10")) return "/6.28.10"; - if (currentPath.includes("/7.6.")) return "/7.6.14"; - if (currentPath.includes("/6.28.")) return "/6.28.10"; - return "/7.6.14"; // Default to 7.6.14 for Xamarin - }, [currentPath]); - - useEffect(() => { - if (!currentPath) return; - const possibleVersions = ["/next", "/6.28.10", "/7.6.14"]; - const match = currentPath.match(/(.*)(?=\/sdks)/); - setLinkVersion(match && match[0] ? `${match[0]}/sdks` : "/sdks"); - - const activeBasePath = newItems.find((item) => - currentPath.includes(item.activeBasePath) - )?.activeBasePath; - - let trimmedPath = activeBasePath - ? currentPath.replace(activeBasePath, "") - : currentPath; - - possibleVersions.forEach((version) => { - trimmedPath = trimmedPath.replace(version, ""); - }); - - trimmedPath = trimmedPath.endsWith("/") - ? trimmedPath.slice(0, -1) - : trimmedPath; - - setLink(trimmedPath || "/add-sdk"); - }, [currentPath]); - - const newItems = [ - { - type: "docsVersion", - label: "iOS", - sidebarId: "iosSidebar", - to: `${linkVersion}/ios${link}`, - activeBasePath: "sdks/ios/", - }, - { - type: "docsVersion", - label: "Android", - sidebarId: "androidSidebar", - to: `${linkVersion}/android${link}`, - activeBasePath: "sdks/android/", - }, - { - type: "docsVersion", - label: "Web", - sidebarId: "webSidebar", - to: `${linkVersion}/web${link}`, - activeBasePath: "sdks/web/", - }, - { - type: "docsVersion", - label: "Cordova", - sidebarId: "cordovaSidebar", - to: `${linkVersion}/cordova${link}`, - activeBasePath: "sdks/cordova/", - }, - { - type: "docsVersion", - label: "React Native", - sidebarId: "reactnativeSidebar", - to: `${linkVersion}/react-native${link}`, - activeBasePath: "sdks/react-native/", - }, - { - type: "docsVersion", - label: "Flutter", - sidebarId: "flutterSidebar", - to: `${linkVersion}/flutter${link}`, - activeBasePath: "sdks/flutter/", - }, - { - type: "docsVersion", - label: "Capacitor", - sidebarId: "capacitorSidebar", - to: `${linkVersion}/capacitor${link}`, - activeBasePath: "sdks/capacitor/", - }, - { - type: "docsVersion", - label: "Titanium", - sidebarId: "titaniumSidebar", - to: `${linkVersion}/titanium${link}`, - activeBasePath: "sdks/titanium/", - }, - { - type: "docsVersion", - label: "Xamarin iOS", - sidebarId: "xamarinIosSidebar", - to: `${xamarinVersion}/sdks/xamarin/ios${link}`, - activeBasePath: "sdks/xamarin/ios/", - }, - { - type: "docsVersion", - label: "Xamarin Android", - sidebarId: "xamarinAndroidSidebar", - to: `${xamarinVersion}/sdks/xamarin/android${link}`, - activeBasePath: "sdks/xamarin/android/", - }, - { - type: "docsVersion", - label: "Xamarin Forms", - sidebarId: "xamarinFormsSidebar", - to: `${xamarinVersion}/sdks/xamarin/forms${link}`, - activeBasePath: "sdks/xamarin/forms/", - }, - { - type: "docsVersion", - label: ".NET iOS", - sidebarId: "netIosSidebar", - to: `${linkVersion}/net/ios${link}`, - activeBasePath: "sdks/net/ios/", - }, - { - type: "docsVersion", - label: ".NET Android", - sidebarId: "netAndroidSidebar", - to: `${linkVersion}/net/android${link}`, - activeBasePath: "sdks/net/android/", - }, - { - type: "docsVersion", - label: "Linux", - sidebarId: "linuxSidebar", - to: `${linkVersion}/linux${link}`, - activeBasePath: "sdks/linux/", - }, - ]; - - // Filter out Xamarin items for versions where Xamarin is deprecated (8.0+) - const filteredItems = useMemo(() => { - if (!isXamarinAvailable) { - return newItems.filter( - (item) => !item.label.startsWith("Xamarin") - ); - } - return newItems; - }, [newItems, isXamarinAvailable]); - const headerItem = (label) => ({ type: "html", value: ``, @@ -251,7 +74,7 @@ function DropdownNavbarItemDesktop({ items && items.some((item) => item.type !== "docsVersion"); const combinedItems = hasDocsVersionItems - ? [headerItem("Framework"), ...filteredItems] + ? [headerItem("Framework"), ...frameworkItems] : items; const shouldShowDropdownMenu = hasSDKsItems || (hasDocsVersionItems && currentFramework); @@ -332,7 +155,14 @@ function DropdownNavbarItemMobile({ ...props }) { const localPathname = useLocalPathname(); - const containsActive = containsActiveItems(items, localPathname); + const { frameworkItems } = useFrameworkItems(); + // The SDKs dropdown (docsVersion items) gets the dynamically built framework + // links so switching frameworks on mobile keeps the current page and version. + const hasDocsVersionItems = items.some( + (item) => item.type === "docsVersion" + ); + const effectiveItems = hasDocsVersionItems ? frameworkItems : items; + const containsActive = containsActiveItems(effectiveItems, localPathname); const { collapsed, toggleCollapsed, setCollapsed } = useCollapsible({ initialState: () => !containsActive, }); @@ -365,16 +195,19 @@ function DropdownNavbarItemMobile({ {props.children ?? props.label} - {items.map((childItemProps, i) => ( - - ))} + {effectiveItems.map((childItemProps, i) => { + const { sidebarId, ...rest } = childItemProps; + return ( + + ); + })} ); diff --git a/src/theme/NavbarItem/DropdownNavbarItem/styles.module.css b/src/theme/NavbarItem/DropdownNavbarItem/styles.module.css index dfe934c8..01233880 100644 --- a/src/theme/NavbarItem/DropdownNavbarItem/styles.module.css +++ b/src/theme/NavbarItem/DropdownNavbarItem/styles.module.css @@ -2,7 +2,8 @@ cursor: pointer; } -@media (width < 997px) { +/* Below tablet width the in-page selector replaces the navbar dropdowns */ +@media (width < 768px) { .frameworkName { display: none; } diff --git a/src/utils/useFrameworkItems.js b/src/utils/useFrameworkItems.js new file mode 100644 index 00000000..524c7d37 --- /dev/null +++ b/src/utils/useFrameworkItems.js @@ -0,0 +1,123 @@ +import { useEffect, useMemo, useState } from "react"; +import { useLocation } from "@docusaurus/router"; +import { FrameworksName } from "@site/src/components/constants/frameworksName"; + +const POSSIBLE_VERSIONS = ["/next", "/6.28.10", "/7.6.14"]; + +const FRAMEWORKS = [ + { label: "iOS", sidebarId: "iosSidebar", slug: "ios", activeBasePath: "sdks/ios/" }, + { label: "Android", sidebarId: "androidSidebar", slug: "android", activeBasePath: "sdks/android/" }, + { label: "Web", sidebarId: "webSidebar", slug: "web", activeBasePath: "sdks/web/" }, + { label: "Cordova", sidebarId: "cordovaSidebar", slug: "cordova", activeBasePath: "sdks/cordova/" }, + { label: "React Native", sidebarId: "reactnativeSidebar", slug: "react-native", activeBasePath: "sdks/react-native/" }, + { label: "Flutter", sidebarId: "flutterSidebar", slug: "flutter", activeBasePath: "sdks/flutter/" }, + { label: "Capacitor", sidebarId: "capacitorSidebar", slug: "capacitor", activeBasePath: "sdks/capacitor/" }, + { label: "Titanium", sidebarId: "titaniumSidebar", slug: "titanium", activeBasePath: "sdks/titanium/" }, + { label: "Xamarin iOS", sidebarId: "xamarinIosSidebar", slug: "xamarin/ios", activeBasePath: "sdks/xamarin/ios/", xamarin: true }, + { label: "Xamarin Android", sidebarId: "xamarinAndroidSidebar", slug: "xamarin/android", activeBasePath: "sdks/xamarin/android/", xamarin: true }, + { label: "Xamarin Forms", sidebarId: "xamarinFormsSidebar", slug: "xamarin/forms", activeBasePath: "sdks/xamarin/forms/", xamarin: true }, + { label: ".NET iOS", sidebarId: "netIosSidebar", slug: "net/ios", activeBasePath: "sdks/net/ios/" }, + { label: ".NET Android", sidebarId: "netAndroidSidebar", slug: "net/android", activeBasePath: "sdks/net/android/" }, + { label: "Linux", sidebarId: "linuxSidebar", slug: "linux", activeBasePath: "sdks/linux/" }, +]; + +/** + * Builds the framework switcher items for the current page, preserving the + * current doc path and version when jumping to another framework + * (e.g. /sdks/android/id-capture/intro -> /sdks/ios/id-capture/intro). + * Shared by the desktop navbar dropdown and the mobile doc sidebar switcher. + */ +export function useFrameworkItems() { + const { pathname: currentPath } = useLocation(); + + const currentFramework = useMemo(() => { + const regex = /(?<=\/sdks\/)(\w+)(?:\/(\w+))?/; + const match = currentPath.match(regex); + if (match) { + const primaryKey = match[1]; + const secondaryKey = match[2]; + if (primaryKey === "xamarin" || primaryKey === "net") { + const frameworkKey = secondaryKey + ? `${primaryKey}/${secondaryKey}` + : primaryKey; + const frameworksMap = { + "xamarin/ios": "Xamarin iOS", + "xamarin/android": "Xamarin Android", + "xamarin/forms": "Xamarin Forms", + "net/android": ".NET Android", + "net/ios": ".NET iOS", + }; + return frameworksMap[frameworkKey] || null; + } + return FrameworksName[primaryKey] || null; + } + return null; + }, [currentPath]); + + // Xamarin is only available in versions 7.6.x and 6.28.x + const isXamarinAvailable = useMemo( + () => + !!currentPath && + (currentPath.includes("/7.6.") || currentPath.includes("/6.28.")), + [currentPath] + ); + + const xamarinVersion = useMemo(() => { + if (!currentPath) return "/7.6.14"; + if (currentPath.includes("/6.28.")) return "/6.28.10"; + return "/7.6.14"; // Default to 7.6.14 for Xamarin + }, [currentPath]); + + // Deliberately computed client-side (after hydration) and not during SSR: + // some pages have no equivalent in every framework, and server-rendering + // those per-page links would make Docusaurus' broken-link checker fail the + // build. At runtime the custom NotFound page redirects gracefully instead. + const [{ link, linkVersion }, setLinkParts] = useState({ + link: "/add-sdk", + linkVersion: "/sdks", + }); + + useEffect(() => { + if (!currentPath) return; + + const versionMatch = currentPath.match(/(.*)(?=\/sdks)/); + const linkVersion = + versionMatch && versionMatch[0] ? `${versionMatch[0]}/sdks` : "/sdks"; + + const activeBasePath = FRAMEWORKS.find((framework) => + currentPath.includes(framework.activeBasePath) + )?.activeBasePath; + + let trimmedPath = activeBasePath + ? currentPath.replace(activeBasePath, "") + : currentPath; + + POSSIBLE_VERSIONS.forEach((version) => { + trimmedPath = trimmedPath.replace(version, ""); + }); + + trimmedPath = trimmedPath.endsWith("/") + ? trimmedPath.slice(0, -1) + : trimmedPath; + + setLinkParts({ link: trimmedPath || "/add-sdk", linkVersion }); + }, [currentPath]); + + const frameworkItems = useMemo( + () => + FRAMEWORKS.filter( + (framework) => isXamarinAvailable || !framework.xamarin + ).map((framework) => ({ + type: "docsVersion", + label: framework.label, + sidebarId: framework.sidebarId, + to: framework.xamarin + ? `${xamarinVersion}/sdks/${framework.slug}${link}` + : `${linkVersion}/${framework.slug}${link}`, + activeBasePath: framework.activeBasePath, + })), + [isXamarinAvailable, xamarinVersion, link, linkVersion] + ); + + return { currentFramework, frameworkItems }; +} From 0e9028075347e502fd14bbc1b560fcf7008b0e46 Mon Sep 17 00:00:00 2001 From: Raffaele Farinaro Date: Sat, 13 Jun 2026 18:14:14 +0200 Subject: [PATCH 2/2] Address PR review: version selector robustness and clarity - Guard against getVersionMainDoc returning undefined; fall back to "/" instead of throwing on .path. - Compare version by name, not reference: v (useVersions) and activeVersion (useActiveDocContext) are from different hooks, so the active-version highlight never matched. - Comment why sidebarId is dropped from docsVersion items (explicit page-preserving `to` would otherwise lose to sidebar main-doc resolution). --- .../FrameworkSelectorMobile/index.tsx | 25 +++++++++++-------- .../NavbarItem/DropdownNavbarItem/index.js | 7 ++++++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/components/FrameworkSelectorMobile/index.tsx b/src/components/FrameworkSelectorMobile/index.tsx index 4ae6e571..841507d9 100644 --- a/src/components/FrameworkSelectorMobile/index.tsx +++ b/src/components/FrameworkSelectorMobile/index.tsx @@ -95,16 +95,21 @@ export default function FrameworkSelectorMobile() { : versions; const activeVersion = activeDocContext?.activeVersion; - const versionLinks = filteredVersions.map((v) => ({ - name: v.name, - label: v.label, - category: VERSION_CATEGORY[v.name] || "legacy", - to: `${ - (activeDocContext?.alternateDocVersions[v.name] ?? getVersionMainDoc(v)) - .path - }${search}${hash}`, - isActive: v === activeVersion, - })); + const versionLinks = filteredVersions.map((v) => { + // Prefer the equivalent of the current page in version v; fall back to that + // version's main doc, and finally to "/" if neither resolves. + const versionDoc = + activeDocContext?.alternateDocVersions[v.name] ?? getVersionMainDoc(v); + return { + name: v.name, + label: v.label, + category: VERSION_CATEGORY[v.name] || "legacy", + to: `${versionDoc?.path ?? "/"}${search}${hash}`, + // v (from useVersions) and activeVersion (from useActiveDocContext) come + // from different hooks, so compare by name rather than reference. + isActive: v.name === activeVersion?.name, + }; + }); const versionTag = (category: string) => ( diff --git a/src/theme/NavbarItem/DropdownNavbarItem/index.js b/src/theme/NavbarItem/DropdownNavbarItem/index.js index d1172bbc..b096a6d2 100644 --- a/src/theme/NavbarItem/DropdownNavbarItem/index.js +++ b/src/theme/NavbarItem/DropdownNavbarItem/index.js @@ -131,6 +131,9 @@ function DropdownNavbarItemDesktop({ {shouldShowDropdownMenu && !isHostedPage && (
    {combinedItems.map((childItemProps, i) => { + // Drop sidebarId so the framework item's explicit `to` is used: + // a docsVersion item with sidebarId resolves to the sidebar's + // main doc instead, which would lose the current page. const { sidebarId, ...rest } = childItemProps; return ( {effectiveItems.map((childItemProps, i) => { + // Drop sidebarId: for a docsVersion item, Docusaurus resolves the + // link from the sidebar's main doc when sidebarId is present, which + // would override our page-preserving `to`. Stripping it lets the + // explicit `to` win (matches the desktop path above). const { sidebarId, ...rest } = childItemProps; return (