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 (
<>