diff --git a/packages/docs/next.config.mjs b/packages/docs/next.config.mjs index 261b223c8..7fcc87d51 100644 --- a/packages/docs/next.config.mjs +++ b/packages/docs/next.config.mjs @@ -26,12 +26,18 @@ const config = { }, { protocol: 'https', - hostname: 'avatars.githubusercontent.com' + hostname: 'avatars.githubusercontent.com', + pathname: '/u/**' }, { protocol: 'https', hostname: 'media.licdn.com', pathname: '/dms/image/**' + }, + { + protocol: 'https', + hostname: 'i.redd.it', + pathname: '/snoovatar/avatars/**' } ] } diff --git a/packages/docs/package.json b/packages/docs/package.json index 2b86a9efb..3106f4431 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -9,16 +9,24 @@ "start": "next start" }, "dependencies": { + "@faker-js/faker": "^8.4.0", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-toggle": "^1.0.3", + "@radix-ui/react-toggle-group": "^1.0.4", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "dayjs": "^1.11.10", + "execa": "^8.0.1", "lucide-react": "^0.298.0", "next": "14.0.4", "next-docs-mdx": "^6.0.2", "next-docs-ui": "^6.0.2", "next-docs-zeta": "^6.0.2", "nuqs": "workspace:*", + "pretty-bytes": "^6.1.1", "react": "^18.2.0", "react-dom": "^18.2.0", "recharts": "^2.10.3", diff --git a/packages/docs/src/app/(pages)/_landing/bundle-size.tsx b/packages/docs/src/app/(pages)/_landing/bundle-size.tsx new file mode 100644 index 000000000..b95280eb3 --- /dev/null +++ b/packages/docs/src/app/(pages)/_landing/bundle-size.tsx @@ -0,0 +1,11 @@ +import { execa } from 'execa' +import path from 'node:path' +import prettyBytes from 'pretty-bytes' + +export async function BundleSize() { + const { stdout } = await execa('./node_modules/.bin/size-limit', ['--json'], { + cwd: path.resolve(process.cwd(), '../../packages/nuqs') + }) + const [{ size }] = JSON.parse(stdout) + return prettyBytes(size) +} diff --git a/packages/docs/src/app/(pages)/_landing/demo.tsx b/packages/docs/src/app/(pages)/_landing/demo.tsx index 685601d09..297d0b44e 100644 --- a/packages/docs/src/app/(pages)/_landing/demo.tsx +++ b/packages/docs/src/app/(pages)/_landing/demo.tsx @@ -1,8 +1,8 @@ +import { CodeBlock } from '@/src/components/code-block' import fs from 'node:fs/promises' import path from 'node:path' import { fileURLToPath } from 'node:url' import { Suspense } from 'react' -import { codeToHtml } from 'shikiji' import { Demo } from './demo.client' export async function LandingDemo() { @@ -11,42 +11,14 @@ export async function LandingDemo() { './demo.client.tsx' ) const demoFile = await fs.readFile(demoFilePath, 'utf8') - const demoCode = await codeToHtml( - demoFile - .split('\n') - .filter( - line => - !line.includes('className="') && !line.includes('data-interacted=') - ) - .join('\n') - .replaceAll('next-usequerystate', 'nuqs'), - { - lang: 'tsx', - themes: { - dark: 'github-dark', - light: 'github-light' - }, - transformers: [ - { - name: 'transparent background', - pre(node) { - if (typeof node.properties.style !== 'string') { - return node - } - node.properties.style = node.properties.style - .split(';') - .filter(style => !style.includes('-bg:')) - .concat([ - '--shiki-dark-bg:transparent', - '--shiki-light-bg:transparent' - ]) - .join(';') - return node - } - } - ] - } - ) + const demoCode = demoFile + .split('\n') + .filter( + line => + !line.includes('className="') && !line.includes('data-interacted=') + ) + .join('\n') + .replaceAll('next-usequerystate', 'nuqs') return ( <> -
+ ) } diff --git a/packages/docs/src/app/(pages)/_landing/features.tsx b/packages/docs/src/app/(pages)/_landing/features.tsx index 729e587c4..881b1d616 100644 --- a/packages/docs/src/app/(pages)/_landing/features.tsx +++ b/packages/docs/src/app/(pages)/_landing/features.tsx @@ -13,6 +13,7 @@ import { TestTube2 } from 'lucide-react' import React from 'react' +import { BundleSize } from './bundle-size' export function FeaturesSection(props: React.ComponentProps<'section'>) { return ( @@ -101,7 +102,11 @@ export function FeaturesSection(props: React.ComponentProps<'section'>) { } title="Tiny" - description="Only 3.9kb gzipped." + description={ + <> + Only gzipped. + + } /> } diff --git a/packages/docs/src/app/(pages)/_landing/quotes/quotes-section.tsx b/packages/docs/src/app/(pages)/_landing/quotes/quotes-section.tsx index a5466c5b4..290e9459c 100644 --- a/packages/docs/src/app/(pages)/_landing/quotes/quotes-section.tsx +++ b/packages/docs/src/app/(pages)/_landing/quotes/quotes-section.tsx @@ -18,7 +18,7 @@ export function QuotesSection() { } author={{ name: 'N8', - handle: 'nathanbrachotte', + handle: '@nathanbrachotte', avatar: 'https://pbs.twimg.com/profile_images/1589918605977722882/Iu7GZSZ9_400x400.jpg' }} @@ -28,7 +28,7 @@ export function QuotesSection() { text="The DX improvement using nuqs for me has been amazing." author={{ name: 'Kingsley O.', - handle: 'Kingsley_codes', + handle: '@Kingsley_codes', avatar: 'https://pbs.twimg.com/profile_images/1679549288689352704/RqDBl9w1_400x400.jpg' }} @@ -48,6 +48,34 @@ export function QuotesSection() { } /> + + + ) } diff --git a/packages/docs/src/app/(pages)/playground/_components/playground-page-layout.tsx b/packages/docs/src/app/(pages)/playground/_components/playground-page-layout.tsx deleted file mode 100644 index 5f71cabbc..000000000 --- a/packages/docs/src/app/(pages)/playground/_components/playground-page-layout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import Link from 'next/link' -import { QuerySpy } from './query-spy' - -export function PlaygroundPageLayout({ - children -}: { - children: React.ReactNode -}) { - return ( -
- ⬅️ Home - - {children} -
- ) -} diff --git a/packages/docs/src/app/(pages)/playground/basic-counter/page.tsx b/packages/docs/src/app/(pages)/playground/basic-counter/page.tsx deleted file mode 100644 index c95572abb..000000000 --- a/packages/docs/src/app/(pages)/playground/basic-counter/page.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client' - -import { parseAsInteger, useQueryState } from 'nuqs' - -export default function BasicCounterDemoPage() { - const [counter, setCounter] = useQueryState( - 'counter', - parseAsInteger.withDefault(0) - ) - - return ( - <> -

