Skip to content
47 changes: 44 additions & 3 deletions shared-route-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down Expand Up @@ -74,16 +76,55 @@ 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',
},
},
];

/**
* URL paths that share common documentation files.
* 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';

Expand Down
137 changes: 111 additions & 26 deletions theme/AfterNavTitle.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 (
<div
className="cursor-pointer hover:bg-accent rounded-md p-2 flex items-center justify-between"
onClick={onClick}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onClick();
}}
role="button"
tabIndex={0}
>
<SubsiteView subsite={subsite} lang={lang} size={size} />
{showArrow && (
<ArrowUpRight className="h-3.5 w-3.5 text-muted-foreground shrink-0" strokeWidth={1.5} />
)}
</div>
);
}

function SectionHeader({ children }: { children: React.ReactNode }) {
return (
<div className="px-2 pt-2 pb-1">
<span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide">
{children}
</span>
</div>
);
}

function NavContent({
onSelect,
isDrawer,
Expand All @@ -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 (
<div className="flex flex-col gap-2 p-1">
{SUBSITES_CONFIG.map((subsite) => (
<div
key={subsite.value}
className="cursor-pointer hover:bg-accent rounded-md p-2"
onClick={() => handleSubsiteClick(subsite)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleSubsiteClick(subsite);
}
}}
role="button"
tabIndex={0}
>
<SubsiteView
if (isDrawer) {
return (
<div className="flex flex-col gap-2 p-1">
{internalSubsites.map((subsite) => (
<SubsiteItem
key={subsite.value}
subsite={subsite}
lang={lang}
size={isDrawer ? 'large' : 'default'}
onClick={() => handleSubsiteClick(subsite)}
size="large"
/>
))}
{externalSubsites.length > 0 && (
<>
<Separator />
<SectionHeader>Ecosystem</SectionHeader>
{externalSubsites.map((subsite) => (
<SubsiteItem
key={subsite.value}
subsite={subsite}
onClick={() => handleSubsiteClick(subsite)}
size="large"
showArrow
/>
))}
</>
)}
</div>
);
}

return (
<div className="grid grid-cols-2 divide-x divide-border">
<div className="px-3 pt-0 pb-3">
<SectionHeader>Core</SectionHeader>
<div className="flex flex-col gap-1 pt-1">
{internalSubsites.map((subsite) => (
<SubsiteItem
key={subsite.value}
subsite={subsite}
onClick={() => handleSubsiteClick(subsite)}
size="default"
/>
))}
</div>
</div>
<div className="px-3 pt-0 pb-3">
<SectionHeader>Ecosystem</SectionHeader>
<div className="flex flex-col gap-1 pt-1">
{externalSubsites.map((subsite) => (
<SubsiteItem
key={subsite.value}
subsite={subsite}
onClick={() => handleSubsiteClick(subsite)}
size="default"
showArrow
/>
))}
</div>
))}
</div>
</div>
);
}
Expand Down Expand Up @@ -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);
Expand All @@ -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]);

Expand Down Expand Up @@ -183,7 +268,7 @@ export default function AfterNavTitle() {
<DropdownMenuTrigger asChild>
<Trigger />
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 p-0" align="start">
<DropdownMenuContent className="w-[520px] p-0" align="start">
<NavContent onSelect={() => setIsOpen(false)} />
</DropdownMenuContent>
</div>
Expand Down
19 changes: 10 additions & 9 deletions theme/subsite-ui.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
Expand Down
Loading