Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { A, Label, useLayout } from '~/common/styleguide';
import ContentContainer from '~/components/ContentContainer';
import { WarningIcon } from '~/components/Icons';
import Navigation from '~/components/Navigation';
import NavigationTab from '~/components/NavigationTab';
import NavigationTabs from '~/components/Package/Navigation/NavigationTabs';
import { type LibraryType } from '~/types';
import { strippedBackground } from '~/util/style';
import tw from '~/util/tailwind';
Expand All @@ -14,10 +14,21 @@ type Props = {
library: LibraryType;
};

export default function DetailsNavigation({ library }: Props) {
export default function NavigationHeader({ library }: Props) {
const { isSmallScreen } = useLayout();
const alternativesLength = library.alternatives?.length ?? 0;

const tabs = [
{ title: 'Overview', path: `/package/${library.npmPkg}` },
{
title: 'Versions',
counter: library.npm?.versionsCount,
path: `/package/${library.npmPkg}/versions`,
},
{ title: 'Code', path: `/package/${library.npmPkg}/code` },
{ title: 'Score', path: `/package/${library.npmPkg}/score` },
];

return (
<Navigation
title="Package information"
Expand Down Expand Up @@ -57,15 +68,8 @@ export default function DetailsNavigation({ library }: Props) {
</View>
) : undefined
}>
<ContentContainer style={[tw`flex-row gap-2 px-5`, isSmallScreen && tw`flex-wrap`]}>
<NavigationTab title="Overview" path={`/package/${library.npmPkg}`} />
<NavigationTab
title="Versions"
counter={library.npm?.versionsCount}
path={`/package/${library.npmPkg}/versions`}
/>
<NavigationTab title="Code" path={`/package/${library.npmPkg}/code`} />
<NavigationTab title="Score" path={`/package/${library.npmPkg}/score`} />
<ContentContainer style={tw`flex-row px-5`}>
<NavigationTabs tabs={tabs} />
</ContentContainer>
</Navigation>
);
Expand Down
137 changes: 137 additions & 0 deletions components/Package/Navigation/NavigationTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import * as Popover from '@radix-ui/react-popover';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { View } from 'react-native';

import { A, HoverEffect, P } from '~/common/styleguide';
import { ArrowIcon } from '~/components/Icons';
import NavigationTab from '~/components/NavigationTab';
import EntityCounter from '~/components/Package/EntityCounter';
import { useNavigationTabs } from '~/components/Package/Navigation/useNavigationTabs';
import SelectorItemHoverEffect from '~/components/Selector/SelectorItemHoverEffect';
import tw from '~/util/tailwind';

type Tab = {
title: string;
path: string;
counter?: number | string;
};

type Props = {
tabs: Tab[];
};

const TABS_GAP = 8;

export default function NavigationTabs({ tabs }: Props) {
const [open, setOpen] = useState(false);
const router = useRouter();

const currentPath = decodeURIComponent(router.asPath.split('?')[0]);
const activeIndex = tabs.findIndex(tab => decodeURIComponent(tab.path) === currentPath);

const {
visible,
measuring,
navigationRef,
tabRef,
triggerRef,
onNavigationLayout,
onTabLayout,
onTriggerLayout,
} = useNavigationTabs(tabs.length, TABS_GAP);

const hiddenTabs = tabs.slice(visible);
const isTriggerActive = activeIndex >= visible;

return (
<View
ref={navigationRef}
style={tw`relative flex-1 overflow-hidden`}
onLayout={onNavigationLayout}>
<View
style={[
tw`pointer-events-none absolute left-0 top-0 flex-row items-center opacity-0`,
{ rowGap: TABS_GAP },
]}
aria-hidden>
{tabs.map((tab, index) => (
<View key={tab.title} ref={tabRef(index)} onLayout={onTabLayout(index)}>
<NavigationTab {...tab} />
</View>
))}
<View ref={triggerRef} onLayout={onTriggerLayout}>
<MoreTrigger active={false} open={false} />
</View>
</View>
<View
style={[
tw`flex-1 flex-row items-center`,
measuring && tw`opacity-0`,
measuring ? tw`pointer-events-none` : tw`pointer-events-auto`,
{ rowGap: TABS_GAP },
]}>
{tabs.slice(0, visible).map(tab => (
<NavigationTab key={tab.title} {...tab} />
))}
{hiddenTabs.length > 0 && (
<Popover.Root open={open} onOpenChange={setOpen}>
<HoverEffect
hoveredStyle={tw`bg-palette-gray6 dark:bg-default`}
pressedStyle={tw`bg-palette-gray6 dark:bg-default`}
style={[tw`rounded`, isTriggerActive && tw`bg-primary-hover`]}>
<Popover.Trigger asChild>
<View role="button">
<MoreTrigger active={isTriggerActive} open={open} />
</View>
</Popover.Trigger>
</HoverEffect>
<Popover.Portal>
<Popover.Content align="end" sideOffset={6}>
<View
style={tw`min-w-40 overflow-hidden rounded-lg border-2 border-palette-gray2 bg-default py-0.5 shadow-lg dark:border-default dark:bg-default`}>
{hiddenTabs.map((tab, index) => (
<SelectorItemHoverEffect key={tab.title} onPress={() => setOpen(false)}>
<A
href={tab.path}
style={tw`flex-row items-center justify-between gap-2 rounded-lg px-2.5 py-1.5 no-underline`}
target="_self">
<P
style={[
tw`text-[inherit]`,
visible + index === activeIndex &&
tw`text-primary-darker dark:text-primary`,
]}>
{tab.title}
</P>
{!!tab.counter && (
<EntityCounter count={tab.counter} style={tw`text-[inherit]`} />
)}
</A>
</SelectorItemHoverEffect>
))}
</View>
</Popover.Content>
</Popover.Portal>
</Popover.Root>
)}
</View>
</View>
);
}

function MoreTrigger({ active, open }: { active: boolean; open: boolean }) {
return (
<View style={tw`cursor-pointer flex-row items-center gap-1 px-4 pb-2 pt-1.5`}>
<P style={[tw`text-white`, active && tw`text-primary`]}>More</P>
<ArrowIcon
style={[
tw`h-3 w-4 shrink-0`,
active ? tw`text-primary` : tw`text-icon`,
open ? tw`rotate-270` : tw`rotate-90`,
{ transition: 'all 0.2s' },
]}
/>
</View>
);
}
98 changes: 98 additions & 0 deletions components/Package/Navigation/useNavigationTabs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useEffectEvent, useLayoutEffect, useRef, useState } from 'react';
import { type LayoutChangeEvent, type ViewInstance } from 'react-native';