Basic counter

-

- State is stored in the URL query string -

- -

Counter: {counter}

-

- - Source on GitHub - -

- - ) -} diff --git a/packages/docs/src/app/(pages)/playground/layout.tsx b/packages/docs/src/app/(pages)/playground/layout.tsx deleted file mode 100644 index df0dc100c..000000000 --- a/packages/docs/src/app/(pages)/playground/layout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import dynamic from 'next/dynamic' -import React, { Suspense } from 'react' -import { PlaygroundPageLayout } from './_components/playground-page-layout' - -export const metadata = { - title: 'Playground' -} - -const DebugControlsSkeleton = () => ( - - - - -) - -const DebugControl = dynamic(() => import('./_components/debug-control'), { - ssr: false, - loading: DebugControlsSkeleton -}) - -export default function PlaygroundLayout({ - children -}: { - children: React.ReactNode -}) { - return ( - <> -
- }> - - -
-
- {children} - - ) -} diff --git a/packages/docs/src/app/(pages)/playground/page.tsx b/packages/docs/src/app/(pages)/playground/page.tsx deleted file mode 100644 index 68d5726f7..000000000 --- a/packages/docs/src/app/(pages)/playground/page.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import Link from 'next/link' -import fs from 'node:fs/promises' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -export default async function PlaygroundIndexPage() { - const appRouterLinks = await getDemoLinks() - return ( - <> -

Playground

-

Demos

-

App router

-
    - {appRouterLinks.map(link => ( -
  • - {link.split('/').at(-1)} -
  • - ))} -
-

Pages router

-
    -
  • - - Server-side counter (with gSSP) - -
  • -
-
- - - ) -} - -async function getDemoLinks() { - const filePath = fileURLToPath(import.meta.url) - const dirname = path.dirname(filePath) - const demos = await fs.readdir(dirname) - return demos - .filter(dir => !dir.startsWith('_')) - .map(dir => `/playground/${dir}`) -} diff --git a/packages/docs/src/app/docs/[[...slug]]/page.tsx b/packages/docs/src/app/docs/[[...slug]]/page.tsx index 62871cad0..4010ea8cf 100644 --- a/packages/docs/src/app/docs/[[...slug]]/page.tsx +++ b/packages/docs/src/app/docs/[[...slug]]/page.tsx @@ -1,4 +1,5 @@ import { getPage, pages } from '@/src/app/source' +import { Description, H1 } from '@/src/components/typography' import type { Metadata } from 'next' import { DocsBody, DocsPage } from 'next-docs-ui/page' import { notFound } from 'next/navigation' @@ -20,12 +21,8 @@ export default async function Page({
-

- {page.matter.title} -

-

- {page.matter.description} -

+

{page.matter.title}

+ {page.matter.description}
diff --git a/packages/docs/src/app/layout.tsx b/packages/docs/src/app/layout.tsx index 004eec450..1c12f46aa 100644 --- a/packages/docs/src/app/layout.tsx +++ b/packages/docs/src/app/layout.tsx @@ -39,6 +39,7 @@ export default function Layout({ children }: { children: ReactNode }) { data-chiffre-public-key="pk.3EPMj_faODyzisb0UNmZnzhIkG9sbj7zR5em6lf7Olk" referrerPolicy="origin" crossOrigin="anonymous" + data-chiffre-ignore-paths="/stats" /> )} diff --git a/packages/docs/src/app/playground/(demos)/_components/query-spy.skeleton.tsx b/packages/docs/src/app/playground/(demos)/_components/query-spy.skeleton.tsx new file mode 100644 index 000000000..bd642bf06 --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/_components/query-spy.skeleton.tsx @@ -0,0 +1,21 @@ +import { twMerge } from 'tailwind-merge' + +export function QuerySpySkeleton({ + children, + className, + ...props +}: React.ComponentProps<'pre'>) { + return ( +
+      {children}
+    
+ ) +} diff --git a/packages/docs/src/app/(pages)/playground/_components/query-spy.tsx b/packages/docs/src/app/playground/(demos)/_components/query-spy.tsx similarity index 50% rename from packages/docs/src/app/(pages)/playground/_components/query-spy.tsx rename to packages/docs/src/app/playground/(demos)/_components/query-spy.tsx index fd4c15bc5..6e7fda3f8 100644 --- a/packages/docs/src/app/(pages)/playground/_components/query-spy.tsx +++ b/packages/docs/src/app/playground/(demos)/_components/query-spy.tsx @@ -3,6 +3,7 @@ import { useSearchParams } from 'next/navigation' import { subscribeToQueryUpdates } from 'nuqs' import React from 'react' +import { QuerySpySkeleton } from './query-spy.skeleton' export const QuerySpy: React.FC = () => { const initialSearchParams = useSearchParams() @@ -26,21 +27,26 @@ export const QuerySpy: React.FC = () => { () => subscribeToQueryUpdates(({ search }) => setSearch(search)), [] ) - const qs = search.toString() return ( -
-      {Boolean(qs) && <>?{qs}}
-      {!qs && {''}}
-    
+ + {search.size > 0 && ( + + ? + {Array.from(search.entries()).map(([key, value], i) => ( + + {key}= + + {value} + + & + + ))} + + )} + {search.size === 0 && ( + {''} + )} + ) } diff --git a/packages/docs/src/app/playground/(demos)/_components/source-on-github.tsx b/packages/docs/src/app/playground/(demos)/_components/source-on-github.tsx new file mode 100644 index 000000000..75623c5e0 --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/_components/source-on-github.tsx @@ -0,0 +1,35 @@ +import { CodeBlock } from '@/src/components/code-block' +import { FileCode2 } from 'lucide-react' +import fs from 'node:fs/promises' + +type SourceOnGitHubProps = { + path: string +} + +export async function SourceOnGitHub({ path }: SourceOnGitHubProps) { + const source = await readSourceCode(path) + return ( + + ) +} + +function readSourceCode(demoPath: string) { + const demoFilePath = process.cwd() + '/src/app/playground/(demos)/' + demoPath + return fs.readFile(demoFilePath, 'utf8') +} diff --git a/packages/docs/src/app/playground/(demos)/basic-counter/client.tsx b/packages/docs/src/app/playground/(demos)/basic-counter/client.tsx new file mode 100644 index 000000000..ca7e324a1 --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/basic-counter/client.tsx @@ -0,0 +1,28 @@ +'use client' + +import { Button } from '@/src/components/ui/button' +import { Minus, Plus } from 'lucide-react' +import { parseAsInteger, useQueryState } from 'nuqs' + +export default function BasicCounterDemoPage() { + const [counter, setCounter] = useQueryState( + 'counter', + parseAsInteger.withDefault(0) + ) + return ( + <> + + + ) +} diff --git a/packages/docs/src/app/playground/(demos)/basic-counter/page.tsx b/packages/docs/src/app/playground/(demos)/basic-counter/page.tsx new file mode 100644 index 000000000..2333c893e --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/basic-counter/page.tsx @@ -0,0 +1,22 @@ +import { Description, H1 } from '@/src/components/typography' +import { Suspense } from 'react' +import { SourceOnGitHub } from '../_components/source-on-github' +import { getMetadata } from '../demos' +import Client from './client' + +export const metadata = getMetadata('basic-counter') + +export default function BasicCounterDemoPage() { + return ( + <> +

+ {metadata.title} +

+ {metadata.description} + + + + + + ) +} diff --git a/packages/docs/src/app/(pages)/playground/batching/page.tsx b/packages/docs/src/app/playground/(demos)/batching/client.tsx similarity index 54% rename from packages/docs/src/app/(pages)/playground/batching/page.tsx rename to packages/docs/src/app/playground/(demos)/batching/client.tsx index d8ac02e0a..520b22a0b 100644 --- a/packages/docs/src/app/(pages)/playground/batching/page.tsx +++ b/packages/docs/src/app/playground/(demos)/batching/client.tsx @@ -1,5 +1,6 @@ 'use client' +import { Button } from '@/src/components/ui/button' import { parseAsFloat, useQueryState } from 'nuqs' const parser = parseAsFloat.withDefault(0) @@ -7,39 +8,25 @@ const parser = parseAsFloat.withDefault(0) export default function BuilderPatternDemoPage() { const [lat, setLat] = useQueryState('lat', parser) const [lng, setLng] = useQueryState('lng', parser) - - const latStr = lat.toString().split('.') - const lngStr = lng.toString().split('.') - return ( <> -

Batching

- -
-        
-          {/* Aligning the decimals */}
-          Lat {latStr[0].padStart(4) + '.' + (latStr[1] ?? '00')}
-          {'\n'}
-          Lng {lngStr[0].padStart(4) + '.' + (lngStr[1] ?? '00')}
-        
-      
-

- - Source on GitHub - -

+ Random coordinates + +
    +
  • Latitude: {lat}
  • +
  • Longitude: {lng}
  • +
) } diff --git a/packages/docs/src/app/playground/(demos)/batching/page.tsx b/packages/docs/src/app/playground/(demos)/batching/page.tsx new file mode 100644 index 000000000..c8b4ef602 --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/batching/page.tsx @@ -0,0 +1,20 @@ +import { Description } from '@/src/components/typography' +import { Suspense } from 'react' +import { SourceOnGitHub } from '../_components/source-on-github' +import { getMetadata } from '../demos' +import Client from './client' + +export const metadata = getMetadata('batching') + +export default function BuilderPatternDemoPage() { + return ( + <> +

{metadata.title}

+ {metadata.description} + + + + + + ) +} diff --git a/packages/docs/src/app/playground/(demos)/demos.ts b/packages/docs/src/app/playground/(demos)/demos.ts new file mode 100644 index 000000000..f8fc826ac --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/demos.ts @@ -0,0 +1,44 @@ +import type { PageTree } from 'next-docs-zeta/server' + +type DemoMetadata = { + title: string + description: string +} + +export const demos = { + 'basic-counter': { + title: 'Basic counter', + description: 'State is stored in the URL query string' + }, + batching: { + title: 'Batching', + description: + 'State updates are collected and batched into one update on the next tick.' + }, + 'hex-colors': { + title: 'Hex colors', + description: 'Parsing RGB values from a hex color' + }, + pagination: { + title: 'Pagination', + description: 'Integer page index with server-side rendering' + } +} as const satisfies Record + +export function getMetadata(path: keyof typeof demos): DemoMetadata { + return demos[path] +} + +// -- + +export function getPlaygroundTree(): PageTree { + return { + name: 'Playground', + children: Object.entries(demos).map(([path, { title, description }]) => ({ + type: 'page', + name: title, + description, + url: `/playground/${path}` + })) + } +} diff --git a/packages/docs/src/app/(pages)/playground/hex-colors/page.tsx b/packages/docs/src/app/playground/(demos)/hex-colors/client.tsx similarity index 84% rename from packages/docs/src/app/(pages)/playground/hex-colors/page.tsx rename to packages/docs/src/app/playground/(demos)/hex-colors/client.tsx index 89d5d42c0..3a8c36713 100644 --- a/packages/docs/src/app/(pages)/playground/hex-colors/page.tsx +++ b/packages/docs/src/app/playground/(demos)/hex-colors/client.tsx @@ -33,10 +33,15 @@ export default function HexColorsDemo() { ) const asHex = '#' + hexColorSchema.serialize(color) return ( - <> -

Hex colors

-
-
+
+
+
+
-
-

- - Source on GitHub - -

- +
) } @@ -112,15 +105,7 @@ const ColorSlider = ({ accentColor }: ColorSliderProps) => { return ( -
+
+

{metadata.title}

+ {metadata.description} + + + + + + ) +} diff --git a/packages/docs/src/app/playground/(demos)/layout.tsx b/packages/docs/src/app/playground/(demos)/layout.tsx new file mode 100644 index 000000000..fe36d797d --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/layout.tsx @@ -0,0 +1,18 @@ +import React, { Suspense } from 'react' +import { QuerySpy } from './_components/query-spy' +import { QuerySpySkeleton } from './_components/query-spy.skeleton' + +export default function PlaygroundDemoLayout({ + children +}: { + children: React.ReactNode +}) { + return ( + <> +  }> + + +
{children}
+ + ) +} diff --git a/packages/docs/src/app/playground/(demos)/pagination/api.ts b/packages/docs/src/app/playground/(demos)/pagination/api.ts new file mode 100644 index 000000000..81d959957 --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/pagination/api.ts @@ -0,0 +1,28 @@ +import { faker } from '@faker-js/faker' + +// Ensure consistent results +faker.seed(47) + +export const pageSize = 5 +export const pageCount = 5 + +// Fake an in-memory product API +const productDatabase = Array.from( + { length: pageSize * pageCount }, + (_, i) => ({ + id: i, + name: faker.commerce.productName(), + price: faker.commerce.price({ symbol: '€' }), + description: faker.commerce.productDescription() + }) +) + +export type Product = (typeof productDatabase)[number] + +export async function fetchProducts( + page: number, + delay: number +): Promise { + await new Promise(resolve => setTimeout(resolve, delay)) + return productDatabase.slice((page - 1) * pageSize, page * pageSize) +} diff --git a/packages/docs/src/app/playground/(demos)/pagination/page.tsx b/packages/docs/src/app/playground/(demos)/pagination/page.tsx new file mode 100644 index 000000000..19b87a756 --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/pagination/page.tsx @@ -0,0 +1,83 @@ +import { Description } from '@/src/components/typography' +import { Separator } from '@/src/components/ui/separator' +import type { SearchParams } from 'nuqs/parsers' +import { Suspense } from 'react' +import { SourceOnGitHub } from '../_components/source-on-github' +import { getMetadata } from '../demos' +import { fetchProducts, pageCount } from './api' +import { ClientPaginationControls } from './pagination-controls.client' +import { ServerPaginationControls } from './pagination-controls.server' +import { ProductView } from './product' +import { RenderingControls } from './rendering-controls' +import { searchParamsCache } from './searchParams' + +export const metadata = getMetadata('pagination') + +type PageProps = { + searchParams: SearchParams +} + +export default async function PaginationDemoPage({ searchParams }: PageProps) { + // Allow nested RSCs to access the search params (in a type-safe way) + searchParamsCache.parse(searchParams) + return ( + <> +

