diff --git a/shared-route-config.ts b/shared-route-config.ts index c4d10321d..dacbc3eef 100644 --- a/shared-route-config.ts +++ b/shared-route-config.ts @@ -20,6 +20,8 @@ export type SubsiteConfig = { light: string; dark: string; }; + /** When set, the subsite links to an external URL instead of an internal route. */ + external?: string; }; export const SUBSITES_CONFIG: SubsiteConfig[] = [ @@ -74,6 +76,45 @@ export const SUBSITES_CONFIG: SubsiteConfig[] = [ dark: '/assets/lynxai-logo-dark.svg', }, }, + { + value: 'sparkling', + label: 'Sparkling', + description: 'Lynx at TikTok scale', + descriptionZh: 'TikTok 规模的 Lynx 基础设施', + external: 'https://tiktok.github.io/sparkling', + home: '', + url: '', + logo: { + light: 'https://tiktok.github.io/sparkling/sparkling_logo_144_light.png', + dark: 'https://tiktok.github.io/sparkling/sparkling_logo_144.png', + }, + }, + { + value: 'vue-lynx', + label: 'Vue Lynx', + description: 'Build Lynx apps with Vue', + descriptionZh: '用 Vue 开发 Lynx 应用', + external: 'https://vue.lynxjs.org', + home: '', + url: '', + logo: { + light: 'https://vuejs.org/logo.svg', + dark: 'https://vuejs.org/logo.svg', + }, + }, + { + value: 'reactlynx-use', + label: 'ReactLynx Use', + description: 'Hooks for ReactLynx', + descriptionZh: 'ReactLynx 的 Hooks 库', + external: 'https://hooks.lynxjs.org', + home: '', + url: '', + logo: { + light: 'https://lf-lynx.tiktok-cdns.com/obj/lynx-artifacts-oss-sg/lynx-website/assets/reactlynx-logo-light.svg', + dark: 'https://lf-lynx.tiktok-cdns.com/obj/lynx-artifacts-oss-sg/lynx-website/assets/reactlynx-logo-dark.svg', + }, + }, ]; /** @@ -81,9 +122,9 @@ export const SUBSITES_CONFIG: SubsiteConfig[] = [ * For example, "start/quick-start" will be accessible at both * "guide/start/quick-start" and "react/start/quick-start". */ -export const SHARED_SIDEBAR_PATHS = SUBSITES_CONFIG.map( - (config) => config.value, -); +export const SHARED_SIDEBAR_PATHS = SUBSITES_CONFIG.filter( + (config) => !config.external, +).map((config) => config.value); const SHARED_DOC_ROOT = 'start'; diff --git a/theme/AfterNavTitle.tsx b/theme/AfterNavTitle.tsx index b4010962a..18ddfa18d 100644 --- a/theme/AfterNavTitle.tsx +++ b/theme/AfterNavTitle.tsx @@ -1,9 +1,10 @@ import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'; -import { ChevronDown } from 'lucide-react'; +import { ArrowUpRight, ChevronDown } from 'lucide-react'; import { forwardRef, useEffect, useState } from 'react'; import { useLang, useLocation, useNavigate } from '@rspress/core/runtime'; import { Link } from '@rspress/core/theme-original'; import { SUBSITES_CONFIG, getLangPrefix } from '@site/shared-route-config'; +import { Separator } from '@/components/ui/separator'; import { SubsiteLogo, SubsiteView } from './subsite-ui'; import { VersionIndicator } from './VersionIndicator'; @@ -13,6 +14,49 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; +const internalSubsites = SUBSITES_CONFIG.filter((s) => !s.external); +const externalSubsites = SUBSITES_CONFIG.filter((s) => s.external); + +function SubsiteItem({ + subsite, + onClick, + size, + showArrow, +}: { + subsite: (typeof SUBSITES_CONFIG)[0]; + onClick: () => void; + size: 'default' | 'large' | 'minimal'; + showArrow?: boolean; +}) { + const lang = useLang(); + return ( +
{ + if (e.key === 'Enter' || e.key === ' ') onClick(); + }} + role="button" + tabIndex={0} + > + + {showArrow && ( + + )} +
+ ); +} + +function SectionHeader({ children }: { children: React.ReactNode }) { + return ( +
+ + {children} + +
+ ); +} + function NavContent({ onSelect, isDrawer, @@ -24,32 +68,73 @@ function NavContent({ const lang = useLang(); const handleSubsiteClick = (subsite: (typeof SUBSITES_CONFIG)[0]) => { - navigate(`${getLangPrefix(lang)}${subsite.home}`); + if (subsite.external) { + window.open(subsite.external, '_blank'); + } else { + navigate(`${getLangPrefix(lang)}${subsite.home}`); + } onSelect(); }; - return ( -
- {SUBSITES_CONFIG.map((subsite) => ( -
handleSubsiteClick(subsite)} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - handleSubsiteClick(subsite); - } - }} - role="button" - tabIndex={0} - > - + {internalSubsites.map((subsite) => ( + handleSubsiteClick(subsite)} + size="large" /> + ))} + {externalSubsites.length > 0 && ( + <> + + Ecosystem + {externalSubsites.map((subsite) => ( + handleSubsiteClick(subsite)} + size="large" + showArrow + /> + ))} + + )} +
+ ); + } + + return ( +
+
+ Core +
+ {internalSubsites.map((subsite) => ( + handleSubsiteClick(subsite)} + size="default" + /> + ))} +
+
+
+ Ecosystem +
+ {externalSubsites.map((subsite) => ( + handleSubsiteClick(subsite)} + size="default" + showArrow + /> + ))}
- ))} +
); } @@ -100,9 +185,9 @@ export default function AfterNavTitle() { const [currentSubsite, setCurrentSubsite] = useState(() => { const segments = pathname.split('/'); return ( - SUBSITES_CONFIG.find((s) => + internalSubsites.find((s) => segments.some((seg) => seg.replace(/\.html$/, '') === s.value), - ) || SUBSITES_CONFIG[0] + ) || internalSubsites[0] ); }); const [isOpen, setIsOpen] = useState(false); @@ -111,9 +196,9 @@ export default function AfterNavTitle() { useEffect(() => { const segments = pathname.split('/'); const subsite = - SUBSITES_CONFIG.find((s) => + internalSubsites.find((s) => segments.some((seg) => seg.replace(/\.html$/, '') === s.value), - ) || SUBSITES_CONFIG[0]; + ) || internalSubsites[0]; setCurrentSubsite(subsite); }, [pathname]); @@ -183,7 +268,7 @@ export default function AfterNavTitle() { - + setIsOpen(false)} />
diff --git a/theme/subsite-ui.tsx b/theme/subsite-ui.tsx index a1f9cc197..b947711f2 100644 --- a/theme/subsite-ui.tsx +++ b/theme/subsite-ui.tsx @@ -1,18 +1,19 @@ import type { SubsiteConfig } from '@site/shared-route-config'; import { withBase } from '@rspress/core/runtime'; -function isAbsoluteUrl(url: string): boolean { - return url.startsWith('/'); +function isExternalUrl(url: string): boolean { + return url.startsWith('http://') || url.startsWith('https://'); +} + +function resolveLogoUrl(url: string): string { + if (isExternalUrl(url)) return url; + if (url.startsWith('/')) return withBase(url); + return url; } export function SubsiteLogo({ subsite }: { subsite: SubsiteConfig }) { - // Ensure the logo URLs are absolute by prepending the site base if they are relative - const lightLogoSrc = isAbsoluteUrl(subsite.logo.light) - ? withBase(subsite.logo.light) - : subsite.logo.light; - const darkLogoSrc = isAbsoluteUrl(subsite.logo.dark) - ? withBase(subsite.logo.dark) - : subsite.logo.dark; + const lightLogoSrc = resolveLogoUrl(subsite.logo.light); + const darkLogoSrc = resolveLogoUrl(subsite.logo.dark); return ( <>