export function useNavigationTabs(tabs: number, gap: number) {
const [navigationWidth, setNavigationWidth] = useState<number | null>(null);
const [triggerWidth, setTriggerWidth] = useState<number | null>(null);
const [tabWidths, setTabWidths] = useState<number[] | null>(null);

const navigationNodeRef = useRef<ViewInstance | null>(null);
const triggerNodeRef = useRef<ViewInstance | null>(null);
const tabRefs = useRef<(ViewInstance | null)[]>([]);

const measuredWidths = useRef<number[]>(null);
const measuredCount = useRef<number>(0);

function updateTabWidth(index: number, width: number) {
const widths = (measuredWidths.current ??= Array.from({ length: tabs }, () => 0));
if (widths[index] === width) {
return;
}
if (!widths[index]) {
measuredCount.current += 1;
}
widths[index] = width;
if (measuredCount.current === tabs) {
setTabWidths([...widths]);
}
}

const onLayout = useEffectEvent(() => {
if (navigationNodeRef.current) {
const { width } = navigationNodeRef.current.getBoundingClientRect();
setNavigationWidth(width);
}
if (triggerNodeRef.current) {
const { width } = triggerNodeRef.current.getBoundingClientRect();
setTriggerWidth(width);
}
tabRefs.current.forEach((tab, index) => {
if (tab) {
const { width } = tab.getBoundingClientRect();
updateTabWidth(index, width);
}
});
});

useLayoutEffect(() => {
onLayout();
}, []);

const measuring = navigationWidth === null || tabWidths === null || triggerWidth === null;

let visible = tabs;

if (!measuring) {
const totalWidth = tabWidths.reduce((sum, width) => sum + width, 0) + gap * (tabs - 1);

if (totalWidth > navigationWidth) {
visible = 0;
let usedWidth = 0;
for (let index = 0; index < tabs; index++) {
const withTab = usedWidth + tabWidths[index] + (index > 0 ? gap : 0);
const hasTriggerAfter = index < tabs - 1;
const withTrigger = withTab + (hasTriggerAfter ? gap + triggerWidth : 0);
if (withTrigger > navigationWidth) {
break;
}
usedWidth = withTab;
visible = index + 1;
}
}
}

return {
visible,
measuring,
navigationRef: (node: ViewInstance | null) => {
navigationNodeRef.current = node;
},
tabRef: (index: number) => (node: ViewInstance | null) => {
tabRefs.current[index] = node;
},
triggerRef: (node: ViewInstance | null) => {
triggerNodeRef.current = node;
},
onNavigationLayout: (event: LayoutChangeEvent) =>
setNavigationWidth(event.nativeEvent.layout.width),
onTabLayout:
(index: number) =>
({
nativeEvent: {
layout: { width },
},
}: LayoutChangeEvent) =>
updateTabWidth(index, width),
onTriggerLayout: (event: LayoutChangeEvent) => setTriggerWidth(event.nativeEvent.layout.width),
};
}
4 changes: 2 additions & 2 deletions scenes/PackageCodeScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import ContentContainer from '~/components/ContentContainer';
import CodeBrowser from '~/components/Package/CodeBrowser';
import { type CodeBrowserSettingsType } from '~/components/Package/CodeBrowser/CodeBrowserSettings';
import PackageVersionSelector from '~/components/Package/CodeBrowser/PackageVersionSelector';
import DetailsNavigation from '~/components/Package/DetailsNavigation';
import NavigationHeader from '~/components/Package/Navigation/NavigationHeader';
import NotFound from '~/components/Package/NotFound';
import PackageHeader from '~/components/Package/PackageHeader';
import PageMeta from '~/components/PageMeta';
Expand Down Expand Up @@ -152,7 +152,7 @@ export default function PackageCodeScene({ apiData, packageName }: PackageCodePa
description="See package directory score details"
path="package"
/>
<DetailsNavigation library={library} />
<NavigationHeader library={library} />
<ContentContainer style={tw`my-6 px-5 pb-3`}>
<View style={tw`flex-1 gap-3`}>
<View
Expand Down
4 changes: 2 additions & 2 deletions scenes/PackageOverviewScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import UpdatedAtView from '~/components/Library/UpdatedAtView';
import CollapsibleSection from '~/components/Package/CollapsibleSection';
import CommunitySection from '~/components/Package/CommunitySection';
import DependenciesSection from '~/components/Package/DependenciesSection';
import DetailsNavigation from '~/components/Package/DetailsNavigation';
import DownloadsSection from '~/components/Package/DownloadsSection';
import EntityCounter from '~/components/Package/EntityCounter';
import ExampleBox from '~/components/Package/ExampleBox';
import MarkdownContentBox from '~/components/Package/MarkdownContentBox';
import NavigationHeader from '~/components/Package/Navigation/NavigationHeader';
import NotFound from '~/components/Package/NotFound';
import PackageHeader from '~/components/Package/PackageHeader';
import TopicsSection from '~/components/Package/TopicsSection';
Expand Down Expand Up @@ -51,7 +51,7 @@ export default function PackageOverviewScene({
description={`See ${library.npmPkg} package detailed information and metadata`}
path="package"
/>
<DetailsNavigation library={library} />
<NavigationHeader library={library} />
<ContentContainer style={tw`px-0 pb-6 pt-3`}>
<View style={[tw`flex-row gap-8 px-5 py-3`, isSmallScreen && tw`flex-col gap-5`]}>
<View style={tw`flex-1 gap-3`}>
Expand Down
4 changes: 2 additions & 2 deletions scenes/PackageScoreScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { View } from 'react-native';
import { A, Caption, H6Section, useLayout } from '~/common/styleguide';
import ContentContainer from '~/components/ContentContainer';
import { DirectoryScore } from '~/components/Library/DirectoryScore';
import DetailsNavigation from '~/components/Package/DetailsNavigation';
import NavigationHeader from '~/components/Package/Navigation/NavigationHeader';
import NotFound from '~/components/Package/NotFound';
import PackageHeader from '~/components/Package/PackageHeader';
import PageMeta from '~/components/PageMeta';
Expand All @@ -28,7 +28,7 @@ export default function PackageScoreScene({ apiData, packageName }: PackageScore
description="See package directory score details"
path="package"
/>
<DetailsNavigation library={library} />
<NavigationHeader library={library} />
<ContentContainer style={tw`my-6 px-5 pb-3`}>
<View style={tw`flex-1 gap-3`}>
<PackageHeader library={library} skipDescription />
Expand Down
4 changes: 2 additions & 2 deletions scenes/PackageVersionsScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { View } from 'react-native';
import { H6Section } from '~/common/styleguide';
import ContentContainer from '~/components/ContentContainer';
import ChartSectionHeader from '~/components/Package/Charts/ChartSectionHeader';
import DetailsNavigation from '~/components/Package/DetailsNavigation';
import NavigationHeader from '~/components/Package/Navigation/NavigationHeader';
import NotFound from '~/components/Package/NotFound';
import PackageHeader from '~/components/Package/PackageHeader';
import ThreeDotsLoader from '~/components/Package/ThreeDotsLoader';
Expand Down Expand Up @@ -71,7 +71,7 @@ export default function PackageVersionsScene({
description={`See ${library.npmPkg} package published versions information and metadata`}
path="package"
/>
<DetailsNavigation library={library} />
<NavigationHeader library={library} />
<ContentContainer style={tw`my-6 px-5 pb-3`}>
<View style={tw`flex-1 gap-3`}>
<PackageHeader library={library} skipDescription />
Expand Down