Skip to content

Commit 8c708dc

Browse files
authored
add package version selector to the code browser (#2423)
1 parent 9d885b5 commit 8c708dc

15 files changed

Lines changed: 347 additions & 22 deletions

File tree

bun.lock

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/styleguide.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,16 +148,27 @@ export function A({
148148
);
149149
}
150150

151-
type HoverEffectProps = PressableProps & { style?: StyleProp<ViewStyle> };
151+
type HoverEffectProps = PressableProps & {
152+
style?: StyleProp<ViewStyle>;
153+
hoveredStyle?: Style;
154+
pressedStyle?: Style;
155+
};
152156

153-
export function HoverEffect({ children, style, onPress, ...rest }: HoverEffectProps) {
157+
export function HoverEffect({
158+
children,
159+
style,
160+
hoveredStyle,
161+
pressedStyle,
162+
onPress,
163+
...rest
164+
}: HoverEffectProps) {
154165
return (
155166
<Pressable
156167
style={({ hovered, pressed }) => [
157168
{ transition: 'opacity 0.33s' },
158-
hovered && tw`opacity-75`,
159-
pressed && tw`opacity-50`,
160169
style,
170+
hovered && (hoveredStyle ?? tw`opacity-75`),
171+
pressed && (pressedStyle ?? tw`opacity-50`),
161172
]}
162173
accessible={false}
163174
focusable={false}

components/Package/CodeBrowser/CodeBrowserContent.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Props = {
3434
packageName: string;
3535
isBrowserMaximized: boolean;
3636
toggleMaximized: () => void;
37+
selectedVersion: string;
3738
repoUrl: string;
3839
filePath: string;
3940
fileData?: UnpkgMeta['files'][number];
@@ -43,6 +44,7 @@ export default function CodeBrowserContent({
4344
packageName,
4445
isBrowserMaximized,
4546
toggleMaximized,
47+
selectedVersion,
4648
repoUrl,
4749
filePath,
4850
fileData,
@@ -68,7 +70,7 @@ export default function CodeBrowserContent({
6870

6971
const { data, isLoading } = useSWR<string>(
7072
!isPreviewDisabled && (!isImageFile || (isImageFile && rawPreview))
71-
? `/api/proxy/unpkg?name=${packageName}&path=${filePath.replaceAll('+', '%2B')}`
73+
? `/api/proxy/unpkg?name=${packageName}&version=${selectedVersion}&path=${filePath.replaceAll('+', '%2B')}`
7274
: undefined,
7375
(url: string) =>
7476
fetch(url).then(res => {
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import * as Popover from '@radix-ui/react-popover';
2+
import { useMemo, useRef, useState } from 'react';
3+
import { type ColorValue, ScrollView, TextInput, View } from 'react-native';
4+
import useSWR from 'swr';
5+
import { useDebounce } from 'use-debounce';
6+
7+
import { Label } from '~/common/styleguide';
8+
import { ArrowIcon } from '~/components/Icons';
9+
import ThreeDotsLoader from '~/components/Package/ThreeDotsLoader';
10+
import SelectorGroupHeader from '~/components/Selector/SelectorGroupHeader';
11+
import SelectorItemHoverEffect from '~/components/Selector/SelectorItemHoverEffect';
12+
import { type PackageVersionsOnlyData } from '~/types';
13+
import { TimeRange } from '~/util/datetime';
14+
import tw from '~/util/tailwind';
15+
16+
type Props = {
17+
packageName: string;
18+
selectedVersion: string;
19+
setVersion: (selectedVersion: string) => void;
20+
};
21+
22+
export default function PackageVersionSelector({
23+
packageName,
24+
selectedVersion,
25+
setVersion,
26+
}: Props) {
27+
const [open, setOpen] = useState(false);
28+
const [search, setSearch] = useState('');
29+
const [debouncedSearch] = useDebounce(search, 150);
30+
const inputRef = useRef<TextInput>(null);
31+
32+
const { data, isLoading } = useSWR<PackageVersionsOnlyData>(
33+
`/api/proxy/npm-registry-versions?name=${packageName}&versionsOnly=true`,
34+
(url: string) => fetch(url).then(res => res.json()),
35+
{
36+
dedupingInterval: TimeRange.HOUR * 1000,
37+
revalidateOnFocus: false,
38+
}
39+
);
40+
41+
const filteredVersions = useMemo(() => {
42+
const query = debouncedSearch.trim();
43+
if (!query) {
44+
return data?.versions ?? [];
45+
}
46+
return (data?.versions ?? []).filter(v => v.includes(query));
47+
}, [data?.versions, debouncedSearch]);
48+
49+
const filteredDistTags = useMemo(() => {
50+
if (!data?.['dist-tags']) {
51+
return null;
52+
}
53+
const query = debouncedSearch.trim();
54+
const entries = Object.entries(data['dist-tags']);
55+
if (!query) {
56+
return entries;
57+
}
58+
const filtered = entries.filter(
59+
([tag, version]) => tag.includes(query) || version.includes(query)
60+
);
61+
return filtered.length > 0 ? filtered : null;
62+
}, [data, debouncedSearch]);
63+
64+
function handleOpenChange(next: boolean) {
65+
setOpen(next);
66+
if (!next) {
67+
setSearch('');
68+
} else {
69+
setTimeout(() => inputRef.current?.focus(), 0);
70+
}
71+
}
72+
73+
function handleSelect(version: string) {
74+
setVersion(version);
75+
setOpen(false);
76+
setSearch('');
77+
}
78+
79+
return (
80+
<Popover.Root open={open} onOpenChange={handleOpenChange}>
81+
<Popover.Trigger asChild>
82+
<View
83+
style={tw`min-h-8 w-[180px] cursor-pointer justify-center overflow-hidden rounded-lg border border-palette-gray2 bg-default px-2 dark:border-default dark:bg-dark`}>
84+
{isLoading ? (
85+
<View style={tw`scale-75 text-center`}>
86+
<ThreeDotsLoader />
87+
</View>
88+
) : (
89+
<View style={tw`flex-row items-center justify-between gap-1.5`}>
90+
<Label numberOfLines={1} style={tw`select-none text-[13px]`}>
91+
{selectedVersion}
92+
</Label>
93+
<ArrowIcon
94+
style={[tw`h-3 w-4 shrink-0 text-icon`, open ? tw`rotate-270` : tw`rotate-90`]}
95+
/>
96+
</View>
97+
)}
98+
</View>
99+
</Popover.Trigger>
100+
<Popover.Portal>
101+
<Popover.Content align="end" sideOffset={6}>
102+
<View
103+
style={tw`w-56 overflow-hidden rounded-lg border border-palette-gray2 bg-default shadow-lg dark:border-default`}>
104+
<View style={tw`border-b border-palette-gray2 dark:border-default`}>
105+
<TextInput
106+
ref={inputRef}
107+
value={search}
108+
onChangeText={setSearch}
109+
placeholder="Filter versions…"
110+
style={tw`px-2.5 py-1.5 text-sm text-black outline-0 dark:text-white`}
111+
autoCorrect={false}
112+
autoCapitalize="none"
113+
placeholderTextColor={
114+
tw.prefixMatch('dark')
115+
? (tw`text-palette-gray5`.color as ColorValue)
116+
: (tw`text-palette-gray3`.color as ColorValue)
117+
}
118+
/>
119+
</View>
120+
<ScrollView style={tw`max-h-76`} keyboardShouldPersistTaps="handled" id="dropdown-list">
121+
{filteredDistTags && (
122+
<>
123+
<SelectorGroupHeader>Dist tags</SelectorGroupHeader>
124+
{filteredDistTags.map(([tag, version]) => (
125+
<SelectorItemHoverEffect key={tag}>
126+
<View onPointerDown={() => handleSelect(tag)}>
127+
<Label
128+
style={[
129+
tw`text-[inherit]`,
130+
selectedVersion === tag && tw`text-primary-darker dark:text-primary`,
131+
]}>
132+
{tag}
133+
</Label>
134+
<Label style={tw`text-[10px] font-thin text-secondary`}>{version}</Label>
135+
</View>
136+
</SelectorItemHoverEffect>
137+
))}
138+
{filteredVersions.length > 0 && (
139+
<View style={tw`mt-1 border-b border-palette-gray2 dark:border-default`} />
140+
)}
141+
</>
142+
)}
143+
{filteredVersions.length > 0 && (
144+
<>
145+
<SelectorGroupHeader>Versions</SelectorGroupHeader>
146+
{filteredVersions.map(version => (
147+
<SelectorItemHoverEffect key={version}>
148+
<View onPointerDown={() => handleSelect(version)}>
149+
<Label
150+
style={[
151+
tw`text-[inherit]`,
152+
selectedVersion === version &&
153+
tw`text-primary-darker dark:text-primary`,
154+
]}>
155+
{version}
156+
</Label>
157+
</View>
158+
</SelectorItemHoverEffect>
159+
))}
160+
</>
161+
)}
162+
{filteredVersions.length === 0 && !filteredDistTags && (
163+
<View style={tw`px-3 py-2`}>
164+
<Label style={tw`text-center font-thin text-secondary`}>No versions match</Label>
165+
</View>
166+
)}
167+
</ScrollView>
168+
</View>
169+
</Popover.Content>
170+
</Popover.Portal>
171+
</Popover.Root>
172+
);
173+
}

0 commit comments

Comments
 (0)