refactor: website facelift (#10823)

This commit is contained in:
Noel
2025-04-10 22:02:37 +02:00
committed by GitHub
parent 1fe53c7ca2
commit 2e3bc69602
80 changed files with 6136 additions and 2529 deletions

View File

@@ -6,3 +6,4 @@ public/searchIndex
src/assets/readme src/assets/readme
src/styles/unocss.css src/styles/unocss.css
next-env.d.ts next-env.d.ts
src/util/shiki.bundle.ts

View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -1,13 +1,19 @@
/** import type { NextConfig } from 'next';
* @type {import('next').NextConfig}
*/
export default { export default {
reactStrictMode: true, reactStrictMode: true,
images: { images: {
dangerouslyAllowSVG: true, dangerouslyAllowSVG: true,
contentDispositionType: 'attachment', contentDispositionType: 'attachment',
contentSecurityPolicy: "default-src 'self'; frame-src 'none'; sandbox;", contentSecurityPolicy: "default-src 'self'; frame-src 'none'; sandbox;",
remotePatterns: [
{
protocol: 'http',
hostname: 'localhost',
},
],
}, },
poweredByHeader: false,
logging: { logging: {
fetches: { fetches: {
fullUrl: true, fullUrl: true,
@@ -16,6 +22,8 @@ export default {
experimental: { experimental: {
ppr: true, ppr: true,
reactCompiler: true, reactCompiler: true,
useCache: true,
dynamicOnHover: true,
}, },
eslint: { eslint: {
ignoreDuringBuilds: true, ignoreDuringBuilds: true,
@@ -37,4 +45,4 @@ export default {
}, },
]; ];
}, },
}; } satisfies NextConfig;

View File

@@ -13,7 +13,7 @@
"build:search_indices": "pnpm node scripts/generateAllIndices.js", "build:search_indices": "pnpm node scripts/generateAllIndices.js",
"build:analyze": "turbo run docs --filter='@discordjs/*' --concurrency=4 && cross-env ANALYZE=true NEXT_PUBLIC_LOCAL_DEV=true pnpm run build:prod", "build:analyze": "turbo run docs --filter='@discordjs/*' --concurrency=4 && cross-env ANALYZE=true NEXT_PUBLIC_LOCAL_DEV=true pnpm run build:prod",
"preview": "next start", "preview": "next start",
"dev": "next dev", "dev": "next dev --turbopack",
"lint": "pnpm run build:check && prettier --check . && cross-env TIMING=1 eslint --format=pretty src ", "lint": "pnpm run build:check && prettier --check . && cross-env TIMING=1 eslint --format=pretty src ",
"format": "pnpm run build:check && prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src ", "format": "pnpm run build:check && prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src ",
"fmt": "pnpm run format" "fmt": "pnpm run format"
@@ -46,57 +46,78 @@
"homepage": "https://discord.js.org", "homepage": "https://discord.js.org",
"funding": "https://github.com/discordjs/discord.js?sponsor", "funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": { "dependencies": {
"@radix-ui/react-collapsible": "^1.1.2", "@radix-ui/react-collapsible": "^1.1.3",
"@react-icons/all-files": "^4.1.0", "@react-icons/all-files": "^4.1.0",
"@vercel/analytics": "^1.4.1", "@vercel/analytics": "^1.5.0",
"@vercel/edge-config": "^1.4.0", "@vercel/edge-config": "^1.4.0",
"@vercel/og": "^0.6.4", "@vercel/og": "^0.6.8",
"@vercel/postgres": "^0.9.0", "@vercel/postgres": "^0.10.0",
"cmdk": "^1.0.4", "cmdk": "^1.1.1",
"cva": "1.0.0-beta.3",
"geist": "^1.3.1", "geist": "^1.3.1",
"jotai": "^2.11.0", "immer": "^10.1.1",
"lucide-react": "^0.379.0", "jotai": "^2.12.2",
"meilisearch": "^0.40.0", "jotai-immer": "^0.4.1",
"next": "15.0.0-rc.0", "lucide-react": "^0.487.0",
"next-mdx-remote-client": "^1.0.3", "meilisearch": "^0.49.0",
"next-themes": "^0.3.0", "motion": "^12.6.3",
"overlayscrollbars": "^2.10.1", "next": "15.3.1-canary.2",
"next-mdx-remote-client": "^2.1.1",
"next-themes": "^0.4.6",
"nuqs": "^2.4.1",
"overlayscrollbars": "^2.11.1",
"overlayscrollbars-react": "^0.5.6", "overlayscrollbars-react": "^0.5.6",
"react": "19.1.0", "react": "^19.1.0",
"react-aria-components": "^1.5.0", "react-aria": "^3.38.1",
"react-dom": "19.1.0", "react-aria-components": "^1.7.1",
"react-dom": "^19.1.0",
"react-error-boundary": "^5.0.0",
"sharp": "^0.33.5", "sharp": "^0.33.5",
"usehooks-ts": "^3.1.0", "tailwind-merge": "^3.1.0",
"tw-animate-css": "^1.2.5",
"usehooks-ts": "^3.1.1",
"vaul": "^1.1.2" "vaul": "^1.1.2"
}, },
"devDependencies": { "devDependencies": {
"@shikijs/rehype": "^1.24.4", "@next/env": "^15.2.4",
"@tailwindcss/typography": "^0.5.15", "@playwright/test": "^1.51.1",
"@shikijs/rehype": "^3.2.1",
"@tailwindcss/postcss": "^4.1.3",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.3",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@types/react": "^19.1.0", "@types/react": "^19.1.0",
"@types/react-dom": "^19.1.1", "@types/react-dom": "^19.1.1",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"@vitest/browser": "^3.1.1",
"@vitest/coverage-v8": "^2.1.8",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"babel-plugin-react-compiler": "0.0.0-experimental-592953e-20240517", "babel-plugin-react-compiler": "19.0.0-beta-e993439-20250328",
"cpy-cli": "^5.0.0", "cpy-cli": "^5.0.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^9.24.0", "eslint": "^9.24.0",
"eslint-config-neon": "^0.2.7", "eslint-config-neon": "^0.2.7",
"eslint-formatter-compact": "^8.40.0",
"eslint-formatter-pretty": "^6.0.1", "eslint-formatter-pretty": "^6.0.1",
"git-describe": "^4.1.1",
"happy-dom": "^17.4.4", "happy-dom": "^17.4.4",
"msw": "^2.7.3",
"playwright": "^1.51.1",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.5.14", "prettier-plugin-tailwindcss": "^0.6.11",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.1",
"remark-rehype": "^11.1.1", "remark-rehype": "^11.1.2",
"shiki": "^1.24.4", "shiki": "^3.2.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^4.1.3",
"tailwindcss-react-aria-components": "^2.0.0",
"turbo": "^2.5.0", "turbo": "^2.5.0",
"typescript": "~5.8.3", "typescript": "^5.8.2",
"vercel": "^41.4.1" "vercel": "^41.4.1",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^2.1.8",
"vitest-browser-react": "^0.1.1"
}, },
"engines": { "engines": {
"node": ">=22.12.0" "node": ">=22.12.0"

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -0,0 +1,5 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
},
};

View File

@@ -0,0 +1,14 @@
import { CmdKNoSRR } from '@/components/CmdK';
import { fetchDependencies } from '@/util/fetchDependencies';
export async function CmdK({
params,
}: {
readonly params: Promise<{ readonly packageName: string; readonly version: string }>;
}) {
const { packageName, version } = await params;
const dependencies = await fetchDependencies({ packageName, version });
return <CmdKNoSRR dependencies={dependencies} />;
}

View File

@@ -1,7 +1,7 @@
/* eslint-disable react/no-unknown-property */ /* eslint-disable react/no-unknown-property */
import { ImageResponse } from 'next/og'; import { ImageResponse } from 'next/og';
import { resolveKind } from '~/util/resolveNodeKind'; import { resolveKind } from '@/util/resolveNodeKind';
export const runtime = 'edge'; export const runtime = 'edge';
@@ -15,14 +15,24 @@ export const contentType = 'image/png';
export default async function Image({ export default async function Image({
params, params,
}: { }: {
readonly params: { readonly item: string; readonly packageName: string; readonly version: string }; readonly params: Promise<{ readonly item: string; readonly packageName: string; readonly version: string }>;
}) { }) {
const normalizeItem = params.item.split(encodeURIComponent(':')).join('.').toLowerCase(); const { item, packageName, version } = await params;
const isMainVersion = params.version === 'main'; const [fontDataBold, fontDataBlack] = await Promise.all([
fetch(new URL('../../../../../../assets/Geist-Bold.ttf', import.meta.url), {
next: { revalidate: 604_800 },
}).then(async (res) => res.arrayBuffer()),
fetch(new URL('../../../../../../assets/Geist-Black.ttf', import.meta.url), {
next: { revalidate: 604_800 },
}).then(async (res) => res.arrayBuffer()),
]);
const normalizeItem = item.split(encodeURIComponent(':')).join('.').toLowerCase();
const isMain = version === 'main';
const fileContent = await fetch( const fileContent = await fetch(
`${process.env.BLOB_STORAGE_URL}/rewrite/${params.packageName}/${params.version}.${normalizeItem}.api.json`, `${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.${normalizeItem}.api.json`,
{ next: isMainVersion ? { revalidate: 0 } : { revalidate: 604_800 } }, { next: { revalidate: isMain ? 0 : 604_800 } },
); );
const node = await fileContent.json(); const node = await fileContent.json();
@@ -30,7 +40,7 @@ export default async function Image({
( (
<div tw="flex bg-[#121212] h-full w-full p-14"> <div tw="flex bg-[#121212] h-full w-full p-14">
<div tw="flex flex-col mx-auto h-full text-white"> <div tw="flex flex-col mx-auto h-full text-white">
<div tw="flex text-4xl text-gray-400">{params.packageName}</div> <div tw="flex text-4xl text-gray-400">{packageName}</div>
<div tw="flex flex-col justify-between h-full w-full pt-14"> <div tw="flex flex-col justify-between h-full w-full pt-14">
<div tw="flex items-center max-w-full"> <div tw="flex items-center max-w-full">
<span tw="mr-6">{resolveKind(node.kind, 94)}</span> <span tw="mr-6">{resolveKind(node.kind, 94)}</span>
@@ -40,7 +50,7 @@ export default async function Image({
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
overflow: 'hidden', overflow: 'hidden',
}} }}
tw="text-[5.5rem] font-bold w-full" tw="text-[5.5rem] font-bold w-full"
> >
{node.displayName} {node.displayName}
</h2> </h2>
@@ -94,6 +104,20 @@ export default async function Image({
), ),
{ {
...size, ...size,
fonts: [
{
name: 'Geist',
data: fontDataBold,
weight: 700,
style: 'normal',
},
{
name: 'Geist',
data: fontDataBlack,
weight: 900,
style: 'normal',
},
],
}, },
); );
} }

View File

@@ -1,38 +1,44 @@
'use cache';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { DocItem } from '~/components/DocItem'; import { DocItem } from '@/components/DocItem';
import { fetchNode } from '~/util/fetchNode'; import { fetchNode } from '@/util/fetchNode';
export async function generateMetadata({ export async function generateMetadata({
params, params,
}: { }: {
readonly params: { readonly params: Promise<{
readonly item: string; readonly item: string;
readonly packageName: string; readonly packageName: string;
readonly version: string; readonly version: string;
}; }>;
}): Promise<Metadata> { }): Promise<Metadata> {
const normalizeItem = params.item.split(encodeURIComponent(':'))[0]; const { item, packageName, version } = await params;
const normalizeItem = item.split(encodeURIComponent(':'))[0];
return { return {
title: `${normalizeItem} (${params.packageName} - ${params.version})`, title: `${normalizeItem} (${packageName} - ${version})`,
}; };
} }
export default async function Page({ export default async function Page({
params, params,
}: { }: {
readonly params: { readonly item: string; readonly packageName: string; readonly version: string }; readonly params: Promise<{ readonly item: string; readonly packageName: string; readonly version: string }>;
}) { }) {
const node = await fetchNode({ item: params.item, packageName: params.packageName, version: params.version }); const { item, packageName, version } = await params;
const node = await fetchNode({ item, packageName, version });
if (!node) { if (!node) {
notFound(); notFound();
} }
return ( return (
<main className="flex w-full flex-col gap-8 pb-12 md:pb-0"> <main className="mx-auto flex w-full max-w-screen-xl flex-col gap-8 px-6 py-4">
<DocItem node={node} packageName={params.packageName} version={params.version} /> <DocItem node={node} packageName={packageName} version={version} />
</main> </main>
); );
} }

View File

@@ -1,24 +1,33 @@
import type { Metadata } from 'next'; 'use cache';
import dynamic from 'next/dynamic';
import type { PropsWithChildren } from 'react';
import { Navigation } from '~/components/Navigation';
import { OverlayScrollbarsComponent } from '~/components/OverlayScrollbars';
import { Drawer } from '~/components/ui/Drawer';
import { Footer } from '~/components/ui/Footer';
import { fetchDependencies } from '~/util/fetchDependencies';
// eslint-disable-next-line promise/prefer-await-to-then import { VscGithubInverted } from '@react-icons/all-files/vsc/VscGithubInverted';
const CmdK = dynamic(async () => import('~/components/ui/CmdK').then((mod) => mod.CmdK), { ssr: false }); import type { Metadata } from 'next';
import Link from 'next/link';
import { Suspense, type PropsWithChildren } from 'react';
import { Footer } from '@/components/Footer';
import { Navigation } from '@/components/Navigation';
import { Scrollbars } from '@/components/OverlayScrollbars';
import { PackageSelect } from '@/components/PackageSelect';
import { SearchButton } from '@/components/SearchButton';
import { ThemeSwitchNoSRR } from '@/components/ThemeSwitch';
import { VersionSelect } from '@/components/VersionSelect';
import { Sidebar, SidebarContent, SidebarHeader, SidebarInset, SidebarTrigger } from '@/components/ui/Sidebar';
import { buttonStyles } from '@/styles/ui/button';
import { ENV } from '@/util/env';
import { fetchVersions } from '@/util/fetchVersions';
import { CmdK } from './CmdK';
export async function generateMetadata({ export async function generateMetadata({
params, params,
}: { }: {
readonly params: { readonly packageName: string; readonly version: string }; readonly params: Promise<{ readonly packageName: string; readonly version: string }>;
}): Promise<Metadata> { }): Promise<Metadata> {
const { packageName, version } = await params;
return { return {
title: { title: {
template: '%s | discord.js', template: '%s | discord.js',
default: `${params.packageName} (${params.version})`, default: `${packageName} (${version})`,
}, },
}; };
} }
@@ -26,44 +35,74 @@ export async function generateMetadata({
export default async function Layout({ export default async function Layout({
params, params,
children, children,
}: PropsWithChildren<{ readonly params: { readonly packageName: string; readonly version: string } }>) { }: PropsWithChildren<{ readonly params: Promise<{ readonly packageName: string; readonly version: string }> }>) {
const dependencies = await fetchDependencies({ packageName: params.packageName, version: params.version }); const { packageName, version } = await params;
const versions = fetchVersions(packageName);
return ( return (
// eslint-disable-next-line react/no-unknown-property <>
<div className="mx-auto flex max-w-screen-2xl flex-col gap-12 p-6 md:flex-row" vaul-drawer-wrapper=""> <Sidebar closeButton={false} intent="inset">
<div className="sticky top-6 hidden flex-shrink-0 self-start md:block"> <SidebarHeader className="bg-[#f3f3f4] p-4 dark:bg-[#121214]">
<OverlayScrollbarsComponent <div className="flex flex-col gap-2">
className="max-h-[calc(100dvh-48px)]" <div className="flex place-content-between place-items-center p-1">
defer <Link className="text-xl font-bold" href={`/docs/packages/${packageName}/${version}`}>
options={{ {packageName}
overflow: { x: 'hidden' }, </Link>
scrollbars: { <div className="flex place-items-center gap-2">
autoHide: 'scroll', <Link
autoHideDelay: 500, aria-label="GitHub"
autoHideSuspend: true, className={buttonStyles({ variant: 'filled', size: 'icon-sm' })}
clickScroll: true, href="https://github.com/discordjs/discord.js"
}, rel="external noopener noreferrer"
}} target="_blank"
> >
<Navigation className="pr-4" packageName={params.packageName} version={params.version} /> <VscGithubInverted aria-hidden data-slot="icon" size={18} />
</OverlayScrollbarsComponent> </Link>
</div> <ThemeSwitchNoSRR />
<div className="pb-12"> </div>
{children} </div>
<Footer /> <PackageSelect />
</div> {/* <h3 className="p-1 text-lg font-semibold">{version}</h3> */}
<div className="fixed bottom-0 left-0 right-0 md:hidden"> <VersionSelect versionsPromise={versions} />
<Drawer> <SearchButton />
<Navigation </div>
className="max-w-none overflow-auto p-0 lg:max-w-none" </SidebarHeader>
drawer <SidebarContent className="bg-[#f3f3f4] p-0 py-4 pl-4 dark:bg-[#121214]">
packageName={params.packageName} <Scrollbars>
version={params.version} <Navigation packageName={packageName} version={version} />
/> </Scrollbars>
</Drawer> </SidebarContent>
</div> </Sidebar>
<CmdK dependencies={dependencies} /> <SidebarInset>
</div> {ENV.IS_LOCAL_DEV ? (
<div className="sticky top-0 z-10 flex place-content-center place-items-center border border-red-400/35 bg-red-500/65 p-2 px-4 text-center text-base text-white shadow-md backdrop-blur">
Local test environment
</div>
) : null}
{ENV.IS_PREVIEW ? (
<div className="sticky top-0 z-10 flex place-content-center place-items-center border border-red-400/35 bg-red-500/65 p-2 px-4 text-center text-base text-white shadow-md backdrop-blur">
Preview environment
</div>
) : null}
<div className="bg-[#fbfbfb] pb-12 dark:bg-[#1a1a1e]">
<div className="relative px-6 pt-6 md:hidden">
<div className="fixed top-5 left-6 z-20 md:hidden">
<SidebarTrigger aria-label="Navigation" size="icon" variant="filled" />
</div>
<div className="flex place-content-end">
<Link className="text-xl font-bold" href={`/docs/packages/${packageName}/${version}`}>
{packageName}
</Link>
</div>
</div>
{children}
<Footer />
</div>
</SidebarInset>
<Suspense>
<CmdK params={params} />
</Suspense>
</>
); );
} }

View File

@@ -1,29 +1,19 @@
'use cache';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { join } from 'node:path'; import { join } from 'node:path';
import rehypeShikiFromHighlighter from '@shikijs/rehype/core'; import rehypeShikiFromHighlighter from '@shikijs/rehype/core';
import { MDXRemote } from 'next-mdx-remote-client/rsc'; import { MDXRemote } from 'next-mdx-remote-client/rsc';
import remarkGfm from 'remark-gfm'; import remarkGfm from 'remark-gfm';
import { getHighlighterCore } from 'shiki/core'; import { getSingletonHighlighter } from '@/util/shiki.bundle';
import getWasm from 'shiki/wasm';
const highlighter = await getHighlighterCore({ export default async function Page({ params }: { readonly params: Promise<{ readonly packageName: string }> }) {
themes: [import('shiki/themes/github-light.mjs'), import('shiki/themes/github-dark-dimmed.mjs')], const { packageName } = await params;
langs: [
import('shiki/langs/typescript.mjs'),
import('shiki/langs/javascript.mjs'),
import('shiki/langs/shellscript.mjs'),
],
loadWasm: getWasm,
});
export default async function Page({ params }: { readonly params: { readonly packageName: string } }) { const fileContent = await readFile(join(process.cwd(), `src/assets/readme/${packageName}/home-README.md`), 'utf8');
const fileContent = await readFile(
join(process.cwd(), `src/assets/readme/${params.packageName}/home-README.md`),
'utf8',
);
return ( return (
<div className="prose prose-neutral mx-auto max-w-screen-xl dark:prose-invert"> <div className="prose prose-neutral dark:prose-invert prose-a:[&>img]:inline-block prose-a:[&>img]:m-0 prose-a:[&>img[height='44']]:h-11 prose-p:my-2 prose-pre:py-3 prose-pre:rounded-sm prose-pre:px-0 prose-pre:border prose-pre:border-[#d4d4d4] dark:prose-pre:border-[#404040] prose-code:font-normal prose-a:text-[#5865F2] prose-a:no-underline prose-a:hover:text-[#3d48c3] dark:prose-a:hover:text-[#7782fa] mx-auto max-w-screen-xl px-6 py-6 [&_code_span:last-of-type:empty]:hidden [&_div[align='center']_p_a+a]:ml-2">
<MDXRemote <MDXRemote
options={{ options={{
mdxOptions: { mdxOptions: {
@@ -31,7 +21,10 @@ export default async function Page({ params }: { readonly params: { readonly pac
rehypePlugins: [ rehypePlugins: [
[ [
rehypeShikiFromHighlighter, rehypeShikiFromHighlighter,
highlighter, await getSingletonHighlighter({
langs: ['typescript', 'javascript', 'shellscript'],
themes: ['github-light', 'github-dark-dimmed'],
}),
{ {
themes: { themes: {
light: 'github-light', light: 'github-light',

View File

@@ -3,18 +3,17 @@ import { GeistMono } from 'geist/font/mono';
import { GeistSans } from 'geist/font/sans'; import { GeistSans } from 'geist/font/sans';
import type { Metadata, Viewport } from 'next'; import type { Metadata, Viewport } from 'next';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { LocalizedStringProvider } from 'react-aria-components/i18n'; import { DESCRIPTION } from '@/util/constants';
import { DESCRIPTION } from '~/util/constants'; import { ENV } from '@/util/env';
import { ENV } from '~/util/env';
import { Providers } from './providers'; import { Providers } from './providers';
import '~/styles/main.css'; import '@/styles/base.css';
import 'overlayscrollbars/overlayscrollbars.css'; import 'overlayscrollbars/overlayscrollbars.css';
export const viewport: Viewport = { export const viewport: Viewport = {
themeColor: [ themeColor: [
{ media: '(prefers-color-scheme: light)', color: '#ffffff' }, { media: '(prefers-color-scheme: light)', color: '#fbfbfb' },
{ media: '(prefers-color-scheme: dark)', color: '#121212' }, { media: '(prefers-color-scheme: dark)', color: '#1a1a1e' },
], ],
colorScheme: 'light dark', colorScheme: 'light dark',
}; };
@@ -70,28 +69,15 @@ export const metadata: Metadata = {
}, },
other: { other: {
'msapplication-TileColor': '#121212', 'msapplication-TileColor': '#1a1a1e',
}, },
}; };
export default async function RootLayout({ children }: PropsWithChildren) { export default async function RootLayout({ children }: PropsWithChildren) {
return ( return (
<html className={`${GeistSans.variable} ${GeistMono.variable} antialiased`} lang="en" suppressHydrationWarning> <html className={`${GeistSans.variable} ${GeistMono.variable} antialiased`} lang="en" suppressHydrationWarning>
<body className="relative bg-white dark:bg-[#121212]"> <body className="text-base-md text-base-neutral-900 dark:text-base-neutral-40 overscroll-y-none bg-[#fbfbfb] dark:bg-[#1a1a1e]">
<LocalizedStringProvider locale="en-US" /> <Providers>{children}</Providers>
<Providers>
{ENV.IS_LOCAL_DEV ? (
<div className="fixed left-1/2 top-2 z-10 flex -translate-x-1/2 place-content-center place-items-center rounded-md border border-red-400/35 bg-red-500/65 p-2 px-4 text-center text-base text-white shadow-md backdrop-blur">
Local test environment
</div>
) : null}
{ENV.IS_PREVIEW ? (
<div className="fixed left-1/2 top-2 z-10 flex -translate-x-1/2 place-content-center place-items-center rounded-md border border-red-400/35 bg-red-500/65 p-2 px-4 text-center text-base text-white shadow-md backdrop-blur">
Preview environment
</div>
) : null}
{children}
</Providers>
<Analytics /> <Analytics />
</body> </body>
</html> </html>

View File

@@ -3,10 +3,10 @@ import Link from 'next/link';
export default function NotFound() { export default function NotFound() {
return ( return (
<div className="mx-auto flex min-h-[calc(100vh_-_100px)] max-w-lg flex-col place-content-center place-items-center gap-8 px-8 py-16 lg:px-6 lg:py-0"> <div className="mx-auto flex min-h-[calc(100vh_-_100px)] max-w-lg flex-col place-content-center place-items-center gap-8 px-8 py-16 lg:px-6 lg:py-0">
<h1 className="text-[9rem] font-black leading-none md:text-[12rem]">404</h1> <h1 className="text-[9rem] leading-none font-black md:text-[12rem]">404</h1>
<h2 className="text-[2rem] md:text-[3rem]">Not found.</h2> <h2 className="text-[2rem] md:text-[3rem]">Not found.</h2>
<Link <Link
className="inline-flex rounded-md border border-transparent bg-blurple px-6 py-2 font-medium text-white" className="bg-base-blurple-400 inline-flex rounded-md border border-transparent px-6 py-2 font-medium text-white"
href="/docs" href="/docs"
> >
Take me back Take me back

View File

@@ -1,4 +1,5 @@
/* eslint-disable react/no-unknown-property */ /* eslint-disable react/no-unknown-property */
import { ImageResponse } from 'next/og'; import { ImageResponse } from 'next/og';
export const runtime = 'edge'; export const runtime = 'edge';
@@ -11,15 +12,19 @@ export const size = {
export const contentType = 'image/png'; export const contentType = 'image/png';
export default async function Image() { export default async function Image() {
const fontData = await fetch(new URL('../assets/Geist-Black.ttf', import.meta.url), {
next: { revalidate: 604_800 },
}).then(async (res) => res.arrayBuffer());
return new ImageResponse( return new ImageResponse(
( (
<div tw="flex bg-[#121212] h-full w-full"> <div tw="flex bg-[#121214] h-full w-full">
<div tw="mx-auto flex items-center h-full"> <div tw="mx-auto flex items-center h-full">
<div tw="flex"> <div tw="flex">
<div tw="flex"> <div tw="flex">
<div tw="flex flex-col font-black text-[5.5rem] text-white"> <div tw="flex flex-col font-black text-8xl text-white leading-tight">
<div tw="flex flex-row"> <div tw="flex flex-row">
The <span tw="bg-[#5865f2] rounded-lg py-1 px-6 ml-4">most popular</span> The <span tw="bg-[#5865f2] rounded-md px-3 py-2 ml-4 bottom-2">most popular</span>
</div> </div>
<span>way to build Discord</span> <span>way to build Discord</span>
<span>bots.</span> <span>bots.</span>
@@ -31,6 +36,14 @@ export default async function Image() {
), ),
{ {
...size, ...size,
fonts: [
{
name: 'Geist',
data: fontData,
weight: 900,
style: 'normal',
},
],
}, },
); );
} }

View File

@@ -1,43 +1,42 @@
import { ExternalLink } from 'lucide-react'; import { ExternalLink } from 'lucide-react';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import vercelLogo from '~/assets/powered-by-vercel.svg'; import vercelLogo from '@/assets/powered-by-vercel.svg';
import workersLogo from '~/assets/powered-by-workers.png'; import workersLogo from '@/assets/powered-by-workers.png';
import { InstallButton } from '~/components/ui/InstallButton'; import { InstallButton } from '@/components/InstallButton';
import { DESCRIPTION } from '~/util/constants'; import { buttonStyles } from '@/styles/ui/button';
import { DESCRIPTION } from '@/util/constants';
export default async function Page() { export default async function Page() {
return ( return (
<div className="mx-auto flex min-h-screen w-full max-w-screen-lg flex-col place-content-center place-items-center gap-24 px-8 pb-16 pt-12"> <div className="mx-auto flex min-h-screen w-full max-w-screen-lg flex-col place-content-center place-items-center gap-24 px-8 pt-12 pb-16">
<div className="flex flex-col gap-10 text-center"> <div className="flex flex-col gap-10 text-center">
<h1 className="z-10 text-3xl font-black leading-tight sm:text-7xl sm:leading-tight"> <h1 className="text-base-heading-xl font-black sm:text-7xl sm:leading-tight">
The <span className="relative rounded bg-blurple px-3 py-1 text-white">most popular</span> way to build The{' '}
Discord bots. <span className="bg-base-blurple-400 text-base-neutral-0 relative rounded-sm px-3 py-2">most popular</span>{' '}
way to build Discord bots.
</h1> </h1>
<p className="z-10 leading-normal text-neutral-700 dark:text-neutral-300 md:my-6">{DESCRIPTION}</p> <p className="text-base-neutral-600 dark:text-base-neutral-300 md:my-6">{DESCRIPTION}</p>
<div className="flex flex-wrap place-content-center gap-4 sm:flex-wrap md:flex-row"> <div className="flex flex-wrap place-content-center gap-4 sm:flex-wrap md:flex-row">
<Link <Link className={buttonStyles({ variant: 'filled' })} href="/docs">
className="inline-flex rounded-md border border-transparent bg-blurple px-6 py-2 font-medium text-white"
href="/docs"
>
Docs Docs
</Link> </Link>
<a <a
className="inline-flex gap-2 rounded-md border border-neutral-300 bg-white px-6 py-2 font-medium hover:bg-neutral-200 dark:border-neutral-700 dark:bg-transparent dark:hover:bg-neutral-800" className={buttonStyles({ variant: 'discreet' })}
href="https://discordjs.guide" href="https://discordjs.guide"
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
> >
Guide <ExternalLink aria-hidden size={20} /> Guide <ExternalLink aria-hidden data-slot="icon" size={18} />
</a> </a>
<a <a
className="inline-flex gap-2 rounded-md border border-neutral-300 bg-white px-6 py-2 font-medium hover:bg-neutral-200 dark:border-neutral-700 dark:bg-transparent dark:hover:bg-neutral-800" className={buttonStyles({ variant: 'discreet' })}
href="https://github.com/discordjs/discord.js" href="https://github.com/discordjs/discord.js"
rel="external noopener noreferrer" rel="external noopener noreferrer"
target="_blank" target="_blank"
> >
GitHub <ExternalLink aria-hidden size={20} /> GitHub <ExternalLink aria-hidden data-slot="icon" size={18} />
</a> </a>
</div> </div>

View File

@@ -3,10 +3,12 @@
import { Provider as JotaiProvider } from 'jotai'; import { Provider as JotaiProvider } from 'jotai';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { ThemeProvider } from 'next-themes'; import { ThemeProvider } from 'next-themes';
import { NuqsAdapter } from 'nuqs/adapters/next/app';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { RouterProvider } from 'react-aria-components'; import { RouterProvider } from 'react-aria-components';
import { useSystemThemeFallback } from '~/hooks/useSystemThemeFallback'; import { SidebarProvider } from '@/components/ui/Sidebar';
import { useUnregisterServiceWorker } from '~/hooks/useUnregisterServiceWorker'; import { useSystemThemeFallback } from '@/hooks/useSystemThemeFallback';
import { useUnregisterServiceWorker } from '@/hooks/useUnregisterServiceWorker';
export function Providers({ children }: PropsWithChildren) { export function Providers({ children }: PropsWithChildren) {
const router = useRouter(); const router = useRouter();
@@ -14,10 +16,14 @@ export function Providers({ children }: PropsWithChildren) {
useSystemThemeFallback(); useSystemThemeFallback();
return ( return (
<RouterProvider navigate={router.push}> <NuqsAdapter>
<JotaiProvider> <ThemeProvider attribute="class">
<ThemeProvider attribute="class">{children}</ThemeProvider> <RouterProvider navigate={router.push}>
</JotaiProvider> <JotaiProvider>
</RouterProvider> <SidebarProvider defaultOpen>{children}</SidebarProvider>
</JotaiProvider>
</RouterProvider>
</ThemeProvider>
</NuqsAdapter>
); );
} }

Binary file not shown.

Binary file not shown.

View File

@@ -4,7 +4,7 @@ import type { PropsWithChildren } from 'react';
export function Badge({ children, className = '' }: PropsWithChildren<{ readonly className?: string }>) { export function Badge({ children, className = '' }: PropsWithChildren<{ readonly className?: string }>) {
return ( return (
<span <span
className={`inline-flex place-items-center gap-1 rounded-full px-2 py-1 font-sans text-sm font-normal leading-none ${className}`} className={`inline-flex place-items-center gap-1 rounded-full px-2 py-1 font-sans text-sm leading-none font-normal ${className}`}
> >
{children} {children}
</span> </span>

View File

@@ -4,13 +4,15 @@ import { Command } from 'cmdk';
import { useAtom, useSetAtom } from 'jotai'; import { useAtom, useSetAtom } from 'jotai';
import { ArrowRight } from 'lucide-react'; import { ArrowRight } from 'lucide-react';
import MeiliSearch from 'meilisearch'; import MeiliSearch from 'meilisearch';
import dynamic from 'next/dynamic';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useDebounceValue } from 'usehooks-ts'; import { useDebounceValue, useMediaQuery } from 'usehooks-ts';
import { isCmdKOpenAtom } from '~/stores/cmdk'; import { Scrollbars } from '@/components/OverlayScrollbars';
import { isDrawerOpenAtom } from '~/stores/drawer'; import { isCmdKOpenAtom } from '@/stores/cmdk';
import { resolveKind } from '~/util/resolveNodeKind'; import { isDrawerOpenAtom } from '@/stores/drawer';
import { OverlayScrollbarsComponent } from '../OverlayScrollbars'; import { cx } from '@/styles/cva';
import { resolveKind } from '@/util/resolveNodeKind';
const client = new MeiliSearch({ const client = new MeiliSearch({
host: 'https://search.discordjs.dev', host: 'https://search.discordjs.dev',
@@ -24,6 +26,7 @@ export function CmdK({ dependencies }: { readonly dependencies: string[] }) {
const setDrawerOpen = useSetAtom(isDrawerOpenAtom); const setDrawerOpen = useSetAtom(isDrawerOpenAtom);
const [search, setSearch] = useDebounceValue('', 250); const [search, setSearch] = useDebounceValue('', 250);
const [searchResults, setSearchResults] = useState<any[]>([]); const [searchResults, setSearchResults] = useState<any[]>([]);
const isMobile = useMediaQuery('(max-width: 600px)');
const packageName = pathname?.split('/').slice(3, 4)[0]; const packageName = pathname?.split('/').slice(3, 4)[0];
const branchName = pathname?.split('/').slice(4, 5)[0]; const branchName = pathname?.split('/').slice(4, 5)[0];
@@ -41,9 +44,9 @@ export function CmdK({ dependencies }: { readonly dependencies: string[] }) {
> >
{resolveKind(item.kind)} {resolveKind(item.kind)}
<div className="flex flex-grow flex-col"> <div className="flex flex-grow flex-col">
<span className="font-semibold">{item.name}</span> <span className="font-semibold wrap-anywhere">{item.name}</span>
<span className="line-clamp-1 text-sm">{item.summary}</span> <span className={cx('truncate text-sm', isMobile ? 'max-w-[30ch]' : 'max-w-[40ch]')}>{item.summary}</span>
<span className="truncate text-xs">{item.path}</span> <span className={cx('truncate text-xs', isMobile ? 'max-w-[30ch]' : 'max-w-[40ch]')}>{item.path}</span>
</div> </div>
<ArrowRight aria-hidden className="flex-shrink-0" /> <ArrowRight aria-hidden className="flex-shrink-0" />
</Command.Item> </Command.Item>
@@ -114,11 +117,11 @@ export function CmdK({ dependencies }: { readonly dependencies: string[] }) {
shouldFilter={false} shouldFilter={false}
> >
<Command.Input <Command.Input
className="mb-4 w-full border-b border-neutral-300 bg-transparent px-2 pb-4 pt-2 outline-none dark:border-neutral-700" className="mb-4 w-full border-b border-neutral-300 bg-transparent px-2 pt-2 pb-4 outline-none dark:border-neutral-700"
onValueChange={setSearch} onValueChange={setSearch}
placeholder="Quick search..." placeholder="Quick search..."
/> />
<OverlayScrollbarsComponent <Scrollbars
className="max-h-96 pr-3" className="max-h-96 pr-3"
defer defer
options={{ options={{
@@ -140,7 +143,9 @@ export function CmdK({ dependencies }: { readonly dependencies: string[] }) {
</div> </div>
)} )}
</Command.List> </Command.List>
</OverlayScrollbarsComponent> </Scrollbars>
</Command.Dialog> </Command.Dialog>
); );
} }
export const CmdKNoSRR = dynamic(async () => CmdK, { ssr: false });

View File

@@ -1,25 +1,25 @@
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod'; import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { Code2, LinkIcon } from 'lucide-react'; import { Code2, LinkIcon } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { ENV } from '~/util/env'; import { ENV } from '@/util/env';
import { ParameterNode } from './ParameterNode'; import { ParameterNode } from './ParameterNode';
import { SummaryNode } from './SummaryNode'; import { SummaryNode } from './SummaryNode';
export async function ConstructorNode({ node, version }: { readonly node: any; readonly version: string }) { export async function ConstructorNode({ node, version }: { readonly node: any; readonly version: string }) {
return ( return (
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-4">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold"> <h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">
<VscSymbolMethod aria-hidden className="flex-shrink-0" size={24} /> <VscSymbolMethod aria-hidden className="flex-shrink-0" size={24} />
Constructors Constructors
</h2> </h2>
<div className="flex place-content-between place-items-center"> <div className="flex place-content-between place-items-center gap-1">
<h3 <h3
className={`${ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-16' : 'scroll-mt-8'} group break-words font-mono font-semibold`} className={`${ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-16' : 'scroll-mt-8'} group px-2 font-mono font-semibold break-all`}
id="constructor" id="constructor"
> >
{/* constructor({parsedContent.constructor.parametersString}) */} {/* constructor({parsedContent.constructor.parametersString}) */}
<Link className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block" href="#constructor"> <Link className="float-left -ml-6 hidden pr-2 pb-2 group-hover:block" href="#constructor">
<LinkIcon aria-hidden size={16} /> <LinkIcon aria-hidden size={16} />
</Link> </Link>
constructor({node.parameters?.length ? <ParameterNode node={node.parameters} version={version} /> : null}) constructor({node.parameters?.length ? <ParameterNode node={node.parameters} version={version} /> : null})
@@ -44,7 +44,7 @@ export async function ConstructorNode({ node, version }: { readonly node: any; r
<SummaryNode node={node.summary.summarySection} padding version={version} /> <SummaryNode node={node.summary.summarySection} padding version={version} />
) : null} ) : null}
<div aria-hidden className="px-4"> <div aria-hidden className="p-4">
<div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" /> <div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" />
</div> </div>
</div> </div>

View File

@@ -6,7 +6,7 @@ import { EventNode } from './EventNode';
import { InformationNode } from './InformationNode'; import { InformationNode } from './InformationNode';
import { MethodNode } from './MethodNode'; import { MethodNode } from './MethodNode';
import { Outline } from './Outline'; import { Outline } from './Outline';
import { OverlayScrollbarsComponent } from './OverlayScrollbars'; import { Scrollbars } from './OverlayScrollbars';
import { ParameterNode } from './ParameterNode'; import { ParameterNode } from './ParameterNode';
import { PropertyNode } from './PropertyNode'; import { PropertyNode } from './PropertyNode';
import { ReturnNode } from './ReturnNode'; import { ReturnNode } from './ReturnNode';
@@ -26,12 +26,14 @@ async function OverloadNode({
readonly packageName: string; readonly packageName: string;
readonly version: string; readonly version: string;
}) { }) {
'use cache';
return ( return (
<Tabs className="flex flex-col gap-4"> <Tabs className="flex flex-col gap-4">
<TabList className="flex gap-2"> <TabList className="flex gap-2">
{node.overloads.map((overload: any) => ( {node.overloads.map((overload: any) => (
<Tab <Tab
className="cursor-pointer rounded-full bg-neutral-800/10 px-2 py-1 font-sans text-sm font-normal leading-none text-neutral-800 hover:bg-neutral-800/20 data-[selected]:bg-neutral-500 data-[selected]:text-neutral-100 dark:bg-neutral-200/10 dark:text-neutral-200 dark:hover:bg-neutral-200/20 dark:data-[selected]:bg-neutral-500/70" className="cursor-pointer rounded-full bg-neutral-800/10 px-2 py-1 font-sans text-sm leading-none font-normal text-neutral-800 hover:bg-neutral-800/20 data-[selected]:bg-neutral-500 data-[selected]:text-neutral-100 dark:bg-neutral-200/10 dark:text-neutral-200 dark:hover:bg-neutral-200/20 dark:data-[selected]:bg-neutral-500/70"
id={`overload-${overload.displayName}-${overload.overloadIndex}`} id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-${overload.displayName}-${overload.overloadIndex}`} key={`overload-tab-${overload.displayName}-${overload.overloadIndex}`}
> >
@@ -41,7 +43,7 @@ async function OverloadNode({
</TabList> </TabList>
{node.overloads.map((overload: any) => ( {node.overloads.map((overload: any) => (
<TabPanel <TabPanel
className="flex flex-col gap-8" className="flex flex-col gap-4"
id={`overload-${overload.displayName}-${overload.overloadIndex}`} id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-panel-${overload.displayName}-${overload.overloadIndex}`} key={`overload-tab-panel-${overload.displayName}-${overload.overloadIndex}`}
> >
@@ -52,7 +54,7 @@ async function OverloadNode({
); );
} }
export function DocItem({ export async function DocItem({
node, node,
packageName, packageName,
version, version,
@@ -61,6 +63,8 @@ export function DocItem({
readonly packageName: string; readonly packageName: string;
readonly version: string; readonly version: string;
}) { }) {
'use cache';
if (node.overloads?.length) { if (node.overloads?.length) {
return <OverloadNode node={node} packageName={packageName} version={version} />; return <OverloadNode node={node} packageName={packageName} version={version} />;
} }
@@ -69,16 +73,13 @@ export function DocItem({
<> <>
<InformationNode node={node} version={version} /> <InformationNode node={node} version={version} />
<OverlayScrollbarsComponent <Scrollbars className="border-base-neutral-200 dark:border-base-neutral-600 bg-base-neutral-100 dark:bg-base-neutral-900 rounded-sm border">
className="rounded-md border border-neutral-300 bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-900" <SyntaxHighlighter
defer className="bg-[#f3f3f4] py-4 text-sm dark:bg-[#121214]"
options={{ code={node.sourceExcerpt}
overflow: { y: 'hidden' }, lang="typescript"
scrollbars: { autoHide: 'scroll', autoHideDelay: 500, autoHideSuspend: true, clickScroll: true }, />
}} </Scrollbars>
>
<SyntaxHighlighter className="py-4 text-sm" code={node.sourceExcerpt} lang="typescript" />
</OverlayScrollbarsComponent>
{node.summary?.deprecatedBlock.length ? ( {node.summary?.deprecatedBlock.length ? (
<DeprecatedNode deprecatedBlock={node.summary.deprecatedBlock} version={version} /> <DeprecatedNode deprecatedBlock={node.summary.deprecatedBlock} version={version} />
@@ -95,7 +96,7 @@ export function DocItem({
{node.constructor?.parametersString ? <ConstructorNode node={node.constructor} version={version} /> : null} {node.constructor?.parametersString ? <ConstructorNode node={node.constructor} version={version} /> : null}
{node.typeParameters?.length ? ( {node.typeParameters?.length ? (
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-4">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold"> <h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">
<VscSymbolParameter aria-hidden className="flex-shrink-0" size={24} /> <VscSymbolParameter aria-hidden className="flex-shrink-0" size={24} />
Type Parameters Type Parameters
@@ -105,7 +106,7 @@ export function DocItem({
) : null} ) : null}
{node.parameters?.length ? ( {node.parameters?.length ? (
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-4">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold"> <h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">
<VscSymbolParameter aria-hidden className="flex-shrink-0" size={24} /> <VscSymbolParameter aria-hidden className="flex-shrink-0" size={24} />
Parameters Parameters

View File

@@ -1,6 +1,6 @@
import Link from 'next/link'; import Link from 'next/link';
import { BuiltinDocumentationLinks } from '~/util/builtinDocumentationLinks'; import { BuiltinDocumentationLinks } from '@/util/builtinDocumentationLinks';
import { OverlayScrollbarsComponent } from './OverlayScrollbars'; import { Scrollbars } from './OverlayScrollbars';
import { SyntaxHighlighter } from './SyntaxHighlighter'; import { SyntaxHighlighter } from './SyntaxHighlighter';
export async function DocNode({ node, version }: { readonly node?: any; readonly version: string }) { export async function DocNode({ node, version }: { readonly node?: any; readonly version: string }) {
@@ -12,9 +12,11 @@ export async function DocNode({ node, version }: { readonly node?: any; readonly
if (node.resolvedPackage) { if (node.resolvedPackage) {
return ( return (
<Link <Link
className="font-mono text-blurple hover:text-blurple-500 dark:hover:text-blurple-300" className="text-base-blurple-400 hover:text-base-blurple-500 dark:hover:text-base-blurple-300 font-mono"
href={`/docs/packages/${node.resolvedPackage.packageName}/${node.resolvedPackage.version ?? version}/${node.uri}`} href={`/docs/packages/${node.resolvedPackage.packageName}/${node.resolvedPackage.version ?? version}/${node.uri}`}
key={`${node.text}-${idx}`} key={`${node.uri}-${idx}`}
// @ts-expect-error - unstable_dynamicOnHover is not part of the public types
unstable_dynamicOnHover
> >
{node.text} {node.text}
</Link> </Link>
@@ -24,7 +26,7 @@ export async function DocNode({ node, version }: { readonly node?: any; readonly
if (node.uri) { if (node.uri) {
return ( return (
<a <a
className="text-blurple hover:text-blurple-500 dark:hover:text-blurple-300" className="text-base-blurple-400 hover:text-base-blurple-500 dark:hover:text-base-blurple-300"
href={node.uri} href={node.uri}
key={`${node.text}-${idx}`} key={`${node.text}-${idx}`}
rel="external noreferrer noopener" rel="external noreferrer noopener"
@@ -39,7 +41,7 @@ export async function DocNode({ node, version }: { readonly node?: any; readonly
const href = BuiltinDocumentationLinks[node.text as keyof typeof BuiltinDocumentationLinks]; const href = BuiltinDocumentationLinks[node.text as keyof typeof BuiltinDocumentationLinks];
return ( return (
<a <a
className="text-blurple hover:text-blurple-500 dark:hover:text-blurple-300" className="text-base-blurple-400 hover:text-base-blurple-500 dark:hover:text-base-blurple-300"
href={href} href={href}
key={`${node.text}-${idx}`} key={`${node.text}-${idx}`}
rel="external noreferrer noopener" rel="external noreferrer noopener"
@@ -64,16 +66,13 @@ export async function DocNode({ node, version }: { readonly node?: any; readonly
const { language, text } = node; const { language, text } = node;
return ( return (
<OverlayScrollbarsComponent <Scrollbars
className="my-4 rounded-md border border-neutral-300 bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-900" className="border-base-neutral-200 dark:border-base-neutral-600 bg-base-neutral-100 dark:bg-base-neutral-900 my-4 rounded-sm border"
defer defer
options={{ key={`${language}-${text}-${idx}`}
overflow: { y: 'hidden' },
scrollbars: { autoHide: 'scroll', autoHideDelay: 500, autoHideSuspend: true, clickScroll: true },
}}
> >
<SyntaxHighlighter className="py-4 text-sm " code={text} lang={language} /> <SyntaxHighlighter className="bg-[#f3f3f4] py-4 text-sm dark:bg-[#121214]" code={text} lang={language} />
</OverlayScrollbarsComponent> </Scrollbars>
); );
} }

View File

@@ -2,7 +2,7 @@ import { VscSymbolEnumMember } from '@react-icons/all-files/vsc/VscSymbolEnumMem
import { Code2, LinkIcon } from 'lucide-react'; import { Code2, LinkIcon } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { ENV } from '~/util/env'; import { ENV } from '@/util/env';
import { Badges } from './Badges'; import { Badges } from './Badges';
import { DeprecatedNode } from './DeprecatedNode'; import { DeprecatedNode } from './DeprecatedNode';
import { ExampleNode } from './ExampleNode'; import { ExampleNode } from './ExampleNode';
@@ -23,25 +23,25 @@ export async function EnumMemberNode({
readonly version: string; readonly version: string;
}) { }) {
return ( return (
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-4">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold"> <h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">
<VscSymbolEnumMember aria-hidden className="flex-shrink-0" size={24} /> <VscSymbolEnumMember aria-hidden className="flex-shrink-0" size={24} />
Members Members
</h2> </h2>
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-4">
{node.map((enumMember: any, idx: number) => ( {node.map((enumMember: any, idx: number) => (
<Fragment key={`${enumMember.displayName}-${idx}`}> <Fragment key={`${enumMember.displayName}-${idx}`}>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center"> <div className="flex place-content-between place-items-center gap-1">
<h3 <h3
className={`${ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-16' : 'scroll-mt-8'} group break-words font-mono font-semibold`} className={`${ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-16' : 'scroll-mt-8'} group px-2 font-mono font-semibold break-all`}
id={enumMember.displayName} id={enumMember.displayName}
> >
<Badges node={enumMember} /> <Badges node={enumMember} />
<span> <span>
<Link <Link
className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block" className="float-left -ml-6 hidden pr-2 pb-2 group-hover:block"
href={`#${enumMember.displayName}`} href={`#${enumMember.displayName}`}
> >
<LinkIcon aria-hidden size={16} /> <LinkIcon aria-hidden size={16} />
@@ -100,7 +100,7 @@ export async function EnumMemberNode({
<SeeNode node={enumMember.summary.seeBlocks} padding version={version} /> <SeeNode node={enumMember.summary.seeBlocks} padding version={version} />
) : null} ) : null}
</div> </div>
<div aria-hidden className="px-4"> <div aria-hidden className="p-4">
<div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" /> <div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" />
</div> </div>
</Fragment> </Fragment>

View File

@@ -1,7 +1,7 @@
import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent'; import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent';
import { ChevronDown, ChevronUp, Code2, LinkIcon } from 'lucide-react'; import { ChevronDown, ChevronUp, Code2, LinkIcon } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { ENV } from '~/util/env'; import { ENV } from '@/util/env';
import { Badges } from './Badges'; import { Badges } from './Badges';
import { DeprecatedNode } from './DeprecatedNode'; import { DeprecatedNode } from './DeprecatedNode';
import { ExampleNode } from './ExampleNode'; import { ExampleNode } from './ExampleNode';
@@ -28,14 +28,14 @@ async function EventBodyNode({
return ( return (
<> <>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center"> <div className="flex place-content-between place-items-center gap-1">
<h3 <h3
className={`${overload ? (ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-24' : 'scroll-mt-16') : ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-16' : 'scroll-mt-8'} group break-words font-mono font-semibold`} className={`${overload ? (ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-24' : 'scroll-mt-16') : ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-16' : 'scroll-mt-8'} group px-2 font-mono font-semibold break-all`}
id={event.displayName} id={event.displayName}
> >
<Badges node={event} /> {event.displayName} <Badges node={event} /> {event.displayName}
<span> <span>
<Link className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block" href={`#${event.displayName}`}> <Link className="float-left -ml-6 hidden pr-2 pb-2 group-hover:block" href={`#${event.displayName}`}>
<LinkIcon aria-hidden size={16} /> <LinkIcon aria-hidden size={16} />
</Link> </Link>
{event.typeParameters?.length ? ( {event.typeParameters?.length ? (
@@ -86,7 +86,7 @@ async function EventBodyNode({
{event.summary?.seeBlocks.length ? <SeeNode node={event.summary.seeBlocks} padding version={version} /> : null} {event.summary?.seeBlocks.length ? <SeeNode node={event.summary.seeBlocks} padding version={version} /> : null}
</div> </div>
<div aria-hidden className="px-4"> <div aria-hidden className="p-4">
<div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" /> <div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" />
</div> </div>
</> </>
@@ -107,7 +107,7 @@ async function OverloadNode({
<TabList className="flex gap-2"> <TabList className="flex gap-2">
{event.overloads.map((overload: any) => ( {event.overloads.map((overload: any) => (
<Tab <Tab
className="cursor-pointer rounded-full bg-neutral-800/10 px-2 py-1 font-sans text-sm font-normal leading-none text-neutral-800 hover:bg-neutral-800/20 data-[selected]:bg-neutral-500 data-[selected]:text-neutral-100 dark:bg-neutral-200/10 dark:text-neutral-200 dark:hover:bg-neutral-200/20 dark:data-[selected]:bg-neutral-500/70" className="cursor-pointer rounded-full bg-neutral-800/10 px-2 py-1 font-sans text-sm leading-none font-normal text-neutral-800 hover:bg-neutral-800/20 data-[selected]:bg-neutral-500 data-[selected]:text-neutral-100 dark:bg-neutral-200/10 dark:text-neutral-200 dark:hover:bg-neutral-200/20 dark:data-[selected]:bg-neutral-500/70"
id={`overload-${overload.displayName}-${overload.overloadIndex}`} id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-${overload.displayName}-${overload.overloadIndex}`} key={`overload-tab-${overload.displayName}-${overload.overloadIndex}`}
> >
@@ -117,7 +117,7 @@ async function OverloadNode({
</TabList> </TabList>
{event.overloads.map((overload: any) => ( {event.overloads.map((overload: any) => (
<TabPanel <TabPanel
className="flex flex-col gap-8" className="flex flex-col gap-4"
id={`overload-${overload.displayName}-${overload.overloadIndex}`} id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-panel-${overload.displayName}-${overload.overloadIndex}`} key={`overload-tab-panel-${overload.displayName}-${overload.overloadIndex}`}
> >
@@ -138,8 +138,8 @@ export async function EventNode({
readonly version: string; readonly version: string;
}) { }) {
return ( return (
<Collapsible className="flex flex-col gap-8" defaultOpen> <Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800"> <CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-[#e7e7e9] dark:hover:bg-[#242428]">
<h2 className="flex place-items-center gap-2 text-xl font-bold"> <h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolEvent aria-hidden className="flex-shrink-0" size={24} /> Events <VscSymbolEvent aria-hidden className="flex-shrink-0" size={24} /> Events
</h2> </h2>
@@ -148,7 +148,7 @@ export async function EventNode({
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-4">
{node.map((event: any) => {node.map((event: any) =>
event.overloads?.length ? ( event.overloads?.length ? (
<OverloadNode <OverloadNode

View File

@@ -2,7 +2,7 @@ import { DocNode } from './DocNode';
export async function ExampleNode({ node, version }: { readonly node: any; readonly version: string }) { export async function ExampleNode({ node, version }: { readonly node: any; readonly version: string }) {
return ( return (
<div className="break-words pl-4"> <div className="pl-4 break-words">
<span className="font-semibold">Examples:</span> <span className="font-semibold">Examples:</span>
<DocNode node={node} version={version} /> <DocNode node={node} version={version} />
</div> </div>

View File

@@ -1,9 +1,9 @@
import Link from 'next/link'; import Link from 'next/link';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { BuiltinDocumentationLinks } from '~/util/builtinDocumentationLinks'; import { BuiltinDocumentationLinks } from '@/util/builtinDocumentationLinks';
export async function ExcerptNode({ node, version }: { readonly node?: any; readonly version: string }) { export async function ExcerptNode({ node, version }: { readonly node?: any; readonly version: string }) {
const createExcerpt = (excerpts: any) => { const createExcerpt = (excerpts: any, idx: number) => {
const excerpt = Array.isArray(excerpts) ? excerpts : (excerpts.excerpts ?? [excerpts]); const excerpt = Array.isArray(excerpts) ? excerpts : (excerpts.excerpts ?? [excerpts]);
return ( return (
@@ -13,14 +13,17 @@ export async function ExcerptNode({ node, version }: { readonly node?: any; read
? 'after:content-[",_"] last-of-type:after:content-none' ? 'after:content-[",_"] last-of-type:after:content-none'
: '' : ''
} }
key={`${excerpt.text}-${idx}`}
> >
{excerpt.map((excerpt: any, idx: number) => { {excerpt.map((excerpt: any, idx: number) => {
if (excerpt.resolvedItem) { if (excerpt.resolvedItem) {
return ( return (
<Link <Link
className="text-blurple hover:text-blurple-500 dark:hover:text-blurple-300" className="text-base-blurple-400 hover:text-base-blurple-500 dark:hover:text-base-blurple-300"
href={`/docs/packages/${excerpt.resolvedItem.packageName}/${excerpt.resolvedItem.version ?? version}/${excerpt.resolvedItem.uri}`} href={`/docs/packages/${excerpt.resolvedItem.packageName}/${excerpt.resolvedItem.version ?? version}/${excerpt.resolvedItem.uri}`}
key={`${excerpt.resolvedItem.displayName}-${idx}`} key={`${excerpt.resolvedItem.displayName}-${idx}`}
// @ts-expect-error - unstable_dynamicOnHover is not part of the public types
unstable_dynamicOnHover
> >
{excerpt.text} {excerpt.text}
</Link> </Link>
@@ -30,7 +33,7 @@ export async function ExcerptNode({ node, version }: { readonly node?: any; read
if (excerpt.href) { if (excerpt.href) {
return ( return (
<a <a
className="text-blurple hover:text-blurple-500 dark:hover:text-blurple-300" className="text-base-blurple-400 hover:text-base-blurple-500 dark:hover:text-base-blurple-300"
href={excerpt.href} href={excerpt.href}
key={`${excerpt.text}-${idx}`} key={`${excerpt.text}-${idx}`}
rel="external noreferrer noopener" rel="external noreferrer noopener"
@@ -43,9 +46,10 @@ export async function ExcerptNode({ node, version }: { readonly node?: any; read
if (excerpt.text in BuiltinDocumentationLinks) { if (excerpt.text in BuiltinDocumentationLinks) {
const href = BuiltinDocumentationLinks[excerpt.text as keyof typeof BuiltinDocumentationLinks]; const href = BuiltinDocumentationLinks[excerpt.text as keyof typeof BuiltinDocumentationLinks];
return ( return (
<a <a
className="text-blurple hover:text-blurple-500 dark:hover:text-blurple-300" className="text-base-blurple-400 hover:text-base-blurple-500 dark:hover:text-base-blurple-300"
href={href} href={href}
key={`${excerpt.text}-${idx}`} key={`${excerpt.text}-${idx}`}
rel="external noreferrer noopener" rel="external noreferrer noopener"

View File

@@ -1,10 +1,10 @@
import Image from 'next/image'; import Image from 'next/image';
import vercelLogo from '~/assets/powered-by-vercel.svg'; import vercelLogo from '@/assets/powered-by-vercel.svg';
import workersLogo from '~/assets/powered-by-workers.png'; import workersLogo from '@/assets/powered-by-workers.png';
export function Footer() { export function Footer() {
return ( return (
<footer className="md:pl-12 md:pr-12"> <footer className="md:pr-12 md:pl-12">
<div className="flex flex-col flex-wrap place-content-center gap-6 pt-12 sm:flex-row md:gap-12"> <div className="flex flex-col flex-wrap place-content-center gap-6 pt-12 sm:flex-row md:gap-12">
<div className="flex flex-wrap place-content-center place-items-center gap-4"> <div className="flex flex-wrap place-content-center place-items-center gap-4">
<a <a

View File

@@ -5,10 +5,10 @@ import { InheritanceNode } from './InheritanceNode';
export async function InformationNode({ node, version }: { readonly node: any; readonly version: string }) { export async function InformationNode({ node, version }: { readonly node: any; readonly version: string }) {
return ( return (
<div className="flex place-content-between place-items-center"> <div className="flex place-content-between place-items-center gap-1">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<h1 className="text-xl"> <h1 className="text-xl">
<DocKind node={node} /> <span className="break-words font-bold">{node.displayName}</span> <DocKind node={node} /> <span className="font-bold break-all">{node.displayName}</span>
</h1> </h1>
{node.implements ? <InheritanceNode node={node.implements} text="implements" version={version} /> : null} {node.implements ? <InheritanceNode node={node.implements} text="implements" version={version} /> : null}
{node.extends ? <InheritanceNode node={node.extends} text="extends" version={version} /> : null} {node.extends ? <InheritanceNode node={node.extends} text="extends" version={version} /> : null}

View File

@@ -11,8 +11,8 @@ export async function InheritanceNode({
}) { }) {
return ( return (
<div> <div>
<h2 className="inline-block min-w-min text-sm italic text-neutral-500 dark:text-neutral-400">{text}</h2>{' '} <h2 className="inline-block min-w-min text-sm text-neutral-500 italic dark:text-neutral-400">{text}</h2>{' '}
<span className="break-words font-mono text-sm"> <span className="font-mono text-sm break-all">
<ExcerptNode node={node} version={version} /> <ExcerptNode node={node} version={version} />
</span> </span>
</div> </div>

View File

@@ -10,11 +10,13 @@ export async function InheritedFromNode({
readonly version: string; readonly version: string;
}) { }) {
return ( return (
<p className="break-words pl-4"> <p className="pl-4 break-words">
<span className="font-semibold">Inherited from:</span>{' '} <span className="font-semibold">Inherited from:</span>{' '}
<Link <Link
className="font-mono text-blurple hover:text-blurple-500 dark:hover:text-blurple-300" className="text-base-blurple-400 hover:text-base-blurple-500 dark:hover:text-base-blurple-300 font-mono"
href={`/docs/packages/${packageName}/${version}/${node}`} href={`/docs/packages/${packageName}/${version}/${node}`}
// @ts-expect-error - unstable_dynamicOnHover is not part of the public types
unstable_dynamicOnHover
> >
{node.slice(0, node.indexOf(':'))} {node.slice(0, node.indexOf(':'))}
</Link> </Link>

View File

@@ -15,14 +15,14 @@ export function InstallButton({ className = '' }: { readonly className?: string
return ( return (
<button <button
className={`cursor-copy rounded-md border border-neutral-300 bg-white px-4 py-2 font-mono hover:bg-neutral-200 dark:border-neutral-700 dark:bg-transparent dark:hover:bg-neutral-800 ${className}`} className={`cursor-copy rounded-sm border border-neutral-300 bg-white px-4 py-2 font-mono hover:bg-neutral-200 dark:border-neutral-700 dark:bg-transparent dark:hover:bg-neutral-800 ${className}`}
onClick={async () => { onClick={async () => {
setInteracted(true); setInteracted(true);
await copyToClipboard('npm install discord.js'); await copyToClipboard('npm install discord.js');
}} }}
type="button" type="button"
> >
<span className="font-semibold text-blurple">{'>'}</span> npm install discord.js{' '} <span className="text-base-blurple-400 font-semibold">{'>'}</span> npm install discord.js{' '}
{copiedText && interacted ? ( {copiedText && interacted ? (
<CopyCheck aria-hidden className="ml-1 inline-block text-green-500" size={20} /> <CopyCheck aria-hidden className="ml-1 inline-block text-green-500" size={20} />
) : ( ) : (

View File

@@ -1,7 +1,7 @@
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod'; import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { ChevronDown, ChevronUp, Code2, LinkIcon } from 'lucide-react'; import { ChevronDown, ChevronUp, Code2, LinkIcon } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { ENV } from '~/util/env'; import { ENV } from '@/util/env';
import { Badges } from './Badges'; import { Badges } from './Badges';
import { DeprecatedNode } from './DeprecatedNode'; import { DeprecatedNode } from './DeprecatedNode';
import { ExampleNode } from './ExampleNode'; import { ExampleNode } from './ExampleNode';
@@ -29,14 +29,14 @@ async function MethodBodyNode({
return ( return (
<> <>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center"> <div className="flex place-content-between place-items-center gap-1">
<h3 <h3
className={`${overload ? (ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-24' : 'scroll-mt-16') : ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-16' : 'scroll-mt-8'} group break-words font-mono font-semibold`} className={`${overload ? (ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-24' : 'scroll-mt-16') : ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-16' : 'scroll-mt-8'} group px-2 font-mono font-semibold break-all`}
id={method.displayName} id={method.displayName}
> >
<Badges node={method} /> {method.displayName} <Badges node={method} /> {method.displayName}
<span> <span>
<Link className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block" href={`#${method.displayName}`}> <Link className="float-left -ml-6 hidden pr-2 pb-2 group-hover:block" href={`#${method.displayName}`}>
<LinkIcon aria-hidden size={16} /> <LinkIcon aria-hidden size={16} />
</Link> </Link>
{method.typeParameters?.length ? ( {method.typeParameters?.length ? (
@@ -90,7 +90,7 @@ async function MethodBodyNode({
<SeeNode node={method.summary.seeBlocks} padding version={version} /> <SeeNode node={method.summary.seeBlocks} padding version={version} />
) : null} ) : null}
</div> </div>
<div aria-hidden className="px-4"> <div aria-hidden className="p-4">
<div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" /> <div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" />
</div> </div>
</> </>
@@ -111,7 +111,7 @@ async function OverloadNode({
<TabList className="flex gap-2"> <TabList className="flex gap-2">
{method.overloads.map((overload: any) => ( {method.overloads.map((overload: any) => (
<Tab <Tab
className="cursor-pointer rounded-full bg-neutral-800/10 px-2 py-1 font-sans text-sm font-normal leading-none text-neutral-800 hover:bg-neutral-800/20 data-[selected]:bg-neutral-500 data-[selected]:text-neutral-100 dark:bg-neutral-200/10 dark:text-neutral-200 dark:hover:bg-neutral-200/20 dark:data-[selected]:bg-neutral-500/70" className="cursor-pointer rounded-full bg-neutral-800/10 px-2 py-1 font-sans text-sm leading-none font-normal text-neutral-800 hover:bg-neutral-800/20 data-[selected]:bg-neutral-500 data-[selected]:text-neutral-100 dark:bg-neutral-200/10 dark:text-neutral-200 dark:hover:bg-neutral-200/20 dark:data-[selected]:bg-neutral-500/70"
id={`overload-${overload.displayName}-${overload.overloadIndex}`} id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-${overload.displayName}-${overload.overloadIndex}`} key={`overload-tab-${overload.displayName}-${overload.overloadIndex}`}
> >
@@ -121,7 +121,7 @@ async function OverloadNode({
</TabList> </TabList>
{method.overloads.map((overload: any) => ( {method.overloads.map((overload: any) => (
<TabPanel <TabPanel
className="flex flex-col gap-8" className="flex flex-col gap-4"
id={`overload-${overload.displayName}-${overload.overloadIndex}`} id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-panel-${overload.displayName}-${overload.overloadIndex}`} key={`overload-tab-panel-${overload.displayName}-${overload.overloadIndex}`}
> >
@@ -142,8 +142,8 @@ export async function MethodNode({
readonly version: string; readonly version: string;
}) { }) {
return ( return (
<Collapsible className="flex flex-col gap-8" defaultOpen> <Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800"> <CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-[#e7e7e9] dark:hover:bg-[#242428]">
<h2 className="flex place-items-center gap-2 text-xl font-bold"> <h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolMethod aria-hidden className="flex-shrink-0" size={24} /> Methods <VscSymbolMethod aria-hidden className="flex-shrink-0" size={24} /> Methods
</h2> </h2>
@@ -152,7 +152,7 @@ export async function MethodNode({
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-4">
{node.map((method: any) => {node.map((method: any) =>
method.overloads?.length ? ( method.overloads?.length ? (
<OverloadNode <OverloadNode

View File

@@ -1,41 +1,17 @@
import { VscGithubInverted } from '@react-icons/all-files/vsc/VscGithubInverted';
import { ChevronDown, ChevronUp } from 'lucide-react'; import { ChevronDown, ChevronUp } from 'lucide-react';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { fetchSitemap } from '~/util/fetchSitemap'; import { fetchSitemap } from '@/util/fetchSitemap';
import { fetchVersions } from '~/util/fetchVersions';
import { resolveNodeKind } from './DocKind'; import { resolveNodeKind } from './DocKind';
import { NavigationItem } from './NavigationItem'; import { NavigationItem } from './NavigationItem';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible'; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible';
import { PackageSelect } from './ui/PackageSelect';
import { SearchButton } from './ui/SearchButton';
import { VersionSelect } from './ui/VersionSelect';
// eslint-disable-next-line promise/prefer-await-to-then export async function Navigation({ packageName, version }: { readonly packageName: string; readonly version: string }) {
const ThemeSwitch = dynamic(async () => import('~/components/ui/ThemeSwitch').then((mod) => mod.ThemeSwitch), {
ssr: false,
});
export async function Navigation({
className = '',
packageName,
version,
drawer = false,
}: {
readonly className?: string;
readonly drawer?: boolean;
readonly packageName: string;
readonly version: string;
}) {
const node = await fetchSitemap({ packageName, version }); const node = await fetchSitemap({ packageName, version });
if (!node) { if (!node) {
notFound(); notFound();
} }
const versions = await fetchVersions(packageName);
const groupedNodes = node.reduce((acc: any, node: any) => { const groupedNodes = node.reduce((acc: any, node: any) => {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
(acc[node.kind.toLowerCase()] ||= []).push(node); (acc[node.kind.toLowerCase()] ||= []).push(node);
@@ -43,187 +19,156 @@ export async function Navigation({
}, {}); }, {});
return ( return (
<aside className={`flex min-w-52 max-w-52 flex-col gap-2 lg:min-w-72 lg:max-w-72 ${className}`}> <nav className="flex flex-col gap-2 pr-3">
<div {groupedNodes.class?.length ? (
className={`sticky top-0 flex flex-col gap-4 pb-4 ${drawer ? 'bg-neutral-100 dark:bg-neutral-900' : 'bg-white dark:bg-[#121212]'}`} <Collapsible className="flex flex-col gap-2" defaultOpen>
> <CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-[#e7e7e9] dark:hover:bg-[#242428]">
<div className="flex flex-col gap-2 pt-px"> <h4 className="font-semibold">Classes</h4>
<div className="flex place-content-between place-items-center p-1"> <ChevronDown aria-hidden className='group-data-[state="open"]:hidden' size={24} />
<Link className="text-xl font-bold" href={`/docs/packages/${packageName}/${version}`}> <ChevronUp aria-hidden className='group-data-[state="closed"]:hidden' size={24} />
{packageName} </CollapsibleTrigger>
</Link> <CollapsibleContent>
<div className="flex gap-2"> <div className="flex flex-col">
<Link {groupedNodes.class.map((node: any, idx: number) => {
aria-label="GitHub" const kind = resolveNodeKind(node.kind);
className="rounded-full" return (
href="https://github.com/discordjs/discord.js" <NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
rel="external noopener noreferrer" <div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
target="_blank" {node.kind[0]}
> </div>{' '}
<VscGithubInverted aria-hidden size={24} /> <span className="font-sans">{node.name}</span>
</Link> </NavigationItem>
<ThemeSwitch /> );
})}
</div> </div>
</div> </CollapsibleContent>
<PackageSelect packageName={packageName} /> </Collapsible>
{/* <h3 className="p-1 text-lg font-semibold">{version}</h3> */} ) : null}
<VersionSelect packageName={packageName} version={version} versions={versions} />
</div>
<SearchButton /> {groupedNodes.function?.length ? (
</div> <Collapsible className="flex flex-col gap-2" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-[#e7e7e9] dark:hover:bg-[#242428]">
<h4 className="font-semibold">Functions</h4>
<ChevronDown aria-hidden className='group-data-[state="open"]:hidden' size={24} />
<ChevronUp aria-hidden className='group-data-[state="closed"]:hidden' size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col">
{groupedNodes.function.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
<nav className="flex flex-col gap-4"> {groupedNodes.enum?.length ? (
{groupedNodes.class?.length ? ( <Collapsible className="flex flex-col gap-2" defaultOpen>
<Collapsible className="flex flex-col gap-4" defaultOpen> <CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-[#e7e7e9] dark:hover:bg-[#242428]">
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800"> <h4 className="font-semibold">Enums</h4>
<h4 className="font-semibold">Classes</h4> <ChevronDown aria-hidden className='group-data-[state="open"]:hidden' size={24} />
<ChevronDown aria-hidden className='group-data-[state="open"]:hidden' size={24} /> <ChevronUp aria-hidden className='group-data-[state="closed"]:hidden' size={24} />
<ChevronUp aria-hidden className='group-data-[state="closed"]:hidden' size={24} /> </CollapsibleTrigger>
</CollapsibleTrigger> <CollapsibleContent>
<CollapsibleContent> <div className="flex flex-col">
<div className="flex flex-col gap-1.5"> {groupedNodes.enum.map((node: any, idx: number) => {
{groupedNodes.class.map((node: any, idx: number) => { const kind = resolveNodeKind(node.kind);
const kind = resolveNodeKind(node.kind); return (
return ( <NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}> <div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}> {node.kind[0]}
{node.kind[0]} </div>{' '}
</div>{' '} <span className="font-sans">{node.name}</span>
<span className="font-sans">{node.name}</span> </NavigationItem>
</NavigationItem> );
); })}
})} </div>
</div> </CollapsibleContent>
</CollapsibleContent> </Collapsible>
</Collapsible> ) : null}
) : null}
{groupedNodes.function?.length ? ( {groupedNodes.interface?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen> <Collapsible className="flex flex-col gap-2" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800"> <CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-[#e7e7e9] dark:hover:bg-[#242428]">
<h4 className="font-semibold">Functions</h4> <h4 className="font-semibold">Interfaces</h4>
<ChevronDown aria-hidden className='group-data-[state="open"]:hidden' size={24} /> <ChevronDown aria-hidden className='group-data-[state="open"]:hidden' size={24} />
<ChevronUp aria-hidden className='group-data-[state="closed"]:hidden' size={24} /> <ChevronUp aria-hidden className='group-data-[state="closed"]:hidden' size={24} />
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col">
{groupedNodes.function.map((node: any, idx: number) => { {groupedNodes.interface.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind); const kind = resolveNodeKind(node.kind);
return ( return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}> <NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}> <div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]} {node.kind[0]}
</div>{' '} </div>{' '}
<span className="font-sans">{node.name}</span> <span className="font-sans">{node.name}</span>
</NavigationItem> </NavigationItem>
); );
})} })}
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
) : null} ) : null}
{groupedNodes.enum?.length ? ( {groupedNodes.typealias?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen> <Collapsible className="flex flex-col gap-2" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800"> <CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-[#e7e7e9] dark:hover:bg-[#242428]">
<h4 className="font-semibold">Enums</h4> <h4 className="font-semibold">Types</h4>
<ChevronDown aria-hidden className='group-data-[state="open"]:hidden' size={24} /> <ChevronDown aria-hidden className='group-data-[state="open"]:hidden' size={24} />
<ChevronUp aria-hidden className='group-data-[state="closed"]:hidden' size={24} /> <ChevronUp aria-hidden className='group-data-[state="closed"]:hidden' size={24} />
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col">
{groupedNodes.enum.map((node: any, idx: number) => { {groupedNodes.typealias.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind); const kind = resolveNodeKind(node.kind);
return ( return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}> <NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}> <div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]} {node.kind[0]}
</div>{' '} </div>{' '}
<span className="font-sans">{node.name}</span> <span className="font-sans">{node.name}</span>
</NavigationItem> </NavigationItem>
); );
})} })}
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
) : null} ) : null}
{groupedNodes.interface?.length ? ( {groupedNodes.variable?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen> <Collapsible className="flex flex-col gap-2" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800"> <CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-[#e7e7e9] dark:hover:bg-[#242428]">
<h4 className="font-semibold">Interfaces</h4> <h4 className="font-semibold">Variables</h4>
<ChevronDown aria-hidden className='group-data-[state="open"]:hidden' size={24} /> <ChevronDown aria-hidden className='group-data-[state="open"]:hidden' size={24} />
<ChevronUp aria-hidden className='group-data-[state="closed"]:hidden' size={24} /> <ChevronUp aria-hidden className='group-data-[state="closed"]:hidden' size={24} />
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="flex flex-col gap-1.5"> <div className="flex flex-col">
{groupedNodes.interface.map((node: any, idx: number) => { {groupedNodes.variable.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind); const kind = resolveNodeKind(node.kind);
return ( return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}> <NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}> <div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]} {node.kind[0]}
</div>{' '} </div>{' '}
<span className="font-sans">{node.name}</span> <span className="font-sans">{node.name}</span>
</NavigationItem> </NavigationItem>
); );
})} })}
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
) : null} ) : null}
</nav>
{groupedNodes.typealias?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Types</h4>
<ChevronDown aria-hidden className='group-data-[state="open"]:hidden' size={24} />
<ChevronUp aria-hidden className='group-data-[state="closed"]:hidden' size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.typealias.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{groupedNodes.variable?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Variables</h4>
<ChevronDown aria-hidden className='group-data-[state="open"]:hidden' size={24} />
<ChevronUp aria-hidden className='group-data-[state="closed"]:hidden' size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.variable.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
</nav>
</aside>
); );
} }

View File

@@ -2,9 +2,10 @@
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren } from 'react';
import { isDrawerOpenAtom } from '~/stores/drawer'; import { isDrawerOpenAtom } from '@/stores/drawer';
import { cx } from '@/styles/cva';
export function NavigationItem({ export function NavigationItem({
node, node,
@@ -16,6 +17,7 @@ export function NavigationItem({
readonly packageName: string; readonly packageName: string;
readonly version: string; readonly version: string;
}>) { }>) {
const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const setDrawerOpen = useSetAtom(isDrawerOpenAtom); const setDrawerOpen = useSetAtom(isDrawerOpenAtom);
@@ -23,10 +25,19 @@ export function NavigationItem({
return ( return (
<Link <Link
className={`truncate rounded-md p-2 font-mono transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-800 md:px-1 md:py-1 ${pathname === href ? 'bg-neutral-200 font-medium text-blurple dark:bg-neutral-800' : ''}`} className={cx(
'dark:hover:text-base-neutral-40 hover:text-base-neutral-900 truncate rounded-lg p-2 font-mono text-[#676771] transition-colors hover:bg-[#e7e7e9] active:bg-[#e4e4e7] md:py-1 dark:text-[#83838b] dark:hover:bg-[#1d1d1e] dark:active:bg-[#27272b]',
pathname === href &&
'dark:text-base-neutral-40 text-base-neutral-900 bg-[#d9d9dc] font-medium dark:bg-[#323235] dark:hover:bg-[#323235]',
)}
href={href} href={href}
onClick={() => setDrawerOpen(false)} onClick={() => setDrawerOpen(false)}
onMouseEnter={() => router.prefetch(href)}
onTouchStart={() => router.prefetch(href)}
prefetch={false}
title={node.name} title={node.name}
// @ts-expect-error - unstable_dynamicOnHover is not part of the public types
unstable_dynamicOnHover
> >
{children} {children}
</Link> </Link>

View File

@@ -12,8 +12,8 @@ export async function Outline({ node }: { readonly node: any }) {
const hasAny = node.members?.properties?.length || node.members?.events?.length || node.members?.methods?.length; const hasAny = node.members?.properties?.length || node.members?.events?.length || node.members?.methods?.length;
return hasAny ? ( return hasAny ? (
<Collapsible className="flex flex-col gap-8" defaultOpen> <Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800"> <CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-[#e7e7e9] dark:hover:bg-[#242428]">
<h2 className="flex place-items-center gap-2 text-xl font-bold"> <h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscListSelection aria-hidden className="flex-shrink-0" size={24} /> Table of contents <VscListSelection aria-hidden className="flex-shrink-0" size={24} /> Table of contents
</h2> </h2>
@@ -22,11 +22,11 @@ export async function Outline({ node }: { readonly node: any }) {
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-4">
<div className="grid gap-2 sm:grid-cols-2"> <div className="grid gap-2 sm:grid-cols-2">
{node.members?.properties?.length ? ( {node.members?.properties?.length ? (
<Collapsible className="flex flex-col gap-4 px-4" defaultOpen> <Collapsible className="flex flex-col gap-2" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800"> <CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-[#e7e7e9] dark:hover:bg-[#242428]">
<h2 className="flex place-items-center gap-2 text-xl font-bold"> <h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolProperty aria-hidden className="flex-shrink-0" size={24} /> <VscSymbolProperty aria-hidden className="flex-shrink-0" size={24} />
Properties Properties
@@ -36,13 +36,13 @@ export async function Outline({ node }: { readonly node: any }) {
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="flex flex-col gap-2 px-4"> <div className="flex flex-col px-4">
{node.members.properties.map((property: any, idx: number) => ( {node.members.properties.map((property: any, idx: number) => (
<Fragment key={`${property.displayName}-${idx}`}> <Fragment key={`${property.displayName}-${idx}`}>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center"> <div className="flex place-content-between place-items-center">
<Link <Link
className="grow truncate rounded-md p-2 font-mono transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-800 md:px-1 md:py-1" className="max-w-[25ch] grow truncate rounded-md p-2 font-mono transition-colors hover:bg-[#e7e7e9] md:max-w-none md:py-1 dark:hover:bg-[#242428]"
href={`#${property.displayName}`} href={`#${property.displayName}`}
> >
{property.displayName} {property.displayName}
@@ -57,8 +57,8 @@ export async function Outline({ node }: { readonly node: any }) {
) : null} ) : null}
{node.members?.methods?.length ? ( {node.members?.methods?.length ? (
<Collapsible className="flex flex-col gap-4 px-4" defaultOpen> <Collapsible className="flex flex-col gap-2" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800"> <CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-[#e7e7e9] dark:hover:bg-[#242428]">
<h2 className="flex place-items-center gap-2 text-xl font-bold"> <h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolMethod aria-hidden className="flex-shrink-0" size={24} /> <VscSymbolMethod aria-hidden className="flex-shrink-0" size={24} />
Methods Methods
@@ -68,13 +68,13 @@ export async function Outline({ node }: { readonly node: any }) {
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="flex flex-col gap-2 px-4"> <div className="flex flex-col px-4">
{node.members.methods.map((method: any, idx: number) => ( {node.members.methods.map((method: any, idx: number) => (
<Fragment key={`${method.displayName}-${idx}`}> <Fragment key={`${method.displayName}-${idx}`}>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center"> <div className="flex place-content-between place-items-center">
<Link <Link
className="grow truncate rounded-md p-2 font-mono transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-800 md:px-1 md:py-1" className="max-w-[25ch] grow truncate rounded-md p-2 font-mono transition-colors hover:bg-[#e7e7e9] md:max-w-none md:py-1 dark:hover:bg-[#242428]"
href={`#${method.displayName}`} href={`#${method.displayName}`}
> >
{method.displayName} {method.displayName}
@@ -89,8 +89,8 @@ export async function Outline({ node }: { readonly node: any }) {
) : null} ) : null}
{node.members?.events?.length ? ( {node.members?.events?.length ? (
<Collapsible className="flex flex-col gap-4 px-4" defaultOpen> <Collapsible className="flex flex-col gap-2" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800"> <CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-[#e7e7e9] dark:hover:bg-[#242428]">
<h2 className="flex place-items-center gap-2 text-xl font-bold"> <h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolEvent aria-hidden className="flex-shrink-0" size={24} /> <VscSymbolEvent aria-hidden className="flex-shrink-0" size={24} />
Events Events
@@ -100,13 +100,13 @@ export async function Outline({ node }: { readonly node: any }) {
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="flex flex-col gap-2 px-4"> <div className="flex flex-col px-4">
{node.members.events.map((event: any, idx: number) => ( {node.members.events.map((event: any, idx: number) => (
<Fragment key={`${event.displayName}-${idx}`}> <Fragment key={`${event.displayName}-${idx}`}>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center"> <div className="flex place-content-between place-items-center">
<Link <Link
className="grow truncate rounded-md p-2 font-mono transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-800 md:px-1 md:py-1" className="max-w-[25ch] grow truncate rounded-md p-2 font-mono transition-colors hover:bg-[#e7e7e9] md:max-w-none md:py-1 dark:hover:bg-[#242428]"
href={`#${event.displayName}`} href={`#${event.displayName}`}
> >
{event.displayName} {event.displayName}
@@ -120,7 +120,7 @@ export async function Outline({ node }: { readonly node: any }) {
</Collapsible> </Collapsible>
) : null} ) : null}
</div> </div>
<div aria-hidden className="px-4"> <div aria-hidden className="p-4">
<div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" /> <div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" />
</div> </div>
</div> </div>

View File

@@ -1,7 +1,18 @@
'use client'; 'use client';
import { OverlayScrollbars, ClickScrollPlugin } from 'overlayscrollbars'; import { OverlayScrollbars, ClickScrollPlugin } from 'overlayscrollbars';
import type { OverlayScrollbarsComponentProps } from 'overlayscrollbars-react';
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
import { cx } from '@/styles/cva';
OverlayScrollbars.plugin(ClickScrollPlugin); OverlayScrollbars.plugin(ClickScrollPlugin);
export { OverlayScrollbarsComponent } from 'overlayscrollbars-react'; export function Scrollbars(props: OverlayScrollbarsComponentProps) {
const { className, children, ...additionalProps } = props;
return (
<OverlayScrollbarsComponent {...additionalProps} className={cx('', className)} defer>
{children}
</OverlayScrollbarsComponent>
);
}

View File

@@ -0,0 +1,30 @@
'use client';
import { useParams, useRouter } from 'next/navigation';
import { Select, SelectList, SelectOption, SelectTrigger } from '@/components/ui/Select';
import { PACKAGES } from '@/util/constants';
export function PackageSelect() {
const router = useRouter();
const params = useParams();
return (
<Select aria-label="Select a package" defaultSelectedKey={params.packageName as string}>
<SelectTrigger className="bg-[#f3f3f4] dark:bg-[#121214]" />
<SelectList classNames={{ popover: 'bg-[#f3f3f4] dark:bg-[#28282d]' }} items={PACKAGES}>
{(item) => (
<SelectOption
className="dark:pressed:bg-[#313135] bg-[#f3f3f4] dark:bg-[#28282d] dark:hover:bg-[#313135]"
href={`/docs/packages/${item.name}/stable`}
id={item.name}
key={item.name}
onHoverStart={() => router.prefetch(`/docs/packages/${item.name}/stable`)}
textValue={item.name}
>
{item.name}
</SelectOption>
)}
</SelectList>
</Select>
);
}

View File

@@ -15,13 +15,13 @@ export async function ParameterNode({
readonly version: string; readonly version: string;
}) { }) {
return ( return (
<div className={`${description ? 'flex flex-col gap-8' : 'inline'}`}> <div className={`${description ? 'flex flex-col gap-4' : 'inline'}`}>
{node.map((parameter: any, idx: number) => ( {node.map((parameter: any, idx: number) => (
<Fragment key={`${parameter.name}-${idx}`}> <Fragment key={`${parameter.name}-${idx}`}>
<div className={description ? 'group' : 'inline after:content-[",_"] last-of-type:after:content-none'}> <div className={description ? 'group' : 'inline after:content-[",_"] last-of-type:after:content-none'}>
<span className="font-mono font-semibold"> <span className="font-mono font-semibold">
{description ? ( {description ? (
<Link className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block" href={`#${parameter.name}`}> <Link className="float-left -ml-6 hidden pr-2 pb-2 group-hover:block" href={`#${parameter.name}`}>
<LinkIcon aria-hidden size={16} /> <LinkIcon aria-hidden size={16} />
</Link> </Link>
) : null} ) : null}
@@ -39,7 +39,7 @@ export async function ParameterNode({
</Fragment> </Fragment>
))} ))}
{description ? ( {description ? (
<div aria-hidden className="px-4"> <div aria-hidden className="p-4">
<div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" /> <div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" />
</div> </div>
) : null} ) : null}

View File

@@ -2,7 +2,7 @@ import { VscSymbolProperty } from '@react-icons/all-files/vsc/VscSymbolProperty'
import { ChevronDown, ChevronUp, Code2, LinkIcon } from 'lucide-react'; import { ChevronDown, ChevronUp, Code2, LinkIcon } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { ENV } from '~/util/env'; import { ENV } from '@/util/env';
import { Badges } from './Badges'; import { Badges } from './Badges';
import { DeprecatedNode } from './DeprecatedNode'; import { DeprecatedNode } from './DeprecatedNode';
import { ExcerptNode } from './ExcerptNode'; import { ExcerptNode } from './ExcerptNode';
@@ -21,8 +21,8 @@ export async function PropertyNode({
readonly version: string; readonly version: string;
}) { }) {
return ( return (
<Collapsible className="flex flex-col gap-8" defaultOpen> <Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800"> <CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-[#e7e7e9] dark:hover:bg-[#242428]">
<h2 className="flex place-items-center gap-2 text-xl font-bold"> <h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolProperty aria-hidden className="flex-shrink-0" size={24} /> <VscSymbolProperty aria-hidden className="flex-shrink-0" size={24} />
Properties Properties
@@ -32,19 +32,19 @@ export async function PropertyNode({
</CollapsibleTrigger> </CollapsibleTrigger>
<CollapsibleContent> <CollapsibleContent>
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-4">
{node.map((property: any, idx: number) => ( {node.map((property: any, idx: number) => (
<Fragment key={`${property.displayName}-${idx}`}> <Fragment key={`${property.displayName}-${idx}`}>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center"> <div className="flex place-content-between place-items-center gap-1">
<h3 <h3
className={`${ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-16' : 'scroll-mt-8'} group flex flex-col gap-2 break-words font-mono font-semibold`} className={`${ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-16' : 'scroll-mt-8'} group flex flex-col gap-2 px-2 font-mono font-semibold break-words`}
id={property.displayName} id={property.displayName}
> >
<Badges node={property} /> <Badges node={property} />
<span> <span>
<Link <Link
className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block" className="float-left -ml-6 hidden pr-2 pb-2 group-hover:block"
href={`#${property.displayName}`} href={`#${property.displayName}`}
> >
<LinkIcon aria-hidden size={16} /> <LinkIcon aria-hidden size={16} />
@@ -91,7 +91,7 @@ export async function PropertyNode({
<SeeNode node={property.summary.seeBlocks} padding version={version} /> <SeeNode node={property.summary.seeBlocks} padding version={version} />
) : null} ) : null}
</div> </div>
<div aria-hidden className="px-4"> <div aria-hidden className="p-4">
<div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" /> <div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" />
</div> </div>
</Fragment> </Fragment>

View File

@@ -2,24 +2,29 @@
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { Command, Search } from 'lucide-react'; import { Command, Search } from 'lucide-react';
import { isCmdKOpenAtom } from '~/stores/cmdk'; import { isCmdKOpenAtom } from '@/stores/cmdk';
import { useSidebar } from './ui/Sidebar';
export function SearchButton() { export function SearchButton() {
const { setOpenMobile } = useSidebar();
const setIsOpen = useSetAtom(isCmdKOpenAtom); const setIsOpen = useSetAtom(isCmdKOpenAtom);
return ( return (
<button <button
aria-label="Open search" aria-label="Open search"
className="flex place-content-between place-items-center rounded-md bg-neutral-200 p-2 dark:bg-neutral-800" className="bg-base-neutral-100 dark:bg-base-neutral-900 flex place-content-between place-items-center rounded-sm p-2"
onClick={() => setIsOpen(true)} onClick={() => {
setOpenMobile(false);
setIsOpen(true);
}}
type="button" type="button"
> >
<span className="flex place-items-center gap-2"> <span className="flex place-items-center gap-2">
<Search aria-hidden size={20} /> <Search aria-hidden size={18} />
Search... Search...
</span> </span>
<span className="hidden place-items-center gap-1 md:flex"> <span className="hidden place-items-center gap-1 md:flex">
<Command aria-hidden size={20} /> K <Command aria-hidden size={18} /> K
</span> </span>
</button> </button>
); );

View File

@@ -1,11 +1,4 @@
import { getHighlighterCore } from 'shiki/core'; import { codeToHtml } from '@/util/shiki.bundle';
import getWasm from 'shiki/wasm';
const highlighter = await getHighlighterCore({
themes: [import('shiki/themes/github-light.mjs'), import('shiki/themes/github-dark-dimmed.mjs')],
langs: [import('shiki/langs/typescript.mjs'), import('shiki/langs/javascript.mjs')],
loadWasm: getWasm,
});
export async function SyntaxHighlighter({ export async function SyntaxHighlighter({
lang, lang,
@@ -16,7 +9,7 @@ export async function SyntaxHighlighter({
readonly code: string; readonly code: string;
readonly lang: string; readonly lang: string;
}) { }) {
const codeHTML = highlighter.codeToHtml(code.trim(), { const codeHTML = await codeToHtml(code.trim(), {
lang, lang,
themes: { themes: {
light: 'github-light', light: 'github-light',

View File

@@ -0,0 +1,21 @@
'use client';
import { VscColorMode } from '@react-icons/all-files/vsc/VscColorMode';
import dynamic from 'next/dynamic';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/Button';
export function ThemeSwitch() {
const { resolvedTheme, setTheme } = useTheme();
const toggleTheme = () => setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
return (
<Button aria-label="Toggle theme" onPress={() => toggleTheme()} size="icon-sm" variant="filled">
<VscColorMode aria-hidden data-slot="icon" size={18} />
</Button>
);
}
export const ThemeSwitchNoSRR = dynamic(async () => ThemeSwitch, {
ssr: false,
});

View File

@@ -1,7 +1,8 @@
import { LinkIcon } from 'lucide-react'; import { LinkIcon } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { ENV } from '~/util/env'; import { cx } from '@/styles/cva';
import { ENV } from '@/util/env';
import { Badges } from './Badges'; import { Badges } from './Badges';
import { DocNode } from './DocNode'; import { DocNode } from './DocNode';
import { ExcerptNode } from './ExcerptNode'; import { ExcerptNode } from './ExcerptNode';
@@ -16,18 +17,22 @@ export async function TypeParameterNode({
readonly version: string; readonly version: string;
}) { }) {
return ( return (
<div className={`${description ? 'flex flex-col gap-8' : 'inline-block'}`}> <div className={`${description ? 'flex flex-col gap-4' : 'inline-block'}`}>
{node.map((typeParameter: any, idx: number) => ( {node.map((typeParameter: any, idx: number) => (
<Fragment key={`${typeParameter.name}-${idx}`}> <Fragment key={`${typeParameter.name}-${idx}`}>
<div className={description ? '' : 'inline after:content-[",_"] last-of-type:after:content-none'}> <div className={description ? '' : 'inline after:content-[",_"] last-of-type:after:content-none'}>
<h3 <h3
className={`${ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-16' : 'scroll-mt-8'} group inline break-words font-mono font-semibold`} className={cx(
ENV.IS_LOCAL_DEV || ENV.IS_PREVIEW ? 'scroll-mt-16' : 'scroll-mt-8',
'group inline font-mono font-semibold break-words',
description ? 'inline-block px-2' : '',
)}
id={typeParameter.name} id={typeParameter.name}
> >
{description ? <Badges node={typeParameter} /> : null} {description ? <Badges node={typeParameter} /> : null}
<span> <span>
{description ? ( {description ? (
<Link className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block" href={`#${typeParameter.name}`}> <Link className="float-left -ml-6 hidden pr-2 pb-2 group-hover:block" href={`#${typeParameter.name}`}>
<LinkIcon aria-hidden size={16} /> <LinkIcon aria-hidden size={16} />
</Link> </Link>
) : null} ) : null}
@@ -57,7 +62,7 @@ export async function TypeParameterNode({
</Fragment> </Fragment>
))} ))}
{description ? ( {description ? (
<div aria-hidden className="px-4"> <div aria-hidden className="p-4">
<div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" /> <div className="h-[2px] bg-neutral-300 dark:bg-neutral-700" role="separator" />
</div> </div>
) : null} ) : null}

View File

@@ -2,10 +2,10 @@ import { ExcerptNode } from './ExcerptNode';
export async function UnionMember({ node, version }: { readonly node: any; readonly version: string }) { export async function UnionMember({ node, version }: { readonly node: any; readonly version: string }) {
return ( return (
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-4">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">Union Members</h2> <h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">Union Members</h2>
<span className="flex flex-col gap-4 break-words font-mono text-sm"> <span className="flex flex-col gap-4 px-2 font-mono text-sm break-words">
<ExcerptNode node={node} version={version} /> <ExcerptNode node={node} version={version} />
</span> </span>
</div> </div>

View File

@@ -0,0 +1,35 @@
'use client';
import { useParams, useRouter } from 'next/navigation';
import { use } from 'react';
import { Select, SelectList, SelectOption, SelectTrigger } from './ui/Select';
export function VersionSelect({
versionsPromise,
}: {
readonly versionsPromise: Promise<{ readonly version: string }[]>;
}) {
const router = useRouter();
const params = useParams();
const versions = use(versionsPromise);
return (
<Select aria-label="Select a version" defaultSelectedKey={params.version as string}>
<SelectTrigger className="bg-[#f3f3f4] dark:bg-[#121214]" />
<SelectList classNames={{ popover: 'bg-[#f3f3f4] dark:bg-[#28282d]' }} items={versions}>
{(item) => (
<SelectOption
className="dark:pressed:bg-[#313135] bg-[#f3f3f4] dark:bg-[#28282d] dark:hover:bg-[#313135]"
href={`/docs/packages/${params.packageName}/${item.version}`}
id={item.version}
key={item.version}
onHoverStart={() => router.prefetch(`/docs/packages/${params.packageName}/${item.version}`)}
textValue={item.version}
>
{item.version}
</SelectOption>
)}
</SelectList>
</Select>
);
}

View File

@@ -48,18 +48,18 @@ export async function Alert({ title, type, children }: PropsWithChildren<IAlert>
const { text, border, icon } = resolveType(type); const { text, border, icon } = resolveType(type);
return ( return (
<div className="mb-4 mt-6" role="alert"> <div className="mt-6 mb-4" role="alert">
<div className="relative flex"> <div className="relative flex">
<div className="p-4">{children}</div> <div className="p-4">{children}</div>
<div className="pointer-events-none absolute flex h-full w-full"> <div className="pointer-events-none absolute flex h-full w-full">
<div className={`w-4 shrink-0 rounded-bl-md rounded-tl-md border-b-2 border-l-2 border-t-2 ${border}`} /> <div className={`w-4 shrink-0 rounded-tl-md rounded-bl-md border-t-2 border-b-2 border-l-2 ${border}`} />
<div className={`relative border-b-2 ${border}`}> <div className={`relative border-b-2 ${border}`}>
<div className={`pointer-events-auto flex -translate-y-1/2 place-items-center gap-2 px-2 ${text}`}> <div className={`pointer-events-auto flex -translate-y-1/2 place-items-center gap-2 px-2 ${text}`}>
{icon} {icon}
{title ? <span className={`font-semibold ${text}`}>{title}</span> : null} {title ? <span className={`font-semibold ${text}`}>{title}</span> : null}
</div> </div>
</div> </div>
<div className={`flex-1 rounded-br-md rounded-tr-md border-b-2 border-r-2 border-t-2 ${border}`} /> <div className={`flex-1 rounded-tr-md rounded-br-md border-t-2 border-r-2 border-b-2 ${border}`} />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,3 +1,28 @@
'use client'; 'use client';
export { Button } from 'react-aria-components'; import type { VariantProps } from 'cva';
import { Button as RACButton, composeRenderProps } from 'react-aria-components';
import type { ButtonProps as RACButtonProps } from 'react-aria-components';
import { buttonStyles } from '@/styles/ui/button';
export type ButtonProps = RACButtonProps & VariantProps<typeof buttonStyles>;
export function Button(props: ButtonProps) {
return (
<RACButton
{...props}
className={composeRenderProps(props.className, (className, renderProps) =>
buttonStyles({
...renderProps,
variant: props.variant,
size: props.size,
isDestructive: props.isDestructive,
isDark: props.isDark,
className,
}),
)}
>
{(values) => <>{typeof props.children === 'function' ? props.children(values) : props.children}</>}
</RACButton>
);
}

View File

@@ -0,0 +1,175 @@
'use client';
import { XIcon } from 'lucide-react';
import { useEffect, useRef, type ComponentProps, type ComponentPropsWithoutRef, type Ref } from 'react';
import type { HeadingProps as RACHeadingProps } from 'react-aria-components';
import { Dialog as RACDialog, Heading as RACHeading, Text as RACText } from 'react-aria-components';
import { useMediaQuery } from 'usehooks-ts';
import { Button, type ButtonProps } from '@/components/ui/Button';
import { cx } from '@/styles/cva';
export function Dialog(props: ComponentProps<typeof RACDialog>) {
return (
<RACDialog
{...props}
className={cx(
'peer/dialog group/dialog relative flex max-h-[inherit] flex-col overflow-hidden outline-hidden [scrollbar-width:thin]',
props.className,
)}
role={props.role!}
/>
);
}
export type DialogHeaderProps = ComponentPropsWithoutRef<'div'> & {
readonly description?: string;
readonly hasBorder?: boolean;
readonly title?: string;
};
export function DialogHeader({ hasBorder = false, ...props }: DialogHeaderProps) {
const headerRef = useRef<HTMLHeadingElement>(null);
useEffect(() => {
const header = headerRef.current;
if (!header) {
return;
}
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
header.parentElement?.style.setProperty('--dialog-header-height', `${entry.target.clientHeight}px`);
}
});
observer.observe(header);
return () => observer.unobserve(header);
}, []);
return (
<div
className={cx(
'relative flex flex-col gap-1 p-6 pb-4 [&[data-slot=dialog-header]:has(+[data-slot=dialog-footer])]:pb-0',
hasBorder &&
'border-base-neutral-100 dark:border-base-neutral-700 border-b [&[data-slot=dialog-header]:has(+[data-slot=dialog-footer])]:border-b',
props.className,
)}
data-slot="dialog-header"
ref={headerRef}
>
{props.title && <DialogTitle>{props.title}</DialogTitle>}
{props.description && <DialogDescription>{props.description}</DialogDescription>}
{!props.title && typeof props.children === 'string' ? <DialogTitle {...props} /> : props.children}
</div>
);
}
export type DialogTitleProps = RACHeadingProps & {
readonly ref?: Ref<HTMLHeadingElement>;
};
export function DialogTitle({ level = 2, ...props }: DialogTitleProps) {
return (
<RACHeading
{...props}
className={cx('text-base-label-md flex flex-1 place-items-center', props.className)}
level={level}
slot="title"
/>
);
}
export type DialogDescriptionProps = ComponentProps<'div'>;
export function DialogDescription(props: DialogDescriptionProps) {
return <RACText {...props} className={cx('text-sm', props.className)} slot="description" />;
}
export type DialogBodyProps = ComponentProps<'div'>;
export function DialogBody(props: DialogBodyProps) {
return (
<div
{...props}
className={cx(
'isolate flex max-h-[calc(var(--visual-viewport-height)-var(--visual-viewport-vertical-padding)-var(--dialog-header-height,0px)-var(--dialog-footer-height,0px))] flex-1 flex-col gap-6 overflow-auto px-6 py-1',
props.className,
)}
data-slot="dialog-body"
/>
);
}
export type DialogFooterProps = ComponentProps<'div'> & {
readonly hasBorder?: boolean;
};
export function DialogFooter({ hasBorder = false, ...props }: DialogFooterProps) {
const footerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const footer = footerRef.current;
if (!footer) {
return;
}
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
footer.parentElement?.style.setProperty('--dialog-footer-height', `${entry.target.clientHeight}px`);
}
});
observer.observe(footer);
return () => {
observer.unobserve(footer);
};
}, []);
return (
<div
{...props}
className={cx(
'isolate mt-auto flex flex-col-reverse place-content-between gap-3 p-6 sm:flex-row sm:place-content-end sm:place-items-center',
hasBorder && 'border-base-neutral-100 dark:border-base-neutral-700 border-t',
props.className,
)}
data-slot="dialog-footer"
ref={footerRef}
/>
);
}
export type DialogCloseProps = ButtonProps;
export function DialogClose(props: DialogCloseProps) {
return <Button {...props} slot="close" />;
}
export type CloseButtonIndicatorProps = Omit<ButtonProps, 'children'> & {
readonly className?: string;
readonly isDismissable?: boolean | undefined;
};
export function DialogCloseIndicator(props: CloseButtonIndicatorProps) {
const isMobile = useMediaQuery('(max-width: 600px)', { initializeWithValue: false });
return props.isDismissable ? (
<Button
{...props}
{...(isMobile ? { autoFocus: true } : {})}
aria-label="Close"
className={cx(
'close text-base-neutral-500 hover:text-base-neutral-700 focus-visible:text-base-neutral-700 pressed:text-base-neutral-900 dark:text-base-neutral-400 dark:hover:text-base-neutral-200 dark:focus-visible:text-base-neutral-200 dark:pressed:text-base-neutral-500 disabled:text-base-neutral-300 dark:disabled:text-base-neutral-300 absolute top-3 right-4 z-50 rounded-full',
props.className,
)}
size="icon-xs"
slot="close"
variant="unset"
>
<XIcon aria-hidden className="size-4.5 stroke-[1.5]" />
</Button>
) : null;
}
export { Button as DialogTrigger } from '@/components/ui/Button';

View File

@@ -1,37 +0,0 @@
'use client';
import { useAtom } from 'jotai';
import { ChevronUp } from 'lucide-react';
import { useEffect, type PropsWithChildren } from 'react';
import { useMediaQuery } from 'usehooks-ts';
import { Drawer as Vaul } from 'vaul';
import { isDrawerOpenAtom } from '~/stores/drawer';
export function Drawer({ children }: PropsWithChildren) {
const [open, setOpen] = useAtom(isDrawerOpenAtom);
const isMedium = useMediaQuery('(min-width: 768px)');
useEffect(() => {
if (isMedium) {
setOpen(false);
}
}, [isMedium, setOpen]);
return (
<Vaul.Root onOpenChange={setOpen} open={open}>
<Vaul.Trigger
aria-label="Open navigation"
className="flex h-12 w-full place-content-center place-items-center rounded-t-lg border-t border-neutral-300 bg-neutral-100 p-2 dark:border-neutral-700 dark:bg-neutral-900"
>
<ChevronUp aria-hidden size={28} />
</Vaul.Trigger>
<Vaul.Portal>
<Vaul.Overlay className="fixed inset-0 bg-black/40" />
<Vaul.Content className="fixed bottom-0 left-0 right-0 flex max-h-[86%] flex-col rounded-t-lg bg-neutral-100 p-4 dark:bg-neutral-900">
<div className="mx-auto mb-8 h-1.5 w-12 flex-shrink-0 rounded-full bg-neutral-400" />
{children}
</Vaul.Content>
</Vaul.Portal>
</Vaul.Root>
);
}

View File

@@ -0,0 +1,166 @@
'use client';
import { CheckIcon } from 'lucide-react';
import type { ComponentProps } from 'react';
import {
Collection as RACCollection,
composeRenderProps,
Header as RACHeader,
ListBoxItem as RACListBoxItem,
ListBoxSection as RACListBoxSection,
Separator as RACSeparator,
Text as RACText,
} from 'react-aria-components';
import type {
SectionProps as RACDropdownSectionProps,
ListBoxItemProps as RACListBoxItemProps,
TextProps as RACTextProps,
SeparatorProps as RACSeparatorProps,
} from 'react-aria-components';
import { Keyboard } from '@/components/ui/Keyboard';
import { cva, cx } from '@/styles/cva';
export type DropdownSectionProps<Type extends object> = RACDropdownSectionProps<Type> & {
readonly title?: string;
};
export function DropdownSection<Type extends object>(props: DropdownSectionProps<Type>) {
return (
<RACListBoxSection
{...props}
className={cx(
'col-span-full grid grid-cols-[auto_1fr] supports-[grid-template-columns:subgrid]:grid-cols-subgrid',
props.className,
)}
>
{props.title && (
<RACHeader className="text-base-label-sm text-base-neutral-600 dark:text-base-neutral-300 col-span-full px-3 py-2">
{props.title}
</RACHeader>
)}
<RACCollection items={props.items!}>{props.children}</RACCollection>
</RACListBoxSection>
);
}
export function DropdownLabel(props: RACTextProps) {
return <RACText {...props} className={cx('col-start-1', props.className)} slot="label" />;
}
export const dropdownItemStyles = cva({
base: [
'col-span-full grid grid-cols-[auto_1fr_1.5rem_0.5rem_auto] not-has-data-[slot=dropdown-item-details]:items-center has-data-[slot=dropdown-item-details]:**:data-[slot=checked-icon]:mt-[1.5px] supports-[grid-template-columns:subgrid]:grid-cols-subgrid',
'group forced-color:text-[Highlight] text-base-md relative h-10 cursor-default rounded-sm px-3 py-2.5 outline-0 forced-color-adjust-none select-none has-data-[slot=dropdown-item-details]:h-[inherit] has-data-[slot=dropdown-item-details]:place-content-center forced-colors:text-[LinkText]',
'text-base-neutral-900 dark:text-base-neutral-40 bg-base-neutral-0 dark:bg-base-neutral-800',
'hover:bg-base-neutral-80 dark:hover:bg-base-neutral-700/72',
'focus-visible:bg-base-neutral-80 dark:focus-visible:bg-base-neutral-700/72',
'pressed:bg-base-neutral-100 dark:pressed:bg-base-neutral-700/72',
'**:data-[slot=avatar]:mr-2 **:data-[slot=avatar]:size-6 **:data-[slot=avatar]:*:mr-2 **:data-[slot=avatar]:*:size-6 sm:**:data-[slot=avatar]:size-5 sm:**:data-[slot=avatar]:*:size-5',
'**:data-[slot=icon]:size-4.5 **:data-[slot=icon]:shrink-0',
'data-destructive:text-base-sunset-600 dark:data-destructive:text-base-sunset-400 data-destructive:hover:bg-base-sunset-100 dark:data-destructive:hover:bg-base-sunset-800 data-destructive:hover:text-base-sunset-700 dark:data-destructive:hover:text-base-sunset-300 data-destructive:focus-visible:bg-base-sunset-100 dark:data-destructive:focus-visible:bg-base-sunset-800 data-destructive:focus-visible:hover:text-base-sunset-700 dark:data-destructive:focus-visible:text-base-sunset-300 data-destructive:pressed:bg-base-sunset-200 dark:data-destructive:pressed:bg-base-sunset-700 data-destructive:pressed:text-base-sunset-800 dark:data-destructive:pressed:text-base-sunset-100',
'*:data-[slot=icon]:mr-2 data-[slot=menu-radio]:*:data-[slot=icon]:size-4.5',
'forced-colors:**:data-[slot=icon]:text-[CanvasText] forced-colors:group-data-focused:**:data-[slot=icon]:text-[Canvas]',
'[&>[slot=label]+[data-slot=icon]]:absolute [&>[slot=label]+[data-slot=icon]]:right-0',
],
variants: {
isFocused: {
true: 'forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]',
false: '',
},
isDisabled: {
true: 'opacity-50 forced-colors:text-[GrayText]',
},
},
});
export type DropdownItemProps = RACListBoxItemProps & {
readonly classNames?: {
readonly selected?: string;
};
};
export function DropdownItem(props: DropdownItemProps) {
const textValue = props.textValue ?? (typeof props.children === 'string' ? props.children : undefined);
return (
<RACListBoxItem
{...props}
className={composeRenderProps(props.className, (className, renderProps) =>
dropdownItemStyles({
...renderProps,
className,
}),
)}
textValue={textValue!}
>
{composeRenderProps(props.children, (children, { isSelected }) => (
<>
{typeof children === 'string' ? <DropdownLabel>{children}</DropdownLabel> : children}
{isSelected && (
<div
className={cx(
'bg-base-blurple-400 dark:bg-base-blurple-400 flex size-[18px] place-content-center place-items-center place-self-start rounded-full',
props.classNames?.selected,
)}
data-slot="checked-icon"
>
<CheckIcon aria-hidden className="text-base-neutral-40 size-3.5" />
</div>
)}
</>
))}
</RACListBoxItem>
);
}
export type DropdownItemDetailProps = RACTextProps & {
readonly classNames?: {
readonly description?: RACTextProps['className'];
readonly label?: RACTextProps['className'];
};
readonly description?: RACTextProps['children'];
readonly label?: RACTextProps['children'];
};
export function DropdownItemDetails(props: DropdownItemDetailProps) {
return (
<div {...props} className="col-start-1 flex flex-col gap-1" data-slot="dropdown-item-details">
{props.label && (
<RACText
{...props}
className={cx('text-base-md max-w-[25ch] truncate', props.classNames?.label)}
slot={props.slot ?? 'label'}
>
{props.label}
</RACText>
)}
{props.description && (
<RACText
{...props}
className={cx('text-base-sm text-base-neutral-600 dark:text-base-neutral-300', props.classNames?.description)}
slot={props.slot ?? 'description'}
>
{props.description}
</RACText>
)}
{!props.title && props.children}
</div>
);
}
export function DropdownSeparator(props: RACSeparatorProps) {
return (
<RACSeparator
{...props}
className={cx(
'bg-base-neutral-100 dark:bg-base-neutral-700 col-span-full mx-3 my-2 h-px forced-colors:bg-[ButtonBorder]',
props.className,
)}
orientation="horizontal"
/>
);
}
export function DropdownKeyboard(props: ComponentProps<typeof Keyboard>) {
return <Keyboard {...props} className={cx('absolute right-2 pl-2', props.className)} />;
}

View File

@@ -0,0 +1,117 @@
'use client';
import { AlertCircleIcon } from 'lucide-react';
import type { ReactNode } from 'react';
import type {
LabelProps as RACLabelProps,
TextProps as RACTextProps,
FieldErrorProps as RACFieldErrorProps,
GroupProps as RACGroupProps,
InputProps as RACInputProps,
} from 'react-aria-components';
import {
Label as RACLabel,
Text as RACText,
FieldError as RACFieldError,
Group as RACGroup,
Input as RACInput,
composeRenderProps,
} from 'react-aria-components';
import { compose, cva, cx } from '@/styles/cva';
import { focusRing } from '@/styles/ui/focusRing';
import { composeTailwindRenderProps } from '@/styles/util';
export function Label(props: RACLabelProps) {
return (
<RACLabel
{...props}
className={cx(
'text-base-neutral-800 dark:text-base-neutral-100 group-invalid:text-base-sunset-500 group-disabled:text-base-neutral-900 dark:group-disabled:text-base-neutral-40 text-base-label-md w-fit group-disabled:opacity-38',
props.className,
)}
/>
);
}
export function Description(props: RACTextProps) {
return (
<RACText
{...props}
className={cx('text-base-neutral-600 dark:text-base-neutral-300 text-base-sm text-pretty', props.className)}
slot="description"
/>
);
}
export function FieldError(props: RACFieldErrorProps) {
return (
<RACFieldError
{...props}
className={composeTailwindRenderProps(
props.className,
'text-base-sunset-500 text-base-sm flex place-items-center gap-1.5 forced-colors:text-[Mark]',
)}
>
<AlertCircleIcon aria-hidden className="shrink-0" data-slot="icon" size={18} strokeWidth={1.5} />
{props.children as ReactNode}
</RACFieldError>
);
}
export const fieldGroupStyles = compose(
focusRing,
cva({
base: [
'group relative flex h-10 place-items-center overflow-hidden rounded-sm border transition duration-200 ease-out forced-colors:outline-[Highlight]',
'bg-base-neutral-0 border-base-neutral-300 dark:bg-base-neutral-800 dark:border-base-neutral-500',
'hover:border-base-neutral-200 dark:hover:border-base-neutral-600',
'focus-within:border-base-neutral-200 dark:focus-within:border-base-neutral-600',
'disabled:opacity-38 forced-colors:disabled:border forced-colors:disabled:border-[GrayText]',
'group-invalid:border-base-sunset-500 forced-colors:group-invalid:border-[Mark]',
'group-invalid:hover:border-base-sunset-200 dark:group-invalid:hover:border-base-sunset-700',
'group-invalid:focus-within:border-base-sunset-200 dark:group-invalid:focus-within:border-base-sunset-700',
'[&>[role=progressbar]:first-child]:ml-2 [&>[role=progressbar]:last-child]:mr-2',
'**:data-[slot=icon]:size-6 **:data-[slot=icon]:shrink-0 **:[button]:shrink-0',
'[&>button:has([data-slot=icon])]:absolute [&>button:has([data-slot=icon]):first-child]:left-0 [&>button:has([data-slot=icon]):last-child]:right-0',
'*:data-[slot=icon]:text-base-neutral-800 dark:*:data-[slot=icon]:text-base-neutral-100 *:data-[slot=icon]:pointer-events-none *:data-[slot=icon]:absolute *:data-[slot=icon]:top-[calc(var(--spacing)_*_1.7)] *:data-[slot=icon]:z-10 *:data-[slot=icon]:size-6',
'[&>[data-slot=icon]:first-child]:left-2 [&>[data-slot=icon]:last-child]:right-2',
'[&:has([data-slot=icon]+input)]:pl-7.5 [&:has(input+[data-slot=icon])]:pr-7.5',
'[&:has([data-slot=icon]+[role=group])]:pl-7.5 [&:has([role=group]+[data-slot=icon])]:pr-7.5',
'has-[[data-slot=icon]:last-child]:[&_input]:pr-7.5',
'*:[button]:size-6 *:[button]:p-0',
'[&>button:first-child]:ml-2 [&>button:last-child]:mr-2',
],
variants: {
isFocusWithin: {
true: 'outline-2',
false: 'outline-0',
},
},
}),
);
export function FieldGroup(props: RACGroupProps) {
return (
<RACGroup
{...props}
className={composeRenderProps(props.className, (className, renderProps) =>
fieldGroupStyles({
...renderProps,
className,
}),
)}
/>
);
}
export function Input(props: RACInputProps) {
return (
<RACInput
{...props}
className={composeTailwindRenderProps(
props.className,
'text-base-neutral-900 placeholder:text-base-neutral-400 dark:placeholder:text-base-neutral-500 dark:text-base-neutral-40 text-base-lg sm:text-base-md w-full min-w-0 bg-transparent px-3 py-2.5 outline-hidden focus:outline-hidden [&::-ms-reveal]:hidden [&::-webkit-search-cancel-button]:hidden',
)}
/>
);
}

View File

@@ -0,0 +1,38 @@
'use client';
import type { ComponentProps } from 'react';
import { Keyboard as RACKeyboard } from 'react-aria-components';
import { cx } from '@/styles/cva';
type KeyboardProps = ComponentProps<'div'> & {
readonly classNames?: {
readonly base?: string;
readonly kbd?: string;
};
readonly keys: string[] | string;
};
export function Keyboard(props: KeyboardProps) {
return (
<RACKeyboard
{...props}
className={cx(
'hidden group-disabled:opacity-50 lg:inline-flex forced-colors:group-focus:text-[HighlightText]',
props.classNames?.base,
)}
>
{(Array.isArray(props.keys) ? props.keys : props.keys.split('')).map((char, idx) => (
<kbd
className={cx(
'inline-grid min-h-5 min-w-[2ch] place-content-center text-center font-sans text-[.75rem] uppercase',
idx > 0 && char.length > 1 && 'pl-1',
props.classNames?.kbd,
)}
key={idx}
>
{char}
</kbd>
))}
</RACKeyboard>
);
}

View File

@@ -1,3 +1,67 @@
'use client'; 'use client';
export { ListBox, ListBoxItem } from 'react-aria-components'; import { CheckIcon, MenuIcon } from 'lucide-react';
import type { ComponentProps } from 'react';
import type { ListBoxItemProps as RACListBoxItemProps, ListBoxProps as RACListBoxProps } from 'react-aria-components';
import { ListBoxItem as RACListBoxItem, ListBox as RACListBox, composeRenderProps } from 'react-aria-components';
import { DropdownLabel, DropdownSection, dropdownItemStyles } from '@/components/ui/Dropdown';
import { cx } from '@/styles/cva';
export function ListBox<Type extends object>(props: RACListBoxProps<Type>) {
return (
<RACListBox
{...props}
className={composeRenderProps(props.className, (className) =>
cx(
[
'border-base-neutral-200 dark:border-base-neutral-600 shadow-base-sm flex max-h-96 w-full min-w-40 flex-col gap-x-1 overflow-y-auto rounded-sm border p-2 outline-hidden [scrollbar-width:thin]',
"grid grid-cols-[1fr_auto] overflow-auto *:[[role='group']+[role=group]]:mt-4 *:[[role='group']+[role=separator]]:mt-1",
],
className,
),
)}
/>
);
}
export type ListBoxItemProps<Type extends object> = RACListBoxItemProps<Type> & {
readonly className?: string;
};
export function ListBoxItem<Type extends object>(props: ListBoxItemProps<Type>) {
const textValue = props.textValue ?? (typeof props.children === 'string' ? props.children : undefined);
return (
<RACListBoxItem
{...props}
className={composeRenderProps(props.className, (className, renderProps) =>
dropdownItemStyles({
...renderProps,
className,
}),
)}
textValue={textValue!}
>
{({ allowsDragging, isSelected, isFocused, isDragging }) => (
<>
{allowsDragging && (
<MenuIcon
className={cx('size-4 shrink-0 transition', isFocused && '', isDragging && '', isSelected && '')}
size={16}
/>
)}
{isSelected && <CheckIcon className="-mx-0.5 mr-2" data-slot="checked-icon" size={16} />}
{typeof props.children === 'string' ? <DropdownLabel>{props.children}</DropdownLabel> : props.children}
</>
)}
</RACListBoxItem>
);
}
export type ListBoxSectionProps = ComponentProps<typeof DropdownSection>;
export function ListBoxSection(props: ListBoxSectionProps) {
return <DropdownSection {...props} className={cx(props.className, 'gap-1')} />;
}
export { DropdownItemDetails as ListBoxItemDetails } from '@/components/ui/Dropdown';

View File

@@ -1,95 +0,0 @@
'use client';
import { ChevronsUpDown } from 'lucide-react';
import { useEffect, useState } from 'react';
import type { Key } from 'react-aria-components';
import { useMediaQuery } from 'usehooks-ts';
import { Drawer as Vaul } from 'vaul';
import { PACKAGES } from '~/util/constants';
import { Button } from './Button';
import { ListBox, ListBoxItem } from './ListBox';
import { Popover } from './Popover';
import { Select, SelectValue } from './Select';
export function PackageSelect({ packageName }: { readonly packageName: string }) {
const [selectedPackage, setSelectedPackage] = useState<Key>(packageName);
const [open, setOpen] = useState(false);
const isMedium = useMediaQuery('(min-width: 768px)');
useEffect(() => {
if (isMedium) {
setOpen(false);
}
}, [isMedium, setOpen]);
return (
<>
<Select
aria-label="Select a package"
className="hidden md:block"
onSelectionChange={(selected) => {
setSelectedPackage(selected);
}}
selectedKey={selectedPackage}
>
<Button className="flex w-full place-content-between place-items-center rounded-md bg-neutral-200 p-2 dark:bg-neutral-800">
<SelectValue className="font-medium" />
<ChevronsUpDown aria-hidden size={20} />
</Button>
<Popover className="max-h-60 w-[--trigger-width] overflow-auto rounded-md border border-neutral-300 bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800">
<ListBox items={PACKAGES} shouldFocusWrap>
{(item) => (
<ListBoxItem
className="flex p-2 outline-none data-[focus-visible]:bg-neutral-300 data-[hovered]:bg-neutral-300 data-[selected]:bg-blurple data-[selected]:data-[focus-visible]:bg-blurple-500 data-[selected]:data-[hovered]:bg-blurple-500 data-[selected]:text-white dark:data-[focus-visible]:bg-neutral-700 dark:data-[hovered]:bg-neutral-700 dark:data-[selected]:data-[focus-visible]:bg-blurple-500 dark:data-[selected]:data-[hovered]:bg-blurple-500"
href={`/docs/packages/${item.name}/stable`}
id={item.name}
textValue={item.name}
>
{item.name}
</ListBoxItem>
)}
</ListBox>
</Popover>
</Select>
<Vaul.NestedRoot onOpenChange={setOpen} open={open}>
<Vaul.Trigger
aria-label="Open package select"
className="flex w-full place-content-between place-items-center rounded-md bg-neutral-200 p-2 dark:bg-neutral-800 md:hidden"
>
<span className="font-medium">{selectedPackage}</span>
<ChevronsUpDown aria-hidden size={20} />
</Vaul.Trigger>
<Vaul.Portal>
<Vaul.Overlay className="fixed inset-0 bg-black/40" />
<Vaul.Content className="fixed bottom-0 left-0 right-0 flex max-h-[80%] flex-col rounded-t-lg bg-neutral-100 p-4 dark:bg-neutral-900">
<div className="mx-auto mb-8 h-1.5 w-12 flex-shrink-0 rounded-full bg-neutral-400" />
<ListBox
aria-label="Select a package"
className="flex flex-col gap-2 overflow-auto"
items={PACKAGES}
onSelectionChange={(selected) => {
const [val] = selected;
setSelectedPackage(val as Key);
}}
selectedKeys={[selectedPackage]}
selectionMode="single"
shouldFocusWrap
>
{(item) => (
<ListBoxItem
className="rounded-md p-2 outline-none data-[focus-visible]:bg-neutral-300 data-[hovered]:bg-neutral-300 data-[selected]:bg-blurple data-[selected]:data-[focus-visible]:bg-blurple-500 data-[selected]:data-[hovered]:bg-blurple-500 data-[selected]:text-white dark:data-[focus-visible]:bg-neutral-700 dark:data-[hovered]:bg-neutral-700 dark:data-[selected]:data-[focus-visible]:bg-blurple-500 dark:data-[selected]:data-[hovered]:bg-blurple-500"
href={`/docs/packages/${item.name}/stable`}
id={item.name}
textValue={item.name}
>
{item.name}
</ListBoxItem>
)}
</ListBox>
</Vaul.Content>
</Vaul.Portal>
</Vaul.NestedRoot>
</>
);
}

View File

@@ -1,3 +1,159 @@
'use client'; 'use client';
export { Popover } from 'react-aria-components'; import type { CSSProperties, ReactNode } from 'react';
import type {
DialogProps as RACDialogProps,
PopoverProps as RACPopoverProps,
ModalOverlayProps as RACModalOverlayProps,
DialogTriggerProps as RACDialogTriggerProps,
} from 'react-aria-components';
import {
Modal as RACModal,
ModalOverlay as RACModalOverlay,
OverlayArrow as RACOverlayArrow,
PopoverContext as RACPopoverContext,
DialogTrigger as RACDialogTrigger,
Popover as RACPopover,
composeRenderProps,
useSlottedContext,
} from 'react-aria-components';
import { useMediaQuery } from 'usehooks-ts';
import type { DialogBodyProps, DialogFooterProps, DialogHeaderProps, DialogTitleProps } from '@/components/ui/Dialog';
import { Dialog, DialogBody, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/Dialog';
import { cva, cx } from '@/styles/cva';
export function Popover(props: RACDialogTriggerProps) {
return <RACDialogTrigger {...props} />;
}
export function PopoverTitle({ level = 2, ...props }: DialogTitleProps) {
return <DialogTitle {...props} className={cx('sm:leading-none', level === 2 && 'sm:text-lg', props.className)} />;
}
export function PopoverHeader(props: DialogHeaderProps) {
return <DialogHeader {...props} className={cx('sm:p-4', props.className)} />;
}
export function PopoverBody(props: DialogBodyProps) {
return <DialogBody {...props} className={cx('gap-0 sm:px-4 sm:pt-0', props.className)} />;
}
export function PopoverFooter(props: DialogFooterProps) {
return <DialogFooter {...props} className={cx('sm:p-4', props.className)} />;
}
const contentStyles = cva({
base: 'peer/popover-content border-base-neutral-200 dark:border-base-neutral-600 shadow-base-sm bg-base-neutral-0 dark:bg-base-neutral-800 text-base-md max-w-xs rounded-sm border bg-clip-padding transition-transform [scrollbar-width:thin] sm:max-w-3xl dark:backdrop-saturate-200 forced-colors:bg-[Canvas]',
variants: {
isPicker: {
true: 'max-h-72 min-w-(--trigger-width) overflow-y-auto',
false: 'min-w-80',
},
isMenu: {
true: '',
},
isEntering: {
true: 'fade-in animate-in data-[placement=left]:slide-in-from-right-1 data-[placement=right]:slide-in-from-left-1 data-[placement=top]:slide-in-from-bottom-1 data-[placement=bottom]:slide-in-from-top-1 duration-150 ease-out',
},
isExiting: {
true: 'fade-out animate-out data-[placement=left]:slide-out-to-right-1 data-[placement=right]:slide-out-to-left-1 data-[placement=top]:slide-out-to-bottom-1 data-[placement=bottom]:slide-out-to-top-1 duration-100 ease-in',
},
},
});
const drawerStyles = cva({
base: 'fixed top-auto bottom-0 z-50 max-h-full w-full max-w-2xl border border-b-transparent bg-neutral-100 outline-hidden dark:bg-neutral-900',
variants: {
isMenu: {
true: 'p-0 [&_[role=dialog]]:*:not-has-[[data-slot=dialog-body]]:px-1',
false: '',
},
isEntering: {
true: [
'[will-change:transform] [transition:transform_0.5s_cubic-bezier(0.32,_0.72,_0,_1)]',
'fade-in-0 slide-in-from-bottom-56 animate-in duration-200',
'[transition:translate3d(0,_100%,_0)]',
'sm:slide-in-from-bottom-auto sm:slide-in-from-top-[20%]',
],
},
isExiting: {
true: 'slide-out-to-bottom-56 animate-out duration-200 ease-in',
},
},
});
export type PopoverContentProps = Omit<RACModalOverlayProps, 'className'> &
Omit<RACPopoverProps, 'children' | 'className'> &
Pick<RACDialogProps, 'aria-label' | 'aria-labelledby'> & {
readonly children: ReactNode;
readonly className?: string | ((values: { defaultClassName?: string }) => string);
readonly respectScreen?: boolean;
readonly showArrow?: boolean;
readonly style?: CSSProperties;
};
export function PopoverContent({ respectScreen = true, showArrow = true, ...props }: PopoverContentProps) {
const isMobile = useMediaQuery('(max-width: 600px)', { initializeWithValue: false });
const popoverContext = useSlottedContext(RACPopoverContext);
const isMenuTrigger = popoverContext?.trigger === 'MenuTrigger';
const isSubmenuTrigger = popoverContext?.trigger === 'SubmenuTrigger';
const isMenu = isMenuTrigger || isSubmenuTrigger;
const isComboBoxTrigger = popoverContext?.trigger === 'ComboBox';
const offset = props.offset ?? (showArrow ? 6 : 4);
const effectiveOffset = isSubmenuTrigger ? offset - 2 : offset;
return isMobile && respectScreen ? (
<RACModalOverlay
{...props}
className="fixed top-0 left-0 isolate z-50 h-(--visual-viewport-height) w-full [--visual-viewport-vertical-padding:16px]"
isDismissable
>
<RACModal
className={composeRenderProps(props.className as string, (className, renderProps) =>
drawerStyles({ ...renderProps, isMenu, className }),
)}
>
<Dialog aria-label={props['aria-label'] ?? 'List item'} role="dialog">
{props.children}
</Dialog>
</RACModal>
</RACModalOverlay>
) : (
<RACPopover
{...props}
className={composeRenderProps(props.className as string, (className, renderProps) =>
contentStyles({
...renderProps,
className,
}),
)}
offset={effectiveOffset}
>
{showArrow && (
<RACOverlayArrow className="group">
<svg
className="fill-base-neutral-0 dark:fill-base-neutral-800 stroke-base-neutral-200 dark:stroke-base-neutral-600 block group-data-[placement=bottom]:rotate-180 group-data-[placement=left]:-rotate-90 group-data-[placement=right]:rotate-90 forced-colors:fill-[Canvas] forced-colors:stroke-[ButtonBorder]"
height={12}
viewBox="0 0 12 12"
width={12}
>
<path d="M0 0 L6 6 L12 0" />
</svg>
</RACOverlayArrow>
)}
{isComboBoxTrigger ? (
props.children
) : (
<Dialog aria-label={props['aria-label'] ?? 'List item'} role="dialog">
{props.children}
</Dialog>
)}
</RACPopover>
);
}
export {
DialogTrigger as PopoverTrigger,
DialogDescription as PopoverDescription,
DialogClose as PopoverClose,
} from '@/components/ui/Dialog';

View File

@@ -1,3 +1,133 @@
'use client'; 'use client';
export { Select, SelectValue } from 'react-aria-components'; import { ChevronDownIcon } from 'lucide-react';
import type { ComponentProps, ReactNode } from 'react';
import type {
ListBoxProps as RACListBoxProps,
SelectProps as RACSelectProps,
ValidationResult as RACValidationResult,
} from 'react-aria-components';
import {
Button as RACButton,
Select as RACSelect,
SelectValue as RACSelectValue,
composeRenderProps,
} from 'react-aria-components';
import type { Button } from '@/components/ui/Button';
import { Description, FieldError, Label } from '@/components/ui/Field';
import { ListBox } from '@/components/ui/ListBox';
import { PopoverContent, type PopoverContentProps } from '@/components/ui/Popover';
import { compose, cva, cx } from '@/styles/cva';
import { focusRing } from '@/styles/ui/focusRing';
import { composeTailwindRenderProps } from '@/styles/util';
const selectTriggerStyles = compose(
focusRing,
cva({
base: [
'relative flex h-10 w-full place-items-center overflow-hidden rounded-sm border transition duration-200 ease-out forced-colors:outline-[Highlight]',
'bg-base-neutral-0 border-base-neutral-300 dark:bg-base-neutral-800 dark:border-base-neutral-500',
'hover:border-base-neutral-200 dark:hover:border-base-neutral-600',
'focus-visible:border-base-neutral-200 dark:focus-visible:border-base-neutral-600',
'group-open:border-base-neutral-200 dark:group-open:border-base-neutral-600 group-open:outline-2',
'group-disabled:bg-base-neutral-100 group-disabled:border-base-neutral-100 dark:group-disabled:border-base-neutral-400 dark:group-disabled:bg-base-neutral-400 group-disabled:opacity-38 group-disabled:forced-colors:border group-disabled:forced-colors:border-[GrayText]',
'group-invalid:border-base-sunset-500 forced-colors:group-invalid:border-[Mark]',
'group-invalid:hover:border-base-sunset-200 dark:group-invalid:hover:border-base-sunset-700',
'group-invalid:focus-visible:border-base-sunset-200 dark:group-invalid:focus-visible:border-base-sunset-700',
'**:data-[slot=icon]:size-6 **:data-[slot=icon]:shrink-0 **:[button]:shrink-0',
'[&>button:has([data-slot=icon])]:absolute [&>button:has([data-slot=icon]):first-child]:left-0 [&>button:has([data-slot=icon]):last-child]:right-0',
'*:data-[slot=icon]:text-base-neutral-800 dark:*:data-[slot=icon]:text-base-neutral-100 *:data-[slot=icon]:pointer-events-none *:data-[slot=icon]:absolute *:data-[slot=icon]:top-[calc(var(--spacing)_*_1.7)] *:data-[slot=icon]:z-10 *:data-[slot=icon]:size-6',
'[&>[data-slot=icon]:first-child]:left-2 [&>[data-slot=icon]:last-child]:right-2',
'[&:has([data-slot=icon]+input)]:pl-7.5 [&:has(input+[data-slot=icon])]:pr-7.5',
'[&:has([data-slot=icon]+[role=group])]:pl-7.5 [&:has([role=group]+[data-slot=icon])]:pr-7.5',
'has-[[data-slot=icon]:last-child]:[&_input]:pr-7.5',
'*:[button]:size-6 *:[button]:p-0',
'[&>button:first-child]:ml-2 [&>button:last-child]:mr-2',
],
}),
);
export type SelectProps<Type extends object> = RACSelectProps<Type> & {
readonly className?: string;
readonly description?: string;
readonly errorMessage?: string | ((validation: RACValidationResult) => string) | undefined;
readonly items?: Iterable<Type>;
readonly label?: ReactNode | string;
};
export function Select<Type extends object>(props: SelectProps<Type>) {
return (
<RACSelect {...props} className={composeTailwindRenderProps(props.className, 'group flex w-full flex-col gap-1.5')}>
{(values) => (
<>
{props.label && <Label>{props.label}</Label>}
{typeof props.children === 'function' ? props.children(values) : props.children}
{props.description && <Description>{props.description}</Description>}
<FieldError>{props.errorMessage}</FieldError>
</>
)}
</RACSelect>
);
}
export type SelectListProps<Type extends object> = Pick<PopoverContentProps, 'placement'> &
RACListBoxProps<Type> & {
readonly classNames?: {
readonly popover?: PopoverContentProps['className'];
};
readonly items?: Iterable<Type>;
};
export function SelectList<Type extends object>(props: SelectListProps<Type>) {
return (
<PopoverContent
className={cx('w-(--trigger-width)', props.classNames?.popover)}
placement={props.placement!}
respectScreen={false}
showArrow={false}
>
<ListBox {...props} className={cx('border-0', props.classNames?.popover)} items={props.items!}>
{props.children}
</ListBox>
</PopoverContent>
);
}
export type SelectTriggerProps = ComponentProps<typeof Button> & {
readonly className?: string;
readonly prefix?: ReactNode;
};
export function SelectTrigger(props: SelectTriggerProps) {
return (
<RACButton
className={composeRenderProps(props.className, (className, renderProps) =>
selectTriggerStyles({
...renderProps,
className,
}),
)}
>
{props.prefix && <span className="-mr-1 ml-2 *:data-[slot=icon]:size-5.5">{props.prefix}</span>}
<RACSelectValue
className="text-base-neutral-900 group-disabled:data-placeholder:text-base-neutral-900 dark:group-disabled:data-placeholder:text-base-neutral-40 dark:data-placeholder:text-base-neutral-500 dark:text-base-neutral-40 data-placeholder:text-base-neutral-400 text-base-lg sm:text-base-md grid flex-1 grid-cols-[auto_1fr] place-items-start items-center px-3 py-2.5 *:data-[slot=avatar]:*:-mx-0.5 *:data-[slot=avatar]:-mx-0.5 *:data-[slot=avatar]:*:mr-2 *:data-[slot=avatar]:mr-2 *:data-[slot=icon]:-mx-0.5 *:data-[slot=icon]:mr-1 *:data-[slot=icon]:size-5.5 [&_[slot=description]]:hidden *:[span]:col-start-2"
data-slot="select-value"
/>
<ChevronDownIcon
aria-hidden
className="size-6 shrink-0 duration-200 group-open:rotate-180 forced-colors:text-[ButtonText] forced-colors:group-disabled:text-[GrayText]"
data-slot="icon"
size={24}
strokeWidth={1.5}
/>
</RACButton>
);
}
export {
DropdownSection as SelectSection,
DropdownSeparator as SelectSeparator,
DropdownLabel as SelectLabel,
DropdownItemDetails as SelectOptionDetails,
DropdownItem as SelectOption,
} from '@/components/ui/Dropdown';

View File

@@ -0,0 +1,148 @@
import type { VariantProps } from 'cva';
import type { ComponentProps } from 'react';
import type {
DialogProps as RACDialogProps,
DialogTriggerProps as RACDialogTriggerProps,
ModalOverlayProps as RACModalOverlayProps,
} from 'react-aria-components';
import {
DialogTrigger as RACDialogTrigger,
Modal as RACModal,
ModalOverlay as RACModalOverlay,
composeRenderProps,
} from 'react-aria-components';
import { Dialog, DialogCloseIndicator } from '@/components/ui/Dialog';
import { cva } from '@/styles/cva';
const overlayStyles = cva({
base: 'fixed top-0 left-0 isolate z-50 flex h-(--visual-viewport-height) w-full place-content-center place-items-center bg-neutral-900/15 p-4 dark:bg-neutral-900/40',
variants: {
isBlurred: {
true: 'supports-backdrop-filter:backdrop-blur',
},
isEntering: {
true: 'fade-in animate-in duration-300 ease-out',
},
isExiting: {
true: 'fade-out animate-out duration-200 ease-in',
},
},
});
type Sides = 'bottom' | 'left' | 'right' | 'top';
const generateCompoundVariants = (sides: Sides[]) =>
sides.map((side) => ({
side,
isFloat: true,
className:
side === 'top'
? 'top-2 inset-x-2 border-b-0'
: side === 'bottom'
? 'bottom-2 inset-x-2 border-t-0'
: side === 'left'
? 'left-2 inset-y-2 border-r-0'
: 'right-2 inset-y-2 border-l-0',
}));
const contentStyles = cva({
base: 'shadow-base-md border-base-neutral-200 dark:border-base-neutral-600 fixed z-50 grid gap-4 bg-neutral-100 transition ease-in-out dark:bg-neutral-900',
variants: {
isEntering: {
true: 'animate-in duration-300',
},
isExiting: {
true: 'animate-out duration-200',
},
side: {
top: 'entering:slide-in-from-top exiting:slide-out-to-top inset-x-0 top-0 border-b',
bottom: 'entering:slide-in-from-bottom exiting:slide-out-to-bottom inset-x-0 bottom-0 border-t',
left: 'entering:slide-in-from-left exiting:slide-out-to-left inset-y-0 left-0 h-auto w-full max-w-xs overflow-y-auto border-r',
right:
'entering:slide-in-from-right exiting:slide-out-to-right inset-y-0 right-0 h-auto w-full max-w-xs overflow-y-auto border-l',
},
isFloat: {
true: '',
false: '',
},
},
compoundVariants: generateCompoundVariants(['top', 'bottom', 'left', 'right']),
});
export type SheetProps = RACDialogTriggerProps;
export function Sheet(props: SheetProps) {
return <RACDialogTrigger {...props} />;
}
export type SheetContentProps = Omit<ComponentProps<typeof RACModal>, 'children' | 'className'> &
Omit<RACModalOverlayProps, 'className'> &
VariantProps<typeof overlayStyles> & {
readonly 'aria-label'?: RACDialogProps['aria-label'];
readonly 'aria-labelledby'?: RACDialogProps['aria-labelledby'];
readonly classNames?: {
content?: RACModalOverlayProps['className'];
overlay?: RACModalOverlayProps['className'];
};
readonly closeButton?: boolean;
readonly isBlurred?: boolean;
readonly isFloat?: boolean;
readonly role?: RACDialogProps['role'];
readonly side?: Sides;
};
export function SheetContent({
isBlurred = false,
isDismissable = true,
side = 'right',
role = 'dialog',
closeButton = true,
isFloat = true,
...props
}: SheetContentProps) {
const _isDismissable = role === 'alertdialog' ? false : isDismissable;
return (
<RACModalOverlay
{...props}
className={composeRenderProps(props.classNames?.overlay, (className, renderProps) =>
overlayStyles({
...renderProps,
isBlurred,
className,
}),
)}
isDismissable={_isDismissable}
>
<RACModal
{...props}
className={composeRenderProps(props.classNames?.content, (className, renderProps) =>
contentStyles({
...renderProps,
side,
isFloat,
className,
}),
)}
>
{(values) => (
<Dialog aria-label={props['aria-label'] ?? undefined!} className="h-full" role={role}>
<>
{typeof props.children === 'function' ? props.children(values) : props.children}
{closeButton && <DialogCloseIndicator className="top-2.5 right-2.5" isDismissable={_isDismissable} />}
</>
</Dialog>
)}
</RACModal>
</RACModalOverlay>
);
}
export {
DialogTrigger as SheetTrigger,
DialogBody as SheetBody,
DialogClose as SheetClose,
DialogDescription as SheetDescription,
DialogFooter as SheetFooter,
DialogHeader as SheetHeader,
DialogTitle as SheetTitle,
} from '@/components/ui/Dialog';

View File

@@ -0,0 +1,485 @@
'use client';
import { MenuIcon, SidebarIcon, XIcon } from 'lucide-react';
import {
createContext,
use,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useState,
type ComponentProps,
} from 'react';
import { chain } from 'react-aria';
import { useMediaQuery } from 'usehooks-ts';
import { Button, type ButtonProps } from '@/components/ui/Button';
import { SheetBody, SheetContent, type SheetContentProps } from '@/components/ui/Sheet';
import { cva, cx } from '@/styles/cva';
const SIDEBAR_COOKIE_NAME = 'sidebar:state';
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
interface SidebarContextProps {
isMobile: boolean;
open: boolean;
openMobile: boolean;
setOpen(open: boolean | ((open: boolean) => boolean)): void;
setOpenMobile(open: boolean | ((open: boolean) => boolean)): void;
state: 'collapsed' | 'expanded';
}
const SidebarContext = createContext<SidebarContextProps | null>(null);
export function useSidebar() {
const context = use(SidebarContext);
if (!context) {
throw new Error('useSidebar must be used within a Sidebar.');
}
return context;
}
export type SidebarProviderProps = ComponentProps<'div'> & {
readonly defaultOpen?: boolean;
readonly isOpen?: boolean;
onOpenChange?(open: boolean): void;
readonly shortcut?: string;
};
export function SidebarProvider({
defaultOpen = false,
isOpen: openProp,
onOpenChange: setOpenProp,
shortcut = 'b',
...props
}: SidebarProviderProps) {
const isMobile = useMediaQuery('(max-width: 767px)', { initializeWithValue: false });
const [openMobile, setOpenMobile] = useState(false);
const [internalOpenState, setInternalOpenState] = useState(defaultOpen);
const open = openProp ?? internalOpenState;
const setOpen = useCallback(
(value: boolean | ((value: boolean) => boolean)) => {
const openState = typeof value === 'function' ? value(open) : value;
if (isMobile) {
setOpenMobile((open) => !open);
} else if (setOpenProp) {
setOpenProp(openState);
} else {
setInternalOpenState(openState);
}
// eslint-disable-next-line react-compiler/react-compiler
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open, isMobile, setOpenMobile],
);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === shortcut && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
setOpen((open) => !open);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [shortcut, setOpen]);
const state = open ? 'expanded' : 'collapsed';
const contextValue = useMemo<SidebarContextProps>(
() => ({
state,
open,
setOpen,
openMobile,
setOpenMobile,
isMobile,
}),
[state, open, setOpen, openMobile, setOpenMobile, isMobile],
);
return (
<SidebarContext value={contextValue}>
<div
{...props}
className={cx(
'@container/sidebar **:data-[slot=icon]:shrink-0',
'[--sidebar-width-dock:3.25rem] [--sidebar-width-mobile:20rem] [--sidebar-width:20rem]',
'[--sidebar-border:var(--color-base-neutral-200)]',
'dark:[--sidebar-border:var(--color-base-neutral-600)]',
'flex min-h-dvh w-full',
'group/sidebar-root has-data-[sidebar-intent=inset]:bg-[#f3f3f4] dark:has-data-[sidebar-intent=inset]:bg-[#121214]',
'[@-moz-document_url-prefix()]:overflow-x-hidden',
props.className,
)}
>
{props.children}
</div>
</SidebarContext>
);
}
const sidebarGapStyles = cva({
base: [
'w-(--sidebar-width) group-data-[sidebar-collapsible=hidden]/sidebar-container:w-0',
'relative h-dvh bg-transparent transition-[width] duration-100 ease-linear',
'group-data-[sidebar-side=right]/sidebar-container:rotate-180',
],
variants: {
intent: {
default: 'group-data-[sidebar-collapsible=dock]/sidebar-container:w-(--sidebar-width-dock)',
fleet: 'group-data-[sidebar-collapsible=dock]/sidebar-container:w-(--sidebar-width-dock)',
float:
'group-data-[sidebar-collapsible=dock]/sidebar-container:w-[calc(var(--sidebar-width-dock)+theme(spacing.4))]',
inset:
'group-data-[sidebar-collapsible=dock]/sidebar-container:w-[calc(var(--sidebar-width-dock)+theme(spacing.2))]',
},
},
});
const sidebarStyles = cva({
base: [
'fixed inset-y-0 z-10 hidden h-dvh w-(--sidebar-width) transition-[left,right,width] duration-100 ease-linear md:flex',
'min-h-dvh bg-[#f3f3f4] dark:bg-[#121214]',
'**:data-[slot=disclosure]:border-0 **:data-[slot=disclosure]:px-2.5',
'has-data-[sidebar-intent=default]:shadow-base-md',
'[@-moz-document_url-prefix()]:h-full [@-moz-document_url-prefix()]:min-h-full',
],
variants: {
side: {
left: 'left-0 group-data-[sidebar-collapsible=hidden]/sidebar-container:left-[calc(var(--sidebar-width)*-1)]',
right: 'right-0 group-data-[sidebar-collapsible=hidden]/sidebar-container:right-[calc(var(--sidebar-width)*-1)]',
},
intent: {
default: [
'group-data-[sidebar-collapsible=dock]/sidebar-container:w-(--sidebar-width-dock) group-data-[sidebar-side=left]/sidebar-container:border-(--sidebar-border) group-data-[sidebar-side=right]/sidebar-container:border-(--sidebar-border)',
'group-data-[sidebar-side=left]/sidebar-container:border-r group-data-[sidebar-side=right]/sidebar-container:border-l',
],
fleet: [
'group-data-[sidebar-collapsible=dock]/sidebar-container:w-(--sidebar-width-dock)',
'**:data-sidebar-disclosure:gap-y-0 **:data-sidebar-disclosure:px-0 **:data-sidebar-section:gap-y-0 **:data-sidebar-section:px-0',
'group-data-[sidebar-side=left]/sidebar-container:border-r group-data-[sidebar-side=right]/sidebar-container:border-l',
],
float: 'bg-bg p-2 group-data-[sidebar-collapsible=dock]/sidebar-container:w-[calc(var+theme(spacing.4)+2px)]',
inset: [
'p-2 group-data-[sidebar-collapsible=dock]/sidebar-container:w-[calc(var(--sidebar-width-dock)+theme(spacing.2)+2px)]',
],
},
},
});
export type SidebarProps = ComponentProps<'div'> &
SheetContentProps & {
readonly closeButton?: boolean;
readonly collapsible?: 'dock' | 'hidden' | 'none';
readonly intent?: 'default' | 'fleet' | 'float' | 'inset';
readonly side?: 'left' | 'right';
};
export function Sidebar({
closeButton = true,
collapsible = 'hidden',
side = 'left',
intent = 'default',
...props
}: SidebarProps) {
const { isMobile, state, open, openMobile, setOpenMobile } = useSidebar();
const [needsScrollbarGutter, setNeedsScrollbarGutter] = useState(false);
useLayoutEffect(() => {
if (collapsible === 'none' || isMobile || side !== 'right') {
return;
}
const scrollbarVisible = (element: HTMLElement) => element.scrollHeight > element.clientHeight;
const observer = new MutationObserver((mutations) => {
if (mutations[0]?.type === 'attributes' && scrollbarVisible(document.documentElement) && open) {
if (getComputedStyle(document.documentElement).paddingRight === '0px') {
setNeedsScrollbarGutter(false);
} else {
setNeedsScrollbarGutter(true);
}
} else {
setNeedsScrollbarGutter(false);
}
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['style'],
});
return () => {
observer.disconnect();
};
}, [collapsible, isMobile, open, side]);
if (collapsible === 'none') {
return (
<div
{...props}
className={cx('flex h-full w-(--sidebar-width) flex-col border-r border-(--sidebar-border)', props.className)}
data-sidebar-collapsible="none"
data-sidebar-intent={intent}
/>
);
}
if (isMobile) {
return (
<SheetContent
{...props}
aria-label="Sidebar"
closeButton={closeButton}
data-sidebar-intent="default"
isFloat={intent === 'float'}
isOpen={openMobile}
onOpenChange={setOpenMobile}
side={side}
>
<SheetBody className="gap-0 p-0">{props.children}</SheetBody>
</SheetContent>
);
}
return (
<div
{...props}
className="group/sidebar-container peer hidden md:block"
data-sidebar-collapsible={state === 'collapsed' ? collapsible : ''}
data-sidebar-intent={intent}
data-sidebar-side={side}
data-sidebar-state={state}
>
<div aria-hidden className={sidebarGapStyles({ intent })} />
<div
{...props}
className={sidebarStyles({
side,
intent,
className: cx(props.className, needsScrollbarGutter && 'right-[11px]', 'transition-[left,width]'),
})}
>
<div
className={cx(
'flex h-full w-full flex-col',
'group-data-[sidebar-intent=inset]/sidebar-container:bg-sidebar dark:group-data-[sidebar-intent=inset]/sidebar-container:bg-bg',
'group-data-[sidebar-intent=float]/sidebar-container:bg-sidebar group-data-[sidebar-intent=float]/sidebar-container:shadow-base-md group-data-[sidebar-intent=float]/sidebar-container:rounded-lg group-data-[sidebar-intent=float]/sidebar-container:border group-data-[sidebar-intent=float]/sidebar-container:border-(--sidebar-border)',
)}
data-sidebar="default"
>
{props.children}
</div>
</div>
</div>
);
}
export function SidebarInsetAnchor({
collapsible = 'hidden',
side = 'left',
intent = 'default',
...props
}: SidebarProps) {
const { state } = useSidebar();
return (
<div
{...props}
className="group/sidebar-container peer hidden"
data-sidebar-collapsible={state === 'collapsed' ? collapsible : ''}
data-sidebar-intent={intent}
data-sidebar-side={side}
data-sidebar-state={state}
/>
);
}
const sidebarHeaderStyles = cva({
base: 'dark:bg-base-neutral-800 bg-base-neutral-0 flex flex-col **:data-[slot=sidebar-label-mask]:hidden',
variants: {
collapsed: {
true: 'mt-2 p-12 group-data-[sidebar-intent=float]/sidebar-container:mt-2 md:mx-auto md:size-9 md:items-center md:justify-center md:rounded-lg md:p-0 md:hover:bg-(--sidebar-accent)',
false: 'px-6 pt-8 pb-4',
},
hasBorder: {
true: 'border-base-neutral-100 dark:border-base-neutral-700 border-b',
false: null,
},
},
});
export type SidebarHeaderProps = ComponentProps<'div'> & {
readonly hasBorder?: boolean;
};
export function SidebarHeader({ hasBorder = false, ...props }: SidebarHeaderProps) {
const { state } = use(SidebarContext)!;
return (
<div
{...props}
className={sidebarHeaderStyles({ collapsed: state === 'collapsed', hasBorder, className: props.className })}
data-sidebar-header="true"
/>
);
}
const sidebarFooterStyles = cva({
base: [
'flex flex-col p-6',
'in-data-[sidebar-intent=fleet]:mt-0 in-data-[sidebar-intent=fleet]:p-0',
'in-data-[sidebar-intent=fleet]:**:data-[slot=menu-trigger]:rounded-none',
'**:data-[slot=menu-trigger]:relative **:data-[slot=menu-trigger]:overflow-hidden',
'**:data-[slot=menu-trigger]:rounded-lg',
'sm:**:data-[slot=menu-trigger]:text-base-md **:data-[slot=menu-trigger]:flex **:data-[slot=menu-trigger]:cursor-default **:data-[slot=menu-trigger]:items-center **:data-[slot=menu-trigger]:p-2 **:data-[slot=menu-trigger]:outline-hidden',
'**:data-[slot=menu-trigger]:hover:text-fg **:data-[slot=menu-trigger]:hover:bg-(--sidebar-accent)',
],
variants: {
collapsed: {
true: [
'**:data-[slot=avatar]:size-6 **:data-[slot=avatar]:*:size-6',
'**:data-[slot=chevron]:hidden **:data-[slot=menu-label]:hidden',
'**:data-[slot=menu-trigger]:grid **:data-[slot=menu-trigger]:size-8 **:data-[slot=menu-trigger]:place-content-center',
],
false: [
'**:data-[slot=avatar]:size-8 **:data-[slot=avatar]:*:size-8 **:data-[slot=menu-trigger]:**:data-[slot=avatar]:mr-2',
'**:data-[slot=menu-trigger]:pressed:**:data-[slot=chevron]:rotate-180 **:data-[slot=menu-trigger]:w-full **:data-[slot=menu-trigger]:**:data-[slot=chevron]:ml-auto **:data-[slot=menu-trigger]:**:data-[slot=chevron]:transition-transform',
],
},
hasBorder: {
true: 'border-base-neutral-100 dark:border-base-neutral-700 border-t',
false: null,
},
},
});
export type SidebarFooterProps = ComponentProps<'div'> & {
readonly hasBorder?: boolean;
};
export function SidebarFooter({ hasBorder = false, ...props }: SidebarFooterProps) {
const { state, isMobile } = useSidebar();
const collapsed = state === 'collapsed' && !isMobile;
return (
<div
{...props}
className={sidebarFooterStyles({ collapsed, hasBorder, className: props.className })}
data-sidebar-footer="true"
/>
);
}
export function SidebarContent(props: ComponentProps<'div'>) {
const { state } = useSidebar();
return (
<div
{...props}
className={cx(
'dark:bg-base-neutral-800 bg-base-neutral-0 flex min-h-0 flex-1 scroll-mb-96 flex-col overflow-auto p-6 *:data-sidebar-section:border-l-0',
state === 'collapsed' && 'place-items-center',
props.className,
)}
data-sidebar-content="true"
/>
);
}
export function SidebarInset(props: ComponentProps<'main'>) {
return (
<main
{...props}
className={cx(
'relative flex min-h-dvh w-full flex-1 flex-col peer-data-[sidebar-intent=inset]:border peer-data-[sidebar-intent=inset]:border-transparent',
'bg-bg dark:peer-data-[sidebar-intent=inset]:bg-sidebar peer-data-[sidebar-intent=inset]:overflow-hidden',
'md:peer-data-[sidebar-intent=inset]:shadow-base-md peer-data-[sidebar-intent=inset]:min-h-[calc(100dvh-theme(spacing.4))] md:peer-data-[sidebar-intent=inset]:m-2 md:peer-data-[sidebar-intent=inset]:rounded-xl md:peer-data-[sidebar-intent=inset]:peer-data-[sidebar-side=left]:ml-0 md:peer-data-[sidebar-intent=inset]:peer-data-[sidebar-side=right]:mr-0 md:peer-data-[sidebar-state=collapsed]:peer-data-[sidebar-intent=inset]:peer-data-[sidebar-side=left]:ml-2 md:peer-data-[sidebar-state=collapsed]:peer-data-[sidebar-intent=inset]:peer-data-[sidebar-side=right]:mr-2',
props.className,
)}
/>
);
}
export function SidebarTrigger({ onPress, children, ...props }: ButtonProps) {
const { setOpen } = useSidebar();
return (
<Button
{...props}
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
aria-label={props['aria-label'] || 'Toggle Sidebar'}
data-sidebar-trigger="true"
onPress={(event) => {
onPress?.(event);
setOpen((open) => !open);
}}
>
{/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */}
{children || (
<>
<SidebarIcon aria-hidden className="hidden md:inline" data-slot="icon" size={18} />
<MenuIcon aria-hidden className="inline md:hidden" data-slot="icon" size={18} />
<span className="sr-only">Toggle Sidebar</span>
</>
)}
</Button>
);
}
export type CloseButtonIndicatorProps = Omit<ButtonProps, 'children'> & {
readonly className?: string;
readonly isDismissable?: boolean | undefined;
};
export function SidebarCloseIndicator({ isDismissable = true, ...props }: CloseButtonIndicatorProps) {
const { setOpen } = useSidebar();
return (
<Button
{...props}
aria-label="Close"
className={cx(
'close text-base-neutral-500 hover:text-base-neutral-700 focus-visible:text-base-neutral-700 pressed:text-base-neutral-900 dark:text-base-neutral-400 dark:hover:text-base-neutral-200 dark:focus-visible:text-base-neutral-200 dark:pressed:text-base-neutral-500 disabled:text-base-neutral-300 dark:disabled:text-base-neutral-300 z-50 rounded-full',
props.className,
)}
onPress={isDismissable ? chain(() => setOpen((open) => !open), props.onPress) : props.onPress!}
size="icon-xs"
slot={props.slot === null ? null : (props.slot ?? 'close')}
variant="unset"
>
<XIcon aria-hidden className="size-4.5 stroke-[1.5]" />
</Button>
);
}
export function SidebarRail({ className, ref, ...props }: ComponentProps<'button'>) {
const { setOpen } = useSidebar();
return (
<button
aria-label="Toggle Sidebar"
className={cx(
'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 outline-hidden transition-all ease-linear group-data-[sidebar-side=left]/sidebar-container:-right-4 group-data-[sidebar-side=right]/sidebar-container:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] data-hovered:after:bg-transparent sm:flex',
'in-data-[sidebar-side=left]:cursor-w-resize in-data-[sidebar-side=right]:cursor-e-resize',
'[[data-sidebar-side=left][data-sidebar-state=collapsed]_&]:cursor-e-resize [[data-sidebar-side=right][data-sidebar-state=collapsed]_&]:cursor-w-resize',
'group-data-[sidebar-collapsible=hidden]/sidebar-container:hover:bg-secondary group-data-[sidebar-collapsible=hidden]/sidebar-container:translate-x-0 group-data-[sidebar-collapsible=hidden]/sidebar-container:after:left-full',
'[[data-sidebar-side=left][data-sidebar-collapsible=hidden]_&]:-right-2 [[data-sidebar-side=right][data-sidebar-collapsible=hidden]_&]:-left-2',
className,
)}
data-sidebar="rail"
onClick={() => setOpen((open) => !open)}
ref={ref}
tabIndex={-1}
title="Toggle Sidebar"
type="button"
{...props}
/>
);
}

View File

@@ -1,16 +0,0 @@
'use client';
import { VscColorMode } from '@react-icons/all-files/vsc/VscColorMode';
import { useTheme } from 'next-themes';
import { Button } from './Button';
export function ThemeSwitch() {
const { resolvedTheme, setTheme } = useTheme();
const toggleTheme = () => setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
return (
<Button aria-label="Toggle theme" className="rounded-full" onPress={() => toggleTheme()}>
<VscColorMode aria-hidden size={24} />
</Button>
);
}

View File

@@ -1,102 +0,0 @@
'use client';
import { ChevronsUpDown } from 'lucide-react';
import { useEffect, useState } from 'react';
import type { Key } from 'react-aria-components';
import { useMediaQuery } from 'usehooks-ts';
import { Drawer as Vaul } from 'vaul';
import { Button } from './Button';
import { ListBox, ListBoxItem } from './ListBox';
import { Popover } from './Popover';
import { Select, SelectValue } from './Select';
export function VersionSelect({
packageName,
version,
versions,
}: {
readonly packageName: string;
readonly version: string;
readonly versions: { readonly version: string }[];
}) {
const [selectedVersion, setSelectedVersion] = useState<Key>(version);
const [open, setOpen] = useState(false);
const isMedium = useMediaQuery('(min-width: 768px)');
useEffect(() => {
if (isMedium) {
setOpen(false);
}
}, [isMedium, setOpen]);
return (
<>
<Select
aria-label="Select a version"
className="hidden md:block"
onSelectionChange={(selected) => {
setSelectedVersion(selected);
}}
selectedKey={selectedVersion}
>
<Button className="flex w-full place-content-between place-items-center rounded-md bg-neutral-200 p-2 dark:bg-neutral-800">
<SelectValue className="font-medium" />
<ChevronsUpDown aria-hidden size={20} />
</Button>
<Popover className="max-h-60 w-[--trigger-width] overflow-auto rounded-md border border-neutral-300 bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800">
<ListBox items={versions} shouldFocusWrap>
{(item) => (
<ListBoxItem
className="flex p-2 outline-none data-[focus-visible]:bg-neutral-300 data-[hovered]:bg-neutral-300 data-[selected]:bg-blurple data-[selected]:data-[focus-visible]:bg-blurple-500 data-[selected]:data-[hovered]:bg-blurple-500 data-[selected]:text-white dark:data-[focus-visible]:bg-neutral-700 dark:data-[hovered]:bg-neutral-700 dark:data-[selected]:data-[focus-visible]:bg-blurple-500 dark:data-[selected]:data-[hovered]:bg-blurple-500"
href={`/docs/packages/${packageName}/${item.version}`}
id={item.version}
textValue={item.version}
>
{item.version}
</ListBoxItem>
)}
</ListBox>
</Popover>
</Select>
<Vaul.NestedRoot onOpenChange={setOpen} open={open}>
<Vaul.Trigger
aria-label="Open version select"
className="flex w-full place-content-between place-items-center rounded-md bg-neutral-200 p-2 dark:bg-neutral-800 md:hidden"
>
<span className="font-medium">{selectedVersion}</span>
<ChevronsUpDown aria-hidden size={20} />
</Vaul.Trigger>
<Vaul.Portal>
<Vaul.Overlay className="fixed inset-0 bg-black/40" />
<Vaul.Content className="fixed bottom-0 left-0 right-0 flex max-h-[80%] flex-col rounded-t-lg bg-neutral-100 p-4 dark:bg-neutral-900">
<div className="mx-auto mb-8 h-1.5 w-12 flex-shrink-0 rounded-full bg-neutral-400" />
<ListBox
aria-label="Select a version"
className="flex flex-col gap-2 overflow-auto"
items={versions}
onSelectionChange={(selected) => {
const [val] = selected;
setSelectedVersion(val as Key);
}}
selectedKeys={[selectedVersion]}
selectionMode="single"
shouldFocusWrap
>
{(item) => (
<ListBoxItem
className="rounded-md p-2 outline-none data-[focus-visible]:bg-neutral-300 data-[hovered]:bg-neutral-300 data-[selected]:bg-blurple data-[selected]:data-[focus-visible]:bg-blurple-500 data-[selected]:data-[hovered]:bg-blurple-500 data-[selected]:text-white dark:data-[focus-visible]:bg-neutral-700 dark:data-[hovered]:bg-neutral-700 dark:data-[selected]:data-[focus-visible]:bg-blurple-500 dark:data-[selected]:data-[hovered]:bg-blurple-500"
href={`/docs/packages/${packageName}/${item.version}`}
id={item.version}
textValue={item.version}
>
{item.version}
</ListBoxItem>
)}
</ListBox>
</Vaul.Content>
</Vaul.Portal>
</Vaul.NestedRoot>
</>
);
}

View File

@@ -0,0 +1,228 @@
@import 'tailwindcss';
@plugin 'tailwindcss-react-aria-components';
@import 'tw-animate-css';
@plugin '@tailwindcss/typography';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-base-neutral-0: #ffffff;
--color-base-neutral-40: #fcfcfc;
--color-base-neutral-60: #fafafa;
--color-base-neutral-80: #f0f0f0;
--color-base-neutral-100: #e6e6e6;
--color-base-neutral-200: #cccccc;
--color-base-neutral-300: #b3b3b3;
--color-base-neutral-400: #999999;
--color-base-neutral-500: #808080;
--color-base-neutral-600: #666666;
--color-base-neutral-700: #4c4c4c;
--color-base-neutral-800: #333333;
--color-base-neutral-900: #191919;
--color-base-blurple-50: #e0e3ff;
--color-base-blurple-100: #cdd2ff;
--color-base-blurple-200: #9ea7ff;
--color-base-blurple-300: #7782fa;
--color-base-blurple-400: #5865f2;
--color-base-blurple-500: #3d48c3;
--color-base-blurple-600: #293294;
--color-base-blurple-700: #1a2165;
--color-base-blurple-800: #0e1137;
--color-base-blurple-900: #020208;
--color-base-sunset-100: #ffe1df;
--color-base-sunset-200: #ffc3bf;
--color-base-sunset-300: #ffa69e;
--color-base-sunset-400: #ff887e;
--color-base-sunset-500: #ff6a5e;
--color-base-sunset-600: #cc554b;
--color-base-sunset-700: #994038;
--color-base-sunset-800: #662a26;
--color-base-sunset-900: #331513;
--color-base-tangerine-100: #fdefd2;
--color-base-tangerine-200: #fce0a5;
--color-base-tangerine-300: #fad078;
--color-base-tangerine-400: #f9c14b;
--color-base-tangerine-500: #f7b11e;
--color-base-tangerine-600: #c68e18;
--color-base-tangerine-700: #946a12;
--color-base-tangerine-800: #63470c;
--color-base-tangerine-900: #312306;
--color-base-green-lime-100: #ecefcc;
--color-base-green-lime-200: #d9df99;
--color-base-green-lime-300: #c6cf66;
--color-base-green-lime-400: #b3bf33;
--color-base-green-lime-500: #a0af00;
--color-base-green-lime-600: #808c00;
--color-base-green-lime-700: #606900;
--color-base-green-lime-800: #404600;
--color-base-green-lime-900: #202300;
--color-base-crystal-100: #dfedff;
--color-base-crystal-200: #bfdbff;
--color-base-crystal-300: #9ec8ff;
--color-base-crystal-400: #7eb6ff;
--color-base-crystal-500: #5ea4ff;
--color-base-crystal-600: #4b83cc;
--color-base-crystal-700: #386299;
--color-base-crystal-800: #264266;
--color-base-crystal-900: #132133;
--text-base-heading-xl: 2.813rem;
--text-base-heading-xl--line-height: 3.25rem;
--text-base-heading-xl--letter-spacing: 0;
--text-base-heading-xl--font-weight: 400;
--text-base-heading-lg: 2.25rem;
--text-base-heading-lg--line-height: 2.75rem;
--text-base-heading-lg--letter-spacing: 0;
--text-base-heading-lg--font-weight: 400;
--text-base-heading-md: 2rem;
--text-base-heading-md--line-height: 2.5rem;
--text-base-heading-md--letter-spacing: 0;
--text-base-heading-md--font-weight: 400;
--text-base-heading-sm: 1.75rem;
--text-base-heading-sm--line-height: 2.25rem;
--text-base-heading-sm--letter-spacing: 0;
--text-base-heading-sm--font-weight: 400;
--text-base-heading-xs: 1.5rem;
--text-base-heading-xs--line-height: 2rem;
--text-base-heading-xs--letter-spacing: 0;
--text-base-heading-xs--font-weight: 400;
--text-base-label-xl: 1.125rem;
--text-base-label-xl--line-height: 1.75rem;
--text-base-label-xl--letter-spacing: 0.5px;
--text-base-label-xl--font-weight: 500;
--text-base-label-lg: 1rem;
--text-base-label-lg--line-height: 1.5rem;
--text-base-label-lg--letter-spacing: 0.5px;
--text-base-label-lg--font-weight: 500;
--text-base-label-md: 1rem;
--text-base-label-md--line-height: 1.5rem;
--text-base-label-md--letter-spacing: 0.5px;
--text-base-label-md--font-weight: 500;
/* --text-base-label-md: 0.875rem;
--text-base-label-md--line-height: 1.25rem;
--text-base-label-md--letter-spacing: 0.1px;
--text-base-label-md--font-weight: 500; */
--text-base-label-sm: 0.75rem;
--text-base-label-sm--line-height: 1rem;
--text-base-label-sm--letter-spacing: 0.5px;
--text-base-label-sm--font-weight: 500;
--text-base-label-xs: 0.688rem;
--text-base-label-xs--line-height: 1rem;
--text-base-label-xs--letter-spacing: 0.5px;
--text-base-label-xs--font-weight: 500;
--text-base-xl: 1.125rem;
--text-base-xl--line-height: 1.75rem;
--text-base-xl--letter-spacing: 0.5px;
--text-base-xl--font-weight: 400;
--text-base-lg: 1rem;
--text-base-lg--line-height: 1.5rem;
--text-base-lg--letter-spacing: 0.5px;
--text-base-lg--font-weight: 400;
--text-base-md: 1rem;
--text-base-md--line-height: 1.5rem;
--text-base-md--letter-spacing: 0.5px;
--text-base-md--font-weight: 400;
/* --text-base-md: 0.875rem;
--text-base-md--line-height: 1.25rem;
--text-base-md--letter-spacing: 0.25px;
--text-base-md--font-weight: 400; */
--text-base-sm: 0.75rem;
--text-base-sm--line-height: 1rem;
--text-base-sm--letter-spacing: 0.4px;
--text-base-sm--font-weight: 400;
--text-base-xs: 0.688rem;
--text-base-xs--line-height: 1rem;
--text-base-xs--letter-spacing: 0.5px;
--text-base-xs--font-weight: 400;
--shadow-base-sm: 0 1px 4px 0 #19191929;
--shadow-base-md: 0 3px 3px 0 #19191929;
--shadow-base-lg: 0 3px 6px 0 #1919193d;
--shadow-base-xl: 0 6px 6px 0 #1919193d;
--shadow-base-2xl: 0 8px 8px 0 #19191952;
}
@layer base {
* {
font-family: var(--font-roboto);
text-rendering: optimizeLegibility;
scrollbar-width: thin;
}
html.dark .os-scrollbar-handle {
--os-handle-bg: rgba(255, 255, 255, 0.5);
--os-handle-bg-hover: rgba(255, 255, 255, 0.7);
--os-handle-bg-active: rgba(255, 255, 255, 0.7);
}
.os-scrollbar-handle {
--os-handle-bg: rgba(0, 0, 0, 0.5);
--os-handle-bg-hover: rgba(0, 0, 0, 0.7);
--os-handle-bg-active: rgba(0, 0, 0, 0.7);
}
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
pre {
@apply bg-[#f3f3f4]! dark:bg-[#121214]!;
}
code {
font-family: var(--font-geist-mono);
}
code > .line {
padding: 0 1rem;
}
}
@utility scrollbar-hidden {
scrollbar-width: none;
}
[cmdk-overlay] {
position: fixed;
inset: 0;
height: 100dvh;
width: 100vw;
@apply bg-base-neutral-900/72 z-30;
}
[cmdk-dialog] {
position: fixed;
left: 50%;
top: 0;
z-index: 50;
transform: translate(-50%, 0);
width: 100%;
max-width: 536px;
height: 100dvh;
@apply h-auto outline-0 md:top-16 md:p-4;
}
[cmdk-list-sizer] {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
}
[cmdk-group-heading] {
@apply text-base-label-sm text-base-neutral-600 dark:text-base-neutral-300 h-8 px-3 py-2;
}

View File

@@ -0,0 +1,34 @@
import { defineConfig } from 'cva';
import { extendTailwindMerge } from 'tailwind-merge';
const twMergeConfig = {
classGroups: {
'font-size': [
'text-base-xs',
'text-base-sm',
'text-base-md',
'text-base-lg',
'text-base-xl',
'text-base-label-xs',
'text-base-label-sm',
'text-base-label-md',
'text-base-label-lg',
'text-base-label-xl',
'text-base-heading-xs',
'text-base-heading-sm',
'text-base-heading-md',
'text-base-heading-lg',
'text-base-heading-xl',
],
},
};
const twMerge = extendTailwindMerge({
extend: twMergeConfig,
});
export const { cva, cx, compose } = defineConfig({
hooks: {
onComplete: (className) => twMerge(className),
},
});

View File

@@ -1,76 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
min-width: 0;
}
body {
font-family: var(--font-geist-sans);
min-height: 100dvh;
}
html.dark .os-scrollbar-handle {
--os-handle-bg: rgba(255, 255, 255, 0.5);
}
html.dark .os-scrollbar-handle:hover {
--os-handle-bg-hover: rgba(255, 255, 255, 0.7);
}
.os-scrollbar-handle {
--os-handle-bg: rgba(0, 0, 0, 0.5);
}
.os-scrollbar-handle:hover {
--os-handle-bg-hover: rgba(0, 0, 0, 0.7);
}
html.dark .shiki,
html.dark .shiki span {
color: var(--shiki-dark) !important;
/* background-color: var(--shiki-dark-bg) !important; */
background-color: transparent !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
pre {
background-color: transparent !important;
}
code {
font-family: var(--font-geist-mono);
}
code > .line {
padding: 0 1rem;
}
[cmdk-overlay] {
position: fixed;
inset: 0;
height: 100dvh;
width: 100vw;
background-color: rgb(0 0 0 / 80%);
}
[cmdk-dialog] {
position: fixed;
left: 50%;
top: 20%;
z-index: 50;
transform: translate(-50%, 0);
width: 100%;
max-width: 640px;
padding: 1rem;
}
[cmdk-list-sizer] {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
}

View File

@@ -0,0 +1,193 @@
import { compose, cva } from '@/styles/cva';
import { focusRing } from '@/styles/ui/focusRing';
export const buttonStyles = compose(
focusRing,
cva({
base: [
'text-base-label-md relative inline-flex place-content-center place-items-center gap-2 border border-transparent',
'*:data-[slot=icon]:size-4.5 *:data-[slot=icon]:shrink-0 print:hidden',
],
variants: {
variant: {
unset: null,
outline: [
'h-10 rounded-sm px-4 py-2.5',
'border-base-neutral-300 text-base-neutral-800 bg-base-neutral-0 dark:bg-base-neutral-800 dark:border-base-neutral-500 dark:text-base-neutral-100',
'hover:bg-base-neutral-700 hover:text-base-neutral-40 dark:hover:bg-base-neutral-100 dark:hover:text-base-neutral-900 hover:border-transparent',
'focus-visible:bg-base-neutral-700 focus-visible:text-base-neutral-40 dark:focus-visible:bg-base-neutral-100 dark:focus-visible:text-base-neutral-900 focus-visible:border-transparent',
'pressed:bg-base-neutral-800 pressed:text-base-neutral-40 pressed:border-transparent dark:pressed:bg-base-neutral-60 dark:pressed:text-base-neutral-900',
'disabled:bg-base-neutral-40 disabled:text-base-neutral-900 disabled:border-base-neutral-300 dark:disabled:bg-base-neutral-800 dark:disabled:text-base-neutral-40',
],
discreet: [
'h-10 rounded-sm px-4 py-2.5',
'text-base-neutral-800 dark:text-base-neutral-100 bg-transparent',
'hover:bg-base-neutral-100 dark:hover:bg-base-neutral-700',
'focus-visible:bg-base-neutral-100 dark:focus-visible:bg-base-neutral-700',
'pressed:bg-base-neutral-200 dark:pressed:bg-base-neutral-600',
'disabled:text-base-neutral-900 dark:disabled:text-base-neutral-40',
],
filled: [
'h-10 rounded-sm px-4 py-2.5',
'bg-base-neutral-700 text-base-neutral-40 dark:bg-base-neutral-100 dark:text-base-neutral-900',
'hover:bg-base-neutral-500 dark:hover:bg-base-neutral-300',
'focus-visible:bg-base-neutral-500 dark:focus-visible:bg-base-neutral-300',
'pressed:bg-base-neutral-400 pressed:text-base-neutral-800 dark:pressed:bg-base-neutral-400',
'disabled:bg-base-neutral-200 disabled:text-base-neutral-900 dark:disabled:bg-base-neutral-400 dark:disabled:text-base-neutral-40',
],
tonal: [
'h-10 rounded-sm px-4 py-2.5',
'bg-base-neutral-500 text-base-neutral-40 dark:bg-base-neutral-400 dark:text-base-neutral-900',
'hover:bg-base-neutral-700 dark:hover:bg-base-neutral-200',
'focus-visible:bg-base-neutral-700 dark:focus-visible:bg-base-neutral-200',
'pressed:bg-base-neutral-800 dark:pressed:bg-base-neutral-100',
'disabled:bg-base-neutral-200 disabled:text-base-neutral-900 dark:disabled:bg-base-neutral-700 dark:disabled:text-base-neutral-40',
],
'secondary-outline': [
'h-10 rounded-sm px-4 py-2.5',
'border-base-blurple-200 text-base-neutral-800 bg-base-neutral-0 dark:bg-base-neutral-800 dark:border-base-blurple-500 dark:text-base-neutral-40',
'hover:bg-base-blurple-400 hover:text-base-neutral-900 hover:border-transparent',
'focus-visible:bg-base-blurple-400 focus-visible:text-base-neutral-900 focus-visible:border-transparent',
'pressed:bg-base-blurple-500 pressed:text-base-neutral-40 pressed:border-transparent dark:pressed:bg-base-blurple-300 dark:pressed:text-base-neutral-900',
'disabled:bg-base-neutral-0 disabled:text-base-neutral-900 disabled:border-base-neutral-200 dark:text-base-neutral-40 dark:disabled:bg-base-neutral-800 dark:disabled:border-base-neutral-700',
],
'secondary-discreet': [
'h-10 rounded-sm px-4 py-2.5',
'text-base-blurple-500 dark:text-base-blurple-300 bg-transparent',
'hover:bg-base-blurple-50 hover:text-base-blurple-600 dark:hover:bg-base-blurple-700 dark:hover:text-base-blurple-200',
'focus-visible:bg-base-blurple-50 focus-visible:text-base-blurple-600 dark:focus-visible:bg-base-blurple-700 dark:focus-visible:text-base-blurple-200',
'pressed:bg-base-blurple-100 pressed:text-base-blurple-700 dark:pressed:bg-base-blurple-600 dark:pressed:text-base-blurple-50',
'disabled:text-base-neutral-900 dark:disabled:text-base-neutral-40',
],
'secondary-filled': [
'h-10 rounded-sm px-4 py-2.5',
'bg-base-blurple-400 text-base-neutral-900',
'hover:bg-base-blurple-200 dark:hover:bg-base-blurple-600 dark:hover:text-base-neutral-200',
'focus-visible:bg-base-blurple-200 dark:focus-visible:bg-base-blurple-600 dark:focus-visible:text-base-neutral-200',
'pressed:bg-base-blurple-100 dark:pressed:bg-base-blurple-700 dark:pressed:text-base-neutral-100',
'disabled:bg-base-neutral-200 disabled:text-base-neutral-900 dark:disabled:bg-base-neutral-700 dark:disabled:text-base-neutral-40',
],
'secondary-tonal': [
'h-10 rounded-sm px-4 py-2.5',
'bg-base-blurple-200 text-base-neutral-900 dark:bg-base-blurple-600 dark:text-base-neutral-40',
'hover:bg-base-blurple-400 dark:hover:text-base-neutral-900',
'focus-visible:bg-base-blurple-400 dark:focus-visible:text-base-neutral-900',
'pressed:bg-base-blurple-500 pressed:text-base-neutral-40 dark:pressed:bg-base-blurple-300 dark:pressed:text-base-neutral-900',
'disabled:bg-base-neutral-200 disabled:text-base-neutral-900 dark:disabled:bg-base-neutral-700 dark:disabled:text-base-neutral-40',
],
'crystal-tonal': [
'h-10 rounded-sm px-4 py-2.5',
'bg-base-crystal-300 text-base-neutral-900 dark:bg-base-crystal-700 dark:text-base-neutral-40',
'hover:bg-base-crystal-500 dark:hover:text-base-neutral-900',
'focus-visible:bg-base-crystal-500 dark:focus-visible:text-base-neutral-900',
'pressed:bg-base-crystal-600 pressed:text-base-neutral-40 dark:pressed:bg-base-crystal-400 dark:pressed:text-base-neutral-900',
'disabled:bg-base-neutral-200 disabled:text-base-neutral-900 dark:disabled:bg-base-neutral-700 dark:disabled:text-base-neutral-40',
],
tooltip: [
'size-6 shrink-0 rounded-full p-0.5',
'text-base-neutral-800 dark:text-base-neutral-100',
'hover:text-base-crystal-500 dark:hover:text-base-crystal-300',
'focus-visible:text-base-crystal-500 dark:focus-visible:text-base-crystal-300',
'disabled:text-base-neutral-900 dark:disabled:text-base-neutral-40',
],
},
size: {
default: null,
icon: null,
sm: null,
xs: null,
'icon-sm': null,
'icon-xs': null,
},
isDestructive: {
true: null,
},
isDark: {
true: null,
},
isDisabled: {
true: 'cursor-default opacity-38 forced-colors:text-[GrayText] forced-colors:group-disabled:text-[GrayText] forced-colors:disabled:text-[GrayText]',
false: 'cursor-pointer',
},
isPending: {
true: 'cursor-default',
},
},
compoundVariants: [
{
variant: 'discreet',
isDestructive: true,
className: [
'text-base-sunset-600 dark:text-base-sunset-400',
'hover:bg-base-sunset-100 hover:text-base-sunset-700 dark:hover:bg-base-sunset-800 dark:hover:text-base-sunset-300',
'focus-visible:bg-base-sunset-100 focus-visible:text-base-sunset-700 dark:focus-visible:bg-base-sunset-700 dark:focus-visible:text-base-sunset-300',
'pressed:bg-base-sunset-200 pressed:text-base-sunset-800 dark:pressed:bg-base-sunset-700 dark:pressed:text-base-sunset-100',
],
},
{
variant: [
'filled',
'outline',
'discreet',
'tonal',
'secondary-filled',
'secondary-outline',
'secondary-discreet',
'secondary-tonal',
'crystal-tonal',
'unset',
],
size: 'icon',
className: 'size-10 shrink-0 rounded-full p-2.5',
},
{
variant: ['filled', 'outline'],
size: 'sm',
className: 'h-8 px-3 py-1.5',
},
{
variant: 'filled',
size: 'sm',
isDark: true,
className: [
'h-8 px-3 py-1.5',
'bg-base-neutral-900 dark:bg-base-neutral-40',
'hover:bg-base-neutral-700 dark:hover:bg-base-neutral-200',
'focus-visible:bg-base-neutral-700 dark:focus-visible:bg-base-neutral-200',
'pressed:bg-base-neutral-600 pressed:text-base-neutral-40 dark:pressed:bg-base-neutral-300 dark:pressed:text-base-neutral-900',
],
},
{
variant: 'filled',
size: 'xs',
isDark: true,
className: [
'h-6 gap-1 px-2 py-1',
'bg-base-neutral-900 dark:bg-base-neutral-40',
'hover:bg-base-neutral-700 dark:hover:bg-base-neutral-200',
'focus-visible:bg-base-neutral-700 dark:focus-visible:bg-base-neutral-200',
'pressed:bg-base-neutral-600 pressed:text-base-neutral-40 dark:pressed:bg-base-neutral-300 dark:pressed:text-base-neutral-900',
],
},
{
variant: ['filled', 'discreet', 'secondary-tonal', 'crystal-tonal', 'unset'],
size: 'icon-sm',
className: 'size-8 shrink-0 rounded-full p-1.5',
},
{
variant: 'outline',
size: 'icon-sm',
className: 'size-8 shrink-0 rounded-sm p-1.5',
},
{
variant: ['discreet', 'unset'],
size: 'icon-xs',
className: 'size-6 shrink-0 rounded-full p-0.5',
},
],
defaultVariants: {
variant: 'outline',
size: 'default',
},
}),
);

View File

@@ -0,0 +1,11 @@
import { cva } from 'cva';
export const focusRing = cva({
base: 'outline-base-blurple-400 dark:outline-base-blurple-400 outline-offset-2 forced-colors:outline-[Highlight]',
variants: {
isFocusVisible: {
true: 'outline-2',
false: 'outline-0',
},
},
});

View File

@@ -0,0 +1,9 @@
import { composeRenderProps } from 'react-aria-components';
import { cx } from '@/styles/cva';
export function composeTailwindRenderProps<Type>(
className: string | ((v: Type) => string) | undefined,
tw: string,
): string | ((v: Type) => string) {
return composeRenderProps(className, (className) => cx(tw, className));
}

View File

@@ -27,10 +27,10 @@ export async function fetchDependencies({
} }
try { try {
const isMainVersion = version === 'main'; const isMain = version === 'main';
const fileContent = await fetch( const fileContent = await fetch(
`${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.dependencies.api.json`, `${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.dependencies.api.json`,
{ next: isMainVersion ? { revalidate: 0 } : { revalidate: 604_800 } }, { next: { revalidate: isMain ? 0 : 604_800 } },
); );
const parsedDependencies = await fileContent.json(); const parsedDependencies = await fileContent.json();

View File

@@ -25,10 +25,10 @@ export async function fetchNode({
return JSON.parse(fileContent); return JSON.parse(fileContent);
} }
const isMainVersion = version === 'main'; const isMain = version === 'main';
const fileContent = await fetch( const fileContent = await fetch(
`${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.${normalizeItem}.api.json`, `${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.${normalizeItem}.api.json`,
{ next: isMainVersion ? { revalidate: 0 } : { revalidate: 604_800 } }, { next: { revalidate: isMain ? 0 : 604_800 } },
); );
if (!fileContent.ok) { if (!fileContent.ok) {

View File

@@ -18,10 +18,12 @@ export async function fetchSitemap({
return JSON.parse(fileContent); return JSON.parse(fileContent);
} }
const isMainVersion = version === 'main'; const isMain = version === 'main';
const fileContent = await fetch( const fileContent = await fetch(
`${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.sitemap.api.json`, `${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.sitemap.api.json`,
{ next: isMainVersion ? { revalidate: 0 } : { revalidate: 604_800 } }, {
next: { revalidate: isMain ? 0 : 604_800 },
},
); );
if (!fileContent.ok) { if (!fileContent.ok) {

View File

@@ -0,0 +1,59 @@
/* Generate by @shikijs/codegen */
import type {
DynamicImportLanguageRegistration,
DynamicImportThemeRegistration,
HighlighterGeneric,
} from 'shiki/types';
import { createSingletonShorthands, createdBundledHighlighter } from 'shiki/core';
import { createJavaScriptRegexEngine } from 'shiki/engine-javascript.mjs';
type BundledLanguage = 'typescript' | 'ts' | 'javascript' | 'js' | 'shellscript' | 'bash' | 'sh' | 'shell' | 'zsh';
type BundledTheme = 'github-light' | 'github-dark-dimmed';
type Highlighter = HighlighterGeneric<BundledLanguage, BundledTheme>;
const bundledLanguages = {
typescript: () => import('shiki/langs/typescript.mjs'),
ts: () => import('shiki/langs/typescript.mjs'),
javascript: () => import('shiki/langs/javascript.mjs'),
js: () => import('shiki/langs/javascript.mjs'),
shellscript: () => import('shiki/langs/shellscript.mjs'),
bash: () => import('shiki/langs/shellscript.mjs'),
sh: () => import('shiki/langs/shellscript.mjs'),
shell: () => import('shiki/langs/shellscript.mjs'),
zsh: () => import('shiki/langs/shellscript.mjs'),
} as Record<BundledLanguage, DynamicImportLanguageRegistration>;
const bundledThemes = {
'github-light': () => import('shiki/themes/github-light.mjs'),
'github-dark-dimmed': () => import('shiki/themes/github-dark-dimmed.mjs'),
} as Record<BundledTheme, DynamicImportThemeRegistration>;
const createHighlighter = /* @__PURE__ */ createdBundledHighlighter<BundledLanguage, BundledTheme>({
langs: bundledLanguages,
themes: bundledThemes,
engine: () => createJavaScriptRegexEngine(),
});
const {
codeToHtml,
codeToHast,
codeToTokensBase,
codeToTokens,
codeToTokensWithThemes,
getSingletonHighlighter,
getLastGrammarState,
} = /* @__PURE__ */ createSingletonShorthands<BundledLanguage, BundledTheme>(createHighlighter);
export {
bundledLanguages,
bundledThemes,
codeToHast,
codeToHtml,
codeToTokens,
codeToTokensBase,
codeToTokensWithThemes,
createHighlighter,
getLastGrammarState,
getSingletonHighlighter,
};
export type { BundledLanguage, BundledTheme, Highlighter };

View File

@@ -1,100 +0,0 @@
import typographyPlugin from '@tailwindcss/typography';
/** @type {import('tailwindcss').Config} */
export default {
content: ['./src/**/*.{js,ts,jsx,tsx}'],
darkMode: 'class',
theme: {
extend: {
colors: {
blurple: {
50: '#e0e3ff',
100: '#cdd2ff',
200: '#9ea7ff',
300: '#7782fa',
DEFAULT: '#5865F2',
500: '#3d48c3',
600: '#293294',
700: '#1a2165',
800: '#0e1137',
900: '#020208',
},
},
fontFamily: {
sans: 'var(--font-geist-sans)',
mono: 'var(--font-geist-mono)',
},
typography: {
DEFAULT: {
css: {
pre: {
padding: '12px 0px',
'line-height': '1.5',
'border-radius': '6px',
'border-width': '1px',
'border-color': 'rgb(212, 212, 212)',
},
'.dark pre': {
'border-color': 'rgb(64, 64, 64)',
},
code: {
'font-size': '1em',
'font-weight': 'unset',
},
'code span:last-of-type:empty': {
display: 'none',
},
a: {
color: '#5865F2',
'text-decoration': 'none',
},
'a:hover': {
color: '#3d48c3',
},
'.dark a:hover': {
color: '#7782fa',
},
'a > img': {
display: 'inline-block',
margin: '0',
},
'a > img[height="44"]': {
height: '44px',
},
'div[align="center"] > p > a + a': {
'margin-left': '0.5em',
},
h1: {
display: 'flex',
'place-items': 'center',
'scroll-margin-top': '6.5rem',
},
h2: {
display: 'flex',
'place-items': 'center',
'margin-top': '1.25em',
'scroll-margin-top': '6.5rem',
},
h3: {
display: 'flex',
'place-items': 'center',
'margin-top': '1.25em',
'scroll-margin-top': '6.5rem',
},
h4: {
display: 'flex',
'place-items': 'center',
'margin-top': '1.25em',
'scroll-margin-top': '6.5rem',
},
// eslint-disable-next-line id-length
p: {
margin: '.5em 0',
},
},
},
},
},
},
plugins: [typographyPlugin],
};

View File

@@ -26,7 +26,7 @@
], ],
"paths": { "paths": {
"~/*": ["./src/*"] "@/*": ["./src/*"]
} }
}, },
"include": [ "include": [

View File

@@ -87,6 +87,7 @@ export default tseslint.config(
'**/coverage/', '**/coverage/',
'**/storybook-static/', '**/storybook-static/',
'**/.next/', '**/.next/',
'**/shiki.bundle.ts',
'packages/discord.js/', 'packages/discord.js/',
], ],
}, },

View File

@@ -75,7 +75,6 @@ export enum Meaning {
* *
* @public * @public
*/ */
export interface IApiItemOptions {} export interface IApiItemOptions {}
export interface IApiItemJson { export interface IApiItemJson {

View File

@@ -77,7 +77,6 @@ export class ApiModel extends ApiItemContainerMixin(ApiItem) {
/** /**
* @override * @override
*/ */
public override get containerKey(): string { public override get containerKey(): string {
return ''; return '';
} }

View File

@@ -1,5 +1,5 @@
diff --git a/lib/TSDocConfigFile.js b/lib/TSDocConfigFile.js diff --git a/lib/TSDocConfigFile.js b/lib/TSDocConfigFile.js
index caf3515d60fd386c5909db5a0aa8b4180b10d602..f8a7ff05d972e8dca4c5a485feb6ef37388274ac 100644 index caf3515d60fd386c5909db5a0aa8b4180b10d602..5f7cfed7611e3fe660b5265ff99c5da0beb7caec 100644
--- a/lib/TSDocConfigFile.js --- a/lib/TSDocConfigFile.js
+++ b/lib/TSDocConfigFile.js +++ b/lib/TSDocConfigFile.js
@@ -31,8 +31,7 @@ const ajv_1 = __importDefault(require("ajv")); @@ -31,8 +31,7 @@ const ajv_1 = __importDefault(require("ajv"));

4907
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ onlyBuiltDependencies:
- '@discordjs/opus' - '@discordjs/opus'
- bufferutil - bufferutil
- esbuild - esbuild
- msw
- protobufjs - protobufjs
- sharp - sharp
- utf-8-validate - utf-8-validate
@@ -16,9 +17,6 @@ packages:
- apps/* - apps/*
- packages/* - packages/*
patchedDependencies:
'@microsoft/tsdoc-config': patches/@microsoft__tsdoc-config.patch
peerDependencyRules: peerDependencyRules:
ignoreMissing: ignoreMissing:
- '*' - '*'
@@ -30,3 +28,6 @@ publicHoistPattern:
- '*prettier*' - '*prettier*'
- '*@rushstack/node-core-library*' - '*@rushstack/node-core-library*'
- '*jju*' - '*jju*'
patchedDependencies:
'@microsoft/tsdoc-config@0.16.2': patches/@microsoft__tsdoc-config@0.16.2.patch

View File

@@ -44,7 +44,22 @@
"outputLogs": "full" "outputLogs": "full"
}, },
"@discordjs/website#build:local": { "@discordjs/website#build:local": {
"env": ["VERCEL_ENV", "NEXT_PUBLIC_LOCAL_DEV"], "env": [
"VERCEL_ENV",
"NEXT_PUBLIC_LOCAL_DEV",
"EDGE_CONFIG",
"DATABASE_URL",
"MAX_FETCH_SIZE",
"POSTGRES_URL",
"POSTGRES_URL_NON_POOLING",
"POSTGRES_PRISMA_URL",
"POSTGRES_USER",
"POSTGRES_PASSWORD",
"POSTGRES_HOST",
"POSTGRES_DATABASE",
"BLOB_READ_WRITE_TOKEN",
"BLOB_STORAGE_URL"
],
"dependsOn": ["^build", "^docs"], "dependsOn": ["^build", "^docs"],
"inputs": [ "inputs": [
"../../packages/*/README.md", "../../packages/*/README.md",
@@ -61,7 +76,22 @@
"outputLogs": "full" "outputLogs": "full"
}, },
"@discordjs/website#build:prod": { "@discordjs/website#build:prod": {
"env": ["VERCEL_ENV", "NEXT_PUBLIC_LOCAL_DEV"], "env": [
"VERCEL_ENV",
"NEXT_PUBLIC_LOCAL_DEV",
"EDGE_CONFIG",
"DATABASE_URL",
"MAX_FETCH_SIZE",
"POSTGRES_URL",
"POSTGRES_URL_NON_POOLING",
"POSTGRES_PRISMA_URL",
"POSTGRES_USER",
"POSTGRES_PASSWORD",
"POSTGRES_HOST",
"POSTGRES_DATABASE",
"BLOB_READ_WRITE_TOKEN",
"BLOB_STORAGE_URL"
],
"dependsOn": ["^build"], "dependsOn": ["^build"],
"inputs": [ "inputs": [
"../../packages/*/README.md", "../../packages/*/README.md",