{metadata.title}

+ {metadata.description} +

Rendering controls

+ + + + + + + + + + + + + + ) +} + +function PaginationRenderer() { + // Showcasing the use of search params cache in nested RSCs + const renderOn = searchParamsCache.get('renderOn') + return ( + <> +

+ Pagination controls{' '} + + ({renderOn}-rendered) + +

+ {renderOn === 'server' && ( + + )} + + {renderOn === 'client' && ( + + )} + + + ) +} + +async function ProductSection() { + const { page, delay } = searchParamsCache.all() + const products = await fetchProducts(page, delay) + return ( +
+

+ Product list{' '} + + (server-rendered) + +

+ {products.map(product => ( + + ))} +
+ ) +} diff --git a/packages/docs/src/app/playground/(demos)/pagination/pagination-controls.client.tsx b/packages/docs/src/app/playground/(demos)/pagination/pagination-controls.client.tsx new file mode 100644 index 000000000..4121f6cb1 --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/pagination/pagination-controls.client.tsx @@ -0,0 +1,69 @@ +'use client' + +import { + Pagination, + PaginationButton, + PaginationContent, + PaginationItem, + PaginationNext, + PaginationPrevious +} from '@/src/components/ui/pagination' +import { cn } from '@/src/lib/utils' +import { useQueryState } from 'nuqs' +import React from 'react' +import { searchParams } from './searchParams' + +type PaginationControlsProps = { + numPages: number +} + +// Use client-side hooks to update the page number +// and observe the loading state +export function ClientPaginationControls({ + numPages +}: PaginationControlsProps) { + const [isLoading, startTransition] = React.useTransition() + const [page, setPage] = useQueryState( + 'page', + searchParams.page.withOptions({ + startTransition, + shallow: false // Send updates to the server + }) + ) + return ( + + + + setPage(p => Math.max(1, p - 1))} + /> + + {Array.from({ length: numPages }, (_, i) => ( + + setPage(i + 1)} + > + {i + 1} + + + ))} + + setPage(p => Math.min(numPages, p + 1))} + /> + + +
+ + ) +} diff --git a/packages/docs/src/app/playground/(demos)/pagination/pagination-controls.server.tsx b/packages/docs/src/app/playground/(demos)/pagination/pagination-controls.server.tsx new file mode 100644 index 000000000..343913a5c --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/pagination/pagination-controls.server.tsx @@ -0,0 +1,63 @@ +import { + Pagination, + PaginationContent, + PaginationItem, + PaginationLink, + PaginationNextLink, + PaginationPreviousLink +} from '@/src/components/ui/pagination' +import { cn } from '@/src/lib/utils' +import { searchParamsCache, serialize } from './searchParams' + +type PaginationControlsProps = { + numPages: number +} + +// Use components to navigate between pages +export function ServerPaginationControls({ + numPages +}: PaginationControlsProps) { + const { page, delay, renderOn } = searchParamsCache.all() + function pageURL(page: number) { + return serialize('/playground/pagination', { + page, + delay, + renderOn + }) + } + return ( + + + + + + {Array.from({ length: numPages }, (_, i) => ( + + + {i + 1} + + + ))} + + + + +
+ + ) +} diff --git a/packages/docs/src/app/playground/(demos)/pagination/product.tsx b/packages/docs/src/app/playground/(demos)/pagination/product.tsx new file mode 100644 index 000000000..9caecf758 --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/pagination/product.tsx @@ -0,0 +1,18 @@ +import type { Product } from './api' + +type ProductViewProps = React.ComponentProps<'div'> & { + product: Product +} + +export function ProductView({ product, ...props }: ProductViewProps) { + return ( +
+
+

{product.name}

+ {product.price} + SKU-{product.id + 1} +
+

{product.description}

+
+ ) +} diff --git a/packages/docs/src/app/playground/(demos)/pagination/rendering-controls.tsx b/packages/docs/src/app/playground/(demos)/pagination/rendering-controls.tsx new file mode 100644 index 000000000..b0820a60d --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/pagination/rendering-controls.tsx @@ -0,0 +1,70 @@ +'use client' + +import { Label } from '@/src/components/ui/label' +import { ToggleGroup, ToggleGroupItem } from '@/src/components/ui/toggle-group' +import { useQueryStates } from 'nuqs' +import { + RenderingOptions, + renderingOptions, + searchParams +} from './searchParams' + +export function RenderingControls() { + const [{ renderOn, delay }, setControls] = useQueryStates(searchParams, { + shallow: false + }) + return ( + + ) +} diff --git a/packages/docs/src/app/playground/(demos)/pagination/searchParams.ts b/packages/docs/src/app/playground/(demos)/pagination/searchParams.ts new file mode 100644 index 000000000..1a8a2f458 --- /dev/null +++ b/packages/docs/src/app/playground/(demos)/pagination/searchParams.ts @@ -0,0 +1,18 @@ +import { + createSearchParamsCache, + createSerializer, + parseAsInteger, + parseAsStringLiteral +} from 'nuqs/parsers' + +export const renderingOptions = ['server', 'client'] as const +export type RenderingOptions = (typeof renderingOptions)[number] + +export const searchParams = { + page: parseAsInteger.withDefault(1), + renderOn: parseAsStringLiteral(renderingOptions).withDefault('server'), + delay: parseAsInteger.withDefault(0) +} + +export const searchParamsCache = createSearchParamsCache(searchParams) +export const serialize = createSerializer(searchParams) diff --git a/packages/docs/src/app/(pages)/playground/builder-pattern/page.tsx b/packages/docs/src/app/playground/_demos/builder-pattern/page.tsx similarity index 100% rename from packages/docs/src/app/(pages)/playground/builder-pattern/page.tsx rename to packages/docs/src/app/playground/_demos/builder-pattern/page.tsx diff --git a/packages/docs/src/app/(pages)/playground/compound-parsers/page.tsx b/packages/docs/src/app/playground/_demos/compound-parsers/page.tsx similarity index 100% rename from packages/docs/src/app/(pages)/playground/compound-parsers/page.tsx rename to packages/docs/src/app/playground/_demos/compound-parsers/page.tsx diff --git a/packages/docs/src/app/(pages)/playground/crosslink/page.tsx b/packages/docs/src/app/playground/_demos/crosslink/page.tsx similarity index 100% rename from packages/docs/src/app/(pages)/playground/crosslink/page.tsx rename to packages/docs/src/app/playground/_demos/crosslink/page.tsx diff --git a/packages/docs/src/app/(pages)/playground/custom-parser/page.tsx b/packages/docs/src/app/playground/_demos/custom-parser/page.tsx similarity index 100% rename from packages/docs/src/app/(pages)/playground/custom-parser/page.tsx rename to packages/docs/src/app/playground/_demos/custom-parser/page.tsx diff --git a/packages/docs/src/app/(pages)/playground/parsers/page.tsx b/packages/docs/src/app/playground/_demos/parsers/page.tsx similarity index 100% rename from packages/docs/src/app/(pages)/playground/parsers/page.tsx rename to packages/docs/src/app/playground/_demos/parsers/page.tsx diff --git a/packages/docs/src/app/(pages)/playground/pretty-urls/page.tsx b/packages/docs/src/app/playground/_demos/pretty-urls/page.tsx similarity index 100% rename from packages/docs/src/app/(pages)/playground/pretty-urls/page.tsx rename to packages/docs/src/app/playground/_demos/pretty-urls/page.tsx diff --git a/packages/docs/src/app/(pages)/playground/repro-359/page.tsx b/packages/docs/src/app/playground/_demos/repro-359/page.tsx similarity index 100% rename from packages/docs/src/app/(pages)/playground/repro-359/page.tsx rename to packages/docs/src/app/playground/_demos/repro-359/page.tsx diff --git a/packages/docs/src/app/(pages)/playground/repro-376/page.tsx b/packages/docs/src/app/playground/_demos/repro-376/page.tsx similarity index 100% rename from packages/docs/src/app/(pages)/playground/repro-376/page.tsx rename to packages/docs/src/app/playground/_demos/repro-376/page.tsx diff --git a/packages/docs/src/app/(pages)/playground/server-side-parsing/client.tsx b/packages/docs/src/app/playground/_demos/server-side-parsing/client.tsx similarity index 100% rename from packages/docs/src/app/(pages)/playground/server-side-parsing/client.tsx rename to packages/docs/src/app/playground/_demos/server-side-parsing/client.tsx diff --git a/packages/docs/src/app/(pages)/playground/server-side-parsing/page.tsx b/packages/docs/src/app/playground/_demos/server-side-parsing/page.tsx similarity index 100% rename from packages/docs/src/app/(pages)/playground/server-side-parsing/page.tsx rename to packages/docs/src/app/playground/_demos/server-side-parsing/page.tsx diff --git a/packages/docs/src/app/(pages)/playground/server-side-parsing/parser.ts b/packages/docs/src/app/playground/_demos/server-side-parsing/parser.ts similarity index 100% rename from packages/docs/src/app/(pages)/playground/server-side-parsing/parser.ts rename to packages/docs/src/app/playground/_demos/server-side-parsing/parser.ts diff --git a/packages/docs/src/app/(pages)/playground/subscribeToQueryUpdates/page.tsx b/packages/docs/src/app/playground/_demos/subscribeToQueryUpdates/page.tsx similarity index 100% rename from packages/docs/src/app/(pages)/playground/subscribeToQueryUpdates/page.tsx rename to packages/docs/src/app/playground/_demos/subscribeToQueryUpdates/page.tsx diff --git a/packages/docs/src/app/(pages)/playground/throttling/client.tsx b/packages/docs/src/app/playground/_demos/throttling/client.tsx similarity index 100% rename from packages/docs/src/app/(pages)/playground/throttling/client.tsx rename to packages/docs/src/app/playground/_demos/throttling/client.tsx diff --git a/packages/docs/src/app/(pages)/playground/throttling/page.tsx b/packages/docs/src/app/playground/_demos/throttling/page.tsx similarity index 100% rename from packages/docs/src/app/(pages)/playground/throttling/page.tsx rename to packages/docs/src/app/playground/_demos/throttling/page.tsx diff --git a/packages/docs/src/app/(pages)/playground/throttling/parsers.ts b/packages/docs/src/app/playground/_demos/throttling/parsers.ts similarity index 100% rename from packages/docs/src/app/(pages)/playground/throttling/parsers.ts rename to packages/docs/src/app/playground/_demos/throttling/parsers.ts diff --git a/packages/docs/src/app/(pages)/playground/_components/debug-control.tsx b/packages/docs/src/app/playground/debug-control.tsx similarity index 84% rename from packages/docs/src/app/(pages)/playground/_components/debug-control.tsx rename to packages/docs/src/app/playground/debug-control.tsx index d5641866c..3fd07ceac 100644 --- a/packages/docs/src/app/(pages)/playground/_components/debug-control.tsx +++ b/packages/docs/src/app/playground/debug-control.tsx @@ -26,9 +26,9 @@ export default function DebugControl() { }, []) return ( - + + Console debugging + ) } diff --git a/packages/docs/src/app/playground/layout.tsx b/packages/docs/src/app/playground/layout.tsx new file mode 100644 index 000000000..dba4d1deb --- /dev/null +++ b/packages/docs/src/app/playground/layout.tsx @@ -0,0 +1,44 @@ +import { NuqsWordmark } from '@/src/components/logo' +import { navItems } from '@/src/components/nav' +import { DocsLayout } from 'next-docs-ui/layout' +import { DocsBody } from 'next-docs-ui/page' +import dynamic from 'next/dynamic' +import React from 'react' +import { getPlaygroundTree } from './(demos)/demos' + +const DebugControlsSkeleton = () => ( + +) + +const DebugControl = dynamic(() => import('./debug-control'), { + ssr: false, + loading: DebugControlsSkeleton +}) + +export default function PlaygroundLayout({ + children +}: { + children: React.ReactNode +}) { + return ( + <> + , + items: navItems, + githubUrl: 'https://github.com/47ng/nuqs' + }} + sidebar={{ + collapsible: false, + footer: + }} + > + {children} + + + ) +} diff --git a/packages/docs/src/app/playground/page.tsx b/packages/docs/src/app/playground/page.tsx new file mode 100644 index 000000000..baf9c383c --- /dev/null +++ b/packages/docs/src/app/playground/page.tsx @@ -0,0 +1,28 @@ +import { Description, H1 } from '@/src/components/typography' +import { Card } from 'next-docs-ui/mdx/card' +import { demos } from './(demos)/demos' + +export const metadata = { + title: 'Playground', + description: 'Examples and demos of nuqs in action.' +} + +export default function PlaygroundIndexPage() { + return ( +
+

{metadata.title}

+ {metadata.description} +
    + {Object.entries(demos).map(([path, { title, description }]) => ( +
  • + +
  • + ))} +
+
+ ) +} diff --git a/packages/docs/src/components/code-block.tsx b/packages/docs/src/components/code-block.tsx new file mode 100644 index 000000000..198e306d5 --- /dev/null +++ b/packages/docs/src/components/code-block.tsx @@ -0,0 +1,50 @@ +import { codeToHtml } from 'shikiji' +import { twMerge } from 'tailwind-merge' + +type CodeBlockProps = React.ComponentProps<'div'> & { + code: string + lang?: 'tsx' +} + +export async function CodeBlock({ + code, + lang = 'tsx', + className, + ...props +}: CodeBlockProps) { + const demoCode = await codeToHtml(code, { + lang, + themes: { + dark: 'github-dark', + light: 'github-light' + }, + transformers: [ + { + name: 'transparent background', + pre(node) { + if (typeof node.properties.style !== 'string') { + return node + } + node.properties.style = node.properties.style + .split(';') + .filter(style => !style.includes('-bg:')) + .concat([ + '--shiki-dark-bg:transparent', + '--shiki-light-bg:transparent' + ]) + .join(';') + return node + } + } + ] + }) + return ( +
+ ) +} diff --git a/packages/docs/src/components/quote.tsx b/packages/docs/src/components/quote.tsx index 3acb1e248..fe784efc4 100644 --- a/packages/docs/src/components/quote.tsx +++ b/packages/docs/src/components/quote.tsx @@ -17,7 +17,7 @@ type QuoteProps = { export function Quote({ text, author, url }: QuoteProps) { return ( -
+
- @{author.handle} + {author.handle}
)}
diff --git a/packages/docs/src/components/typography.tsx b/packages/docs/src/components/typography.tsx new file mode 100644 index 000000000..88bef9e9a --- /dev/null +++ b/packages/docs/src/components/typography.tsx @@ -0,0 +1,27 @@ +import { twMerge } from 'tailwind-merge' + +export const H1: React.FC> = ({ + children, + className, + ...props +}) => ( +

+ {children} +

+) + +export const Description: React.FC> = ({ + children, + className, + ...props +}) => ( +

+ {children} +

+) diff --git a/packages/docs/src/components/ui/label.tsx b/packages/docs/src/components/ui/label.tsx new file mode 100644 index 000000000..65690f69a --- /dev/null +++ b/packages/docs/src/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/src/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/packages/docs/src/components/ui/pagination.tsx b/packages/docs/src/components/ui/pagination.tsx new file mode 100644 index 000000000..e4cdde1c0 --- /dev/null +++ b/packages/docs/src/components/ui/pagination.tsx @@ -0,0 +1,180 @@ +import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react' +import * as React from 'react' + +import { Button, ButtonProps, buttonVariants } from '@/src/components/ui/button' +import { cn } from '@/src/lib/utils' +import Link, { LinkProps } from 'next/link' + +const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => ( +