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
106 changes: 54 additions & 52 deletions bun.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type Props = {

const SHIKI_OPTS = { langAlias: { gradle: 'groovy' } } as const;

export default function ReadmeCodeBlock({ code, theme, lang }: Props) {
export default function MarkdownCodeBlock({ code, theme, lang }: Props) {
const highlighter = useShikiHighlighter(code, lang, theme, SHIKI_OPTS);

const copyButton = <CopyButton data={code} tooltip="Copy code" label="Copy code to clipboard" />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type Dispatch, type SetStateAction, useState } from 'react';
import { useState } from 'react';
import { Pressable, View } from 'react-native';

import { P } from '~/common/styleguide';
Expand All @@ -8,25 +8,26 @@ import tw from '~/util/tailwind';
type Props = {
tab: MarkdownTab;
activeTab: MarkdownTabsType;
setActiveTab: Dispatch<SetStateAction<MarkdownTabsType>>;
onPress?: (tab: MarkdownTabsType) => void;
};

export default function MarkdownContentTab({ tab, activeTab, setActiveTab }: Props) {
export default function MarkdownContentTab({ tab, activeTab, onPress }: Props) {
const [isHovered, setHovered] = useState<boolean>(false);
const Element = onPress ? Pressable : View;
return (
<Pressable
onPress={() => setActiveTab(tab.title)}
onPointerEnter={() => setHovered(true)}
onPointerLeave={() => setHovered(false)}
<Element
onPress={() => onPress?.(tab.title)}
onPointerEnter={() => onPress && setHovered(true)}
onPointerLeave={() => onPress && setHovered(false)}
style={[
tw`relative my-1.5 flex-row items-center gap-2 rounded px-2 py-1.5`,
tw`relative my-1.5 select-none flex-row items-center gap-2 rounded px-2 py-1.5`,
isHovered && tw`bg-palette-gray1 dark:bg-palette-gray7`,
]}>
<tab.Icon style={tw`size-5 text-tertiary dark:text-pewter`} />
<P style={tw`text-[14px]`}>{tab.title}</P>
{tab.title === activeTab && (
<View style={tw`absolute -bottom-1.5 left-0 h-0.5 w-full rounded bg-primary-dark`} />
)}
</Pressable>
</Element>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type Props = PropsWithChildren<{
tagName: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
}>;

export default function ReadmeHeading({ children, tagName }: Props) {
export default function MarkdownHeading({ children, tagName }: Props) {
const Heading = tagName;
const slug = typeof children === 'string' ? kebabCase(children) : undefined;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Md } from '@m2d/react-markdown/client';
import { capitalize } from 'es-toolkit/string';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { useEffect, useMemo, useState } from 'react';
import { View } from 'react-native';
import { type Theme } from 'react-shiki';
import rehypeRaw from 'rehype-raw';
Expand All @@ -12,8 +13,7 @@ import useSWR from 'swr';
import { A, P } from '~/common/styleguide';
import { CCFile, ChangelogFile, Check, ContributingFile, ReadmeFile } from '~/components/Icons';
import CopyButton from '~/components/Package/CopyButton';
import MarkdownContentTab from '~/components/Package/MarkdownContentTab';
import ReadmeHeading from '~/components/Package/ReadmeHeading';
import ThreeDotsLoader from '~/components/Package/ThreeDotsLoader';
import rndDark from '~/styles/shiki/rnd-dark.json';
import rndLight from '~/styles/shiki/rnd-light.json';
import { type LibraryType, type MarkdownTab, type MarkdownTabsType } from '~/types';
Expand All @@ -23,8 +23,10 @@ import { getReadmeAssetURL } from '~/util/getReadmeAssetUrl';
import { parseGitHubUrl } from '~/util/parseGitHubUrl';
import tw from '~/util/tailwind';

import ReadmeCodeBlock from './ReadmeCodeBlock';
import ThreeDotsLoader from './ThreeDotsLoader';
import MarkdownCodeBlock from './MarkdownCodeBlock';
import MarkdownContentTab from './MarkdownContentTab';
import MarkdownHeading from './MarkdownHeading';
import { DEFAULT_MARKDOWN_TAB, MARKDOWN_CONTENT_QUERY_PARAM, parseMarkdownTab } from './utils';

type Props = {
packageName?: string;
Expand All @@ -33,45 +35,63 @@ type Props = {
};

export default function MarkdownContentBox({ packageName, library, loader = false }: Props) {
const [activeTab, setActiveTab] = useState<MarkdownTabsType>('Readme');
const router = useRouter();
const repoUrl = library?.github.urls.repo;

const contentTabs: MarkdownTab[] = [
{
title: 'Readme' as const,
Icon: ReadmeFile,
url: library?.template
? getTabContentUrl(library, 'README.md')
: `https://unpkg.com/${packageName}/README.md`,
},
...(library?.github?.hasChangelog
? [
{
title: 'Changelog' as const,
Icon: ChangelogFile,
url: getTabContentUrl(library, 'CHANGELOG.md'),
},
]
: []),
...(library?.github?.hasContributing
? [
{
title: 'Contributing' as const,
Icon: ContributingFile,
url: getTabContentUrl(library, 'CONTRIBUTING.md'),
},
]
: []),
...(library?.github?.hasCC
? [
{
title: 'Code of Conduct' as const,
Icon: CCFile,
url: getTabContentUrl(library, 'CODE_OF_CONDUCT.md'),
},
]
: []),
].flat();
const contentTabs = useMemo<MarkdownTab[]>(
() =>
[
{
title: 'Readme' as const,
Icon: ReadmeFile,
url: library?.template
? getTabContentUrl(library, 'README.md')
: `https://unpkg.com/${packageName}/README.md`,
},
...(library?.github?.hasChangelog
? [
{
title: 'Changelog' as const,
Icon: ChangelogFile,
url: getTabContentUrl(library, 'CHANGELOG.md'),
},
]
: []),
...(library?.github?.hasContributing
? [
{
title: 'Contributing' as const,
Icon: ContributingFile,
url: getTabContentUrl(library, 'CONTRIBUTING.md'),
},
]
: []),
...(library?.github?.hasCC
? [
{
title: 'Code of Conduct' as const,
Icon: CCFile,
url: getTabContentUrl(library, 'CODE_OF_CONDUCT.md'),
},
]
: []),
].flat(),
[library, packageName]
);

const availableTabs = useMemo<MarkdownTabsType[]>(
() => contentTabs.map(({ title }) => title),
[contentTabs]
);
const routeTab = useMemo(
() => parseMarkdownTab(router.query[MARKDOWN_CONTENT_QUERY_PARAM], availableTabs),
[availableTabs, router.query]
);
const [activeTab, setActiveTab] = useState<MarkdownTabsType>(routeTab);

useEffect(() => {
setActiveTab(currentTab => (currentTab === routeTab ? currentTab : routeTab));
}, [routeTab]);

const { data, error, isLoading } = useSWR(
contentTabs.find(({ title }) => title === activeTab)?.url,
Expand Down Expand Up @@ -111,6 +131,29 @@ export default function MarkdownContentBox({ packageName, library, loader = fals
}
}, [noData]);

function handleTabChange(nextTab: MarkdownTabsType) {
if (nextTab === activeTab) {
return;
}

setActiveTab(nextTab);

const url = new URL(window.location.href);

if (nextTab === DEFAULT_MARKDOWN_TAB) {
url.searchParams.delete(MARKDOWN_CONTENT_QUERY_PARAM);
} else {
url.searchParams.set(MARKDOWN_CONTENT_QUERY_PARAM, nextTab);
}

url.hash = '';

void router.replace(`${url.pathname}${url.search}`, undefined, {
shallow: true,
scroll: false,
});
}

return (
<View
style={tw`my-2 rounded-xl border border-palette-gray2 text-black dark:border-default dark:text-white`}>
Expand All @@ -120,7 +163,7 @@ export default function MarkdownContentBox({ packageName, library, loader = fals
<MarkdownContentTab
tab={tab}
activeTab={activeTab}
setActiveTab={setActiveTab}
onPress={availableTabs.length > 1 ? handleTabChange : undefined}
key={`tab-${tab.title.toLocaleLowerCase()}`}
/>
))}
Expand All @@ -144,22 +187,22 @@ export default function MarkdownContentBox({ packageName, library, loader = fals
id="markdownContentContainer"
components={{
h1: ({ children, node }: any) => (
<ReadmeHeading tagName={node.tagName}>{children}</ReadmeHeading>
<MarkdownHeading tagName={node.tagName}>{children}</MarkdownHeading>
),
h2: ({ children, node }: any) => (
<ReadmeHeading tagName={node.tagName}>{children}</ReadmeHeading>
<MarkdownHeading tagName={node.tagName}>{children}</MarkdownHeading>
),
h3: ({ children, node }: any) => (
<ReadmeHeading tagName={node.tagName}>{children}</ReadmeHeading>
<MarkdownHeading tagName={node.tagName}>{children}</MarkdownHeading>
),
h4: ({ children, node }: any) => (
<ReadmeHeading tagName={node.tagName}>{children}</ReadmeHeading>
<MarkdownHeading tagName={node.tagName}>{children}</MarkdownHeading>
),
h5: ({ children, node }: any) => (
<ReadmeHeading tagName={node.tagName}>{children}</ReadmeHeading>
<MarkdownHeading tagName={node.tagName}>{children}</MarkdownHeading>
),
h6: ({ children, node }: any) => (
<ReadmeHeading tagName={node.tagName}>{children}</ReadmeHeading>
<MarkdownHeading tagName={node.tagName}>{children}</MarkdownHeading>
),
br: () => null,
hr: () => null,
Expand Down Expand Up @@ -217,7 +260,7 @@ export default function MarkdownContentBox({ packageName, library, loader = fals
const langClass = children?.props?.className;
if (langClass) {
return (
<ReadmeCodeBlock
<MarkdownCodeBlock
code={children.props.children}
theme={(tw.prefixMatch('dark') ? rndDark : rndLight) as Theme}
lang={langClass ? (langClass.split('-')[1] ?? 'sh').toLowerCase() : 'sh'}
Expand Down
25 changes: 25 additions & 0 deletions components/Package/MarkdownContentBox/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { type MarkdownTabsType } from '~/types';

export const MARKDOWN_CONTENT_QUERY_PARAM = 'tab';
export const MARKDOWN_TABS = ['Readme', 'Changelog', 'Contributing', 'Code of Conduct'] as const;
export const DEFAULT_MARKDOWN_TAB: MarkdownTabsType = 'Readme';

export function isValidMarkdownTab(
value?: string | string[],
availableTabs: readonly MarkdownTabsType[] = MARKDOWN_TABS
): value is MarkdownTabsType {
return typeof value === 'string' && availableTabs.includes(value as MarkdownTabsType);
}

export function parseMarkdownTab(
value?: string | string[],
availableTabs: readonly MarkdownTabsType[] = MARKDOWN_TABS
): MarkdownTabsType {
if (isValidMarkdownTab(value, availableTabs)) {
return value;
}

return availableTabs.includes(DEFAULT_MARKDOWN_TAB)
? DEFAULT_MARKDOWN_TAB
: (availableTabs[0] ?? DEFAULT_MARKDOWN_TAB);
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,12 @@
"browserslist": "^4.28.1",
"cheerio": "^1.2.0",
"dotenv": "^17.3.1",
"lint-staged": "^16.3.3",
"lint-staged": "^16.4.0",
"next-compose-plugins": "^2.2.1",
"next-fonts": "^1.5.1",
"next-images": "^1.8.5",
"oxfmt": "^0.39.0",
"oxlint": "^1.54.0",
"oxfmt": "^0.40.0",
"oxlint": "^1.55.0",
"oxlint-tsgolint": "^0.16.0",
"simple-git-hooks": "^2.13.1",
"typescript": "^5.9.3",
Expand Down
4 changes: 1 addition & 3 deletions util/queryParams.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { omit } from 'es-toolkit/object';
import { type NextRouter } from 'next/router';

import { CHART_MODE_QUERY_PARAM } from '~/components/Package/VersionDownloadsChart/utils';

export function parseQueryParams(params: Partial<Record<string, string | string[]>>) {
return Object.fromEntries(
Object.entries(params).map(([key, val]) => [
Expand All @@ -13,7 +11,7 @@ export function parseQueryParams(params: Partial<Record<string, string | string[
}

export function replaceQueryParam(router: NextRouter, paramName: string, paramValue?: string) {
const queryParams = omit(router.query, [CHART_MODE_QUERY_PARAM, paramName]);
const queryParams = omit(router.query, [paramName]);

void router.replace(
{
Expand Down