From 2ff9f9014ff96ba55460ebe28602d60cd4493b8e Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Tue, 28 Apr 2026 21:11:36 -0500 Subject: [PATCH] feat: new libraries/framework pages --- src/components/LibraryCard.tsx | 17 ++- src/images/alpine-logo.svg | 1 + src/libraries/frameworks.tsx | 22 ++-- src/libraries/libraries.ts | 5 +- src/libraries/types.ts | 1 + src/routeTree.gen.ts | 21 ++++ src/routes/-libraries-utils.ts | 43 +++++++ src/routes/libraries.tsx | 61 +++++++--- src/routes/libraries_.$framework.tsx | 161 +++++++++++++++++++++++++++ src/utils/npm-packages.ts | 1 + 10 files changed, 304 insertions(+), 29 deletions(-) create mode 100644 src/images/alpine-logo.svg create mode 100644 src/routes/-libraries-utils.ts create mode 100644 src/routes/libraries_.$framework.tsx diff --git a/src/components/LibraryCard.tsx b/src/components/LibraryCard.tsx index a7bbf7a85..fe0c28df6 100644 --- a/src/components/LibraryCard.tsx +++ b/src/components/LibraryCard.tsx @@ -1,24 +1,35 @@ -import { Link } from '@tanstack/react-router' +import { Link, useParams } from '@tanstack/react-router' import { Library } from '~/libraries' +import { frameworkOptions } from '~/libraries/frameworks' import { twMerge } from 'tailwind-merge' export default function LibraryCard({ library, index = 0, isGeneric = false, + namePrefix, }: { library: Library index?: number isGeneric?: boolean + namePrefix?: string }) { const isExternal = library.to?.startsWith('http') const Component = isExternal ? 'a' : Link const props = isExternal ? { href: library.to, target: '_blank', rel: 'noopener noreferrer' } : { to: library.to ?? '#' } + const params = useParams({ strict: false }) + const frameworkPrefix = + namePrefix ?? + frameworkOptions.find((framework) => framework.value === params.framework) + ?.label const hasTanStackPrefix = library.name.startsWith('TanStack ') const nameWithoutPrefix = library.name.replace('TanStack ', '') + const displayName = frameworkPrefix + ? `${frameworkPrefix}-${nameWithoutPrefix}` + : nameWithoutPrefix return ( - {nameWithoutPrefix} + {displayName} ) : ( @@ -93,7 +104,7 @@ export default function LibraryCard({ : 'text-current' } > - {nameWithoutPrefix} + {displayName} )} diff --git a/src/images/alpine-logo.svg b/src/images/alpine-logo.svg new file mode 100644 index 000000000..932c1bdaa --- /dev/null +++ b/src/images/alpine-logo.svg @@ -0,0 +1 @@ + diff --git a/src/libraries/frameworks.tsx b/src/libraries/frameworks.tsx index f54b22962..38605e842 100644 --- a/src/libraries/frameworks.tsx +++ b/src/libraries/frameworks.tsx @@ -1,3 +1,4 @@ +import alpineLogo from '../images/alpine-logo.svg' import angularLogo from '../images/angular-logo.svg' import jsLogo from '../images/js-logo.svg' import litLogo from '../images/lit-logo.svg' @@ -45,13 +46,6 @@ export const frameworkOptions = [ color: 'bg-blue-600', fontColor: 'text-blue-600', }, - { - label: 'Lit', - value: 'lit', - logo: litLogo, - color: 'bg-emerald-500', - fontColor: 'text-emerald-500', - }, { label: 'Svelte', value: 'svelte', @@ -66,6 +60,20 @@ export const frameworkOptions = [ color: 'bg-indigo-500', fontColor: 'text-indigo-500', }, + { + label: 'Lit', + value: 'lit', + logo: litLogo, + color: 'bg-emerald-500', + fontColor: 'text-emerald-500', + }, + { + label: 'Alpine', + value: 'alpine', + logo: alpineLogo, + color: 'bg-slate-500', + fontColor: 'text-slate-500', + }, { label: 'Vanilla', value: 'vanilla', diff --git a/src/libraries/libraries.ts b/src/libraries/libraries.ts index 53a2fa926..a68835646 100644 --- a/src/libraries/libraries.ts +++ b/src/libraries/libraries.ts @@ -321,12 +321,13 @@ export const table: LibrarySlim = { repo: 'tanstack/table', frameworks: [ 'angular', - 'lit', - 'qwik', 'react', 'solid', 'svelte', 'vue', + 'qwik', + 'lit', + 'alpine', 'vanilla', ], latestVersion: 'v8', diff --git a/src/libraries/types.ts b/src/libraries/types.ts index b6debb7b3..12c9a6f4b 100644 --- a/src/libraries/types.ts +++ b/src/libraries/types.ts @@ -2,6 +2,7 @@ import * as React from 'react' export type Framework = | 'angular' + | 'alpine' | 'lit' | 'preact' | 'qwik' diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 103d0b56c..1e88095e5 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -57,6 +57,7 @@ import { Route as PartnersPartnerRouteImport } from './routes/partners.$partner' import { Route as OauthTokenRouteImport } from './routes/oauth/token' import { Route as OauthRegisterRouteImport } from './routes/oauth/register' import { Route as OauthAuthorizeRouteImport } from './routes/oauth/authorize' +import { Route as LibrariesFrameworkRouteImport } from './routes/libraries_.$framework' import { Route as BuilderDocsRouteImport } from './routes/builder.docs' import { Route as BlogSplatRouteImport } from './routes/blog.$' import { Route as AuthSignoutRouteImport } from './routes/auth/signout' @@ -390,6 +391,11 @@ const OauthAuthorizeRoute = OauthAuthorizeRouteImport.update({ path: '/oauth/authorize', getParentRoute: () => rootRouteImport, } as any) +const LibrariesFrameworkRoute = LibrariesFrameworkRouteImport.update({ + id: '/libraries_/$framework', + path: '/libraries/$framework', + getParentRoute: () => rootRouteImport, +} as any) const BuilderDocsRoute = BuilderDocsRouteImport.update({ id: '/docs', path: '/docs', @@ -925,6 +931,7 @@ export interface FileRoutesByFullPath { '/auth/signout': typeof AuthSignoutRoute '/blog/$': typeof BlogSplatRoute '/builder/docs': typeof BuilderDocsRoute + '/libraries/$framework': typeof LibrariesFrameworkRoute '/oauth/authorize': typeof OauthAuthorizeRoute '/oauth/register': typeof OauthRegisterRoute '/oauth/token': typeof OauthTokenRoute @@ -1059,6 +1066,7 @@ export interface FileRoutesByTo { '/auth/signout': typeof AuthSignoutRoute '/blog/$': typeof BlogSplatRoute '/builder/docs': typeof BuilderDocsRoute + '/libraries/$framework': typeof LibrariesFrameworkRoute '/oauth/authorize': typeof OauthAuthorizeRoute '/oauth/register': typeof OauthRegisterRoute '/oauth/token': typeof OauthTokenRoute @@ -1200,6 +1208,7 @@ export interface FileRoutesById { '/auth/signout': typeof AuthSignoutRoute '/blog/$': typeof BlogSplatRoute '/builder/docs': typeof BuilderDocsRoute + '/libraries_/$framework': typeof LibrariesFrameworkRoute '/oauth/authorize': typeof OauthAuthorizeRoute '/oauth/register': typeof OauthRegisterRoute '/oauth/token': typeof OauthTokenRoute @@ -1344,6 +1353,7 @@ export interface FileRouteTypes { | '/auth/signout' | '/blog/$' | '/builder/docs' + | '/libraries/$framework' | '/oauth/authorize' | '/oauth/register' | '/oauth/token' @@ -1478,6 +1488,7 @@ export interface FileRouteTypes { | '/auth/signout' | '/blog/$' | '/builder/docs' + | '/libraries/$framework' | '/oauth/authorize' | '/oauth/register' | '/oauth/token' @@ -1618,6 +1629,7 @@ export interface FileRouteTypes { | '/auth/signout' | '/blog/$' | '/builder/docs' + | '/libraries_/$framework' | '/oauth/authorize' | '/oauth/register' | '/oauth/token' @@ -1747,6 +1759,7 @@ export interface RootRouteChildren { AuthCliRoute: typeof AuthCliRoute AuthPopupSuccessRoute: typeof AuthPopupSuccessRoute AuthSignoutRoute: typeof AuthSignoutRoute + LibrariesFrameworkRoute: typeof LibrariesFrameworkRoute OauthAuthorizeRoute: typeof OauthAuthorizeRoute OauthRegisterRoute: typeof OauthRegisterRoute OauthTokenRoute: typeof OauthTokenRoute @@ -2139,6 +2152,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof OauthAuthorizeRouteImport parentRoute: typeof rootRouteImport } + '/libraries_/$framework': { + id: '/libraries_/$framework' + path: '/libraries/$framework' + fullPath: '/libraries/$framework' + preLoaderRoute: typeof LibrariesFrameworkRouteImport + parentRoute: typeof rootRouteImport + } '/builder/docs': { id: '/builder/docs' path: '/docs' @@ -3041,6 +3061,7 @@ const rootRouteChildren: RootRouteChildren = { AuthCliRoute: AuthCliRoute, AuthPopupSuccessRoute: AuthPopupSuccessRoute, AuthSignoutRoute: AuthSignoutRoute, + LibrariesFrameworkRoute: LibrariesFrameworkRoute, OauthAuthorizeRoute: OauthAuthorizeRoute, OauthRegisterRoute: OauthRegisterRoute, OauthTokenRoute: OauthTokenRoute, diff --git a/src/routes/-libraries-utils.ts b/src/routes/-libraries-utils.ts new file mode 100644 index 000000000..50dd51ec8 --- /dev/null +++ b/src/routes/-libraries-utils.ts @@ -0,0 +1,43 @@ +import { libraries, type Framework, type LibrarySlim } from '~/libraries' + +export function getVisibleLibraries() { + return libraries.filter((library) => library.to && library.visible !== false) +} + +export function orderLibrariesForBrowse( + allLibraries: Array, +) { + const others = allLibraries.filter( + (library) => + library.id !== 'ranger' && + library.id !== 'config' && + library.id !== 'react-charts', + ) + const ranger = allLibraries.filter((library) => library.id === 'ranger') + const config = allLibraries.filter((library) => library.id === 'config') + + const devtoolsIndex = others.findIndex((library) => library.id === 'devtools') + + if (devtoolsIndex === -1) { + return [...others, ...config, ...ranger] + } + + return [ + ...others.slice(0, devtoolsIndex + 1), + ...config, + ...others.slice(devtoolsIndex + 1), + ...ranger, + ] +} + +export function getFrameworkLibraryCounts(allLibraries: Array) { + const counts = {} as Partial> + + for (const library of allLibraries) { + for (const framework of library.frameworks) { + counts[framework] = (counts[framework] ?? 0) + 1 + } + } + + return counts +} diff --git a/src/routes/libraries.tsx b/src/routes/libraries.tsx index 0fa34d790..d0cc06df4 100644 --- a/src/routes/libraries.tsx +++ b/src/routes/libraries.tsx @@ -1,8 +1,14 @@ -import { createFileRoute } from '@tanstack/react-router' +import { Link, createFileRoute } from '@tanstack/react-router' import * as React from 'react' -import { libraries, Library } from '~/libraries' +import { type Library } from '~/libraries' +import { frameworkOptions } from '~/libraries/frameworks' import { reactChartsProject } from '~/libraries/react-charts' import LibraryCard from '~/components/LibraryCard' +import { + getFrameworkLibraryCounts, + getVisibleLibraries, + orderLibrariesForBrowse, +} from './-libraries-utils' export const Route = createFileRoute('/libraries')({ component: LibrariesPage, @@ -20,22 +26,12 @@ export const Route = createFileRoute('/libraries')({ }) function LibrariesPage() { - const allLibraries = libraries.filter((d) => d.to && d.visible !== false) - const others = allLibraries.filter( - (l) => l.id !== 'ranger' && l.id !== 'config' && l.id !== 'react-charts', + const allLibraries = getVisibleLibraries() + const ordered = orderLibrariesForBrowse(allLibraries) + const frameworkCounts = getFrameworkLibraryCounts(allLibraries) + const frameworksWithLibraries = frameworkOptions.filter( + (framework) => (frameworkCounts[framework.value] ?? 0) > 0, ) - const ranger = allLibraries.filter((l) => l.id === 'ranger') - const config = allLibraries.filter((l) => l.id === 'config') - - // Find devtools index in others to insert config after it - const devtoolsIndex = others.findIndex((l) => l.id === 'devtools') - const ordered = [ - ...others.slice(0, devtoolsIndex + 1), - ...config, - ...others.slice(devtoolsIndex + 1), - ...ranger, - ] - const deprecatedLibraries = [reactChartsProject] return ( @@ -45,6 +41,37 @@ function LibrariesPage() { Browse all TanStack libraries.

+
+

Browse by Your Framework

+
+ {frameworksWithLibraries.map((framework) => { + const count = frameworkCounts[framework.value] ?? 0 + + return ( + + + {framework.label} + + {count} {count === 1 ? 'library' : 'libraries'} + + + ) + })} +
+
+
{ + const frameworkOption = getFrameworkOption(params.framework) + + if (!frameworkOption) { + throw notFound() + } + + return { + framework: frameworkOption, + } + }, + head: ({ loaderData }) => ({ + meta: [ + { + title: loaderData?.framework + ? `TanStack for ${loaderData.framework.label} - Libraries` + : 'TanStack Libraries', + }, + { + name: 'description', + content: loaderData?.framework + ? `Browse TanStack libraries with ${loaderData.framework.label} adapters.` + : 'Browse TanStack libraries by framework.', + }, + ], + }), + component: LibrariesFrameworkPage, +}) + +function LibrariesFrameworkPage() { + const { framework } = Route.useParams() + const { framework: frameworkOption } = Route.useLoaderData() + + const allLibraries = getVisibleLibraries() + const frameworkCounts = getFrameworkLibraryCounts(allLibraries) + const frameworksWithLibraries = frameworkOptions.filter( + (option) => (frameworkCounts[option.value] ?? 0) > 0, + ) + const filteredLibraries = orderLibrariesForBrowse( + allLibraries.filter((library) => + library.frameworks.includes(framework as Framework), + ), + ) + const count = filteredLibraries.length + + return ( +
+
+ + All Libraries + + / + + {frameworkOption.label} + +
+ +
+
+
+ +

+ TanStack for {frameworkOption.label}! +

+
+

+ Browse TanStack libraries with {frameworkOption.label} adapters. +

+
+ +
+ {count} {count === 1 ? 'library' : 'libraries'} +
+
+ +
+

Browse by Your Framework

+
+ {frameworksWithLibraries.map((option) => { + const optionCount = frameworkCounts[option.value] ?? 0 + const isActive = option.value === frameworkOption.value + + return ( + + + {option.label} + + {optionCount} {optionCount === 1 ? 'library' : 'libraries'} + + + ) + })} +
+
+ + {filteredLibraries.length > 0 ? ( +
+ {filteredLibraries.map((library, i) => { + return ( + + ) + })} +
+ ) : ( +
+ No TanStack libraries currently list a {frameworkOption.label}{' '} + adapter. +
+ )} +
+ ) +} + +function getFrameworkOption(framework: string) { + return frameworkOptions.find((option) => option.value === framework) +} diff --git a/src/utils/npm-packages.ts b/src/utils/npm-packages.ts index 53141a741..a30c48950 100644 --- a/src/utils/npm-packages.ts +++ b/src/utils/npm-packages.ts @@ -27,6 +27,7 @@ export const frameworkMeta: Record = vue: { name: 'Vue', color: '#42B883' }, svelte: { name: 'Svelte', color: '#FF3E00' }, angular: { name: 'Angular', color: '#DD0031' }, + alpine: { name: 'Alpine', color: '#77C1D2' }, lit: { name: 'Lit', color: '#325CFF' }, qwik: { name: 'Qwik', color: '#18B6F6' }, vanilla: { name: 'Vanilla', color: '#F7DF1E' },