From bd6e3af0fdf956389ab79cbd44801bce2888e343 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 27 Apr 2026 23:31:31 -0600 Subject: [PATCH 1/7] feat(partners): introduce gold/silver/bronze tiers across grid and rail Replace score-based dynamic sizing in PartnersGrid and PartnersRail with a tiered system. Each active partner is assigned a tier (gold, silver, or bronze) that drives sizing, layout, and accent styling. Score is retained for in-tier ordering. - Add `PartnerTier` type, `partnerTierLabels`, `partnerTierOrder`, and `partnerTierFlares` (gradients, icons, label colors) shared between PartnersGrid and PartnersRail - PartnersGrid: render each tier as its own Card with a gradient L-shape, rounded TL/BR + sharp TR/BL corners, and a centered pill tier label. Different per-tier sizing with clear visual jumps between tiers. Mobile-responsive (gold 1-col, silver 1-col, bronze 2-col on small) - PartnersRail: group by tier with per-tier max widths, gradient top bars, and centered pill tier labels. Desaturate + brightness-90 logos with hover restoration. Hide scrollbars and prevent flex shrink so the rail no longer clips its content - Move `Become a Partner` link into PartnersRail itself so it shows up on blog pages too (previously docs-only via overlay) - Mark Convex, Fireship, Nozzle, Vercel, Speakeasy inactive - Restructure blog index layout so the rail meets the page edge while keeping content padding intact - Fix GamVrec1 to use `w-full max-w-[300px]` (was hardcoded `w-[300px]`, causing horizontal overflow inside the 280px docs rail) - Tune per-partner image scale on Clerk, Netlify, OpenRouter, PowerSync for visual balance within their tier --- src/components/DocsLayout.tsx | 38 ++------ src/components/Gam.tsx | 5 +- src/components/PartnersGrid.tsx | 124 +++++++++++++++++++++----- src/components/RightRail.tsx | 147 ++++++++++++++++++++++++++----- src/routes/blog.$.tsx | 8 +- src/routes/blog.index.tsx | 37 ++++---- src/utils/partners.tsx | 149 +++++++++++++++++++++----------- 7 files changed, 356 insertions(+), 152 deletions(-) diff --git a/src/components/DocsLayout.tsx b/src/components/DocsLayout.tsx index 50ea2fde9..bf4fec98d 100644 --- a/src/components/DocsLayout.tsx +++ b/src/components/DocsLayout.tsx @@ -599,10 +599,7 @@ export function DocsLayout({ const [isFullWidth, setIsFullWidth] = useLocalStorage('docsFullWidth', false) - const activePartners = partners.filter( - (d) => - d.status === 'active' && d.name !== 'Nozzle.io' && d.id !== 'fireship', - ) + const activePartners = partners.filter((d) => d.status === 'active') const groupInitialOpenState = React.useMemo(() => { return menuConfig.reduce>((acc, group, index) => { @@ -937,31 +934,14 @@ export function DocsLayout({ {!isLandingPage && ( -
- - { - trackEvent('become_partner_clicked', { - framework: currentFramework.framework, - library_id: libraryId, - placement: 'docs_right_rail', - }) - }} - > - Become a Partner - -
+
diff --git a/src/components/Gam.tsx b/src/components/Gam.tsx index c444bc092..ee1bd8724 100644 --- a/src/components/Gam.tsx +++ b/src/components/Gam.tsx @@ -59,7 +59,10 @@ export function GamVrec1({ return (
{promos.map((promo) => ( = { + gold: { + flexBasis: 'basis-full sm:basis-1/2', + minHeight: 'min-h-[220px]', + logoMaxWidth: 'max-w-[400px]', + logoMaxHeight: 'max-h-[120px]', + padding: 'p-12', + }, + silver: { + flexBasis: 'basis-full sm:basis-1/2 md:basis-1/3 lg:basis-1/4', + minHeight: 'min-h-[130px]', + logoMaxWidth: 'max-w-[180px]', + logoMaxHeight: 'max-h-[56px]', + padding: 'p-6', + }, + bronze: { + flexBasis: 'basis-1/2 sm:basis-1/3 md:basis-1/4 lg:basis-1/5 xl:basis-1/6', + minHeight: 'min-h-[100px]', + logoMaxWidth: 'max-w-[110px]', + logoMaxHeight: 'max-h-[36px]', + padding: 'p-4', + }, +} + function PartnerGridItem({ analyticsPlacement, analyticsProperties, @@ -33,7 +73,7 @@ function PartnerGridItem({ }, }) - const width = Math.max(Math.round(120 + 280 * partner.score), 150) + const layout = tierLayout[partner.tier ?? 'bronze'] return ( { trackEvent('partner_card_clicked', { partner_id: partner.id, @@ -56,7 +96,15 @@ function PartnerGridItem({ }) }} > - +
+ +
) } @@ -70,22 +118,56 @@ export function PartnersGrid({ (partner) => partner.status === 'active', ) - // Sort by score descending so larger partners come first - const sortedItems = [...items].sort((a, b) => b.score - a.score) + const tiers: Array = ['gold', 'silver', 'bronze'] + + const tiersWithPartners = tiers + .map((tier) => ({ + tier, + partners: items + .filter((partner) => (partner.tier ?? 'bronze') === tier) + .sort((a, b) => b.score - a.score), + })) + .filter((row) => row.partners.length > 0) + .sort((a, b) => partnerTierOrder[a.tier] - partnerTierOrder[b.tier]) + + let slotIndex = 0 return ( - -
- {sortedItems.map((partner, index) => ( - - ))} -
-
+
+ {tiersWithPartners.map((row) => { + const flare = partnerTierFlares[row.tier] + return ( + +
+
+ {flare.icon} + + {partnerTierLabels[row.tier]} + +
+
+ {row.partners.map((partner) => { + const index = slotIndex++ + return ( + + ) + })} +
+
+
+ ) + })} +
) } diff --git a/src/components/RightRail.tsx b/src/components/RightRail.tsx index 85057d9c9..041a2482a 100644 --- a/src/components/RightRail.tsx +++ b/src/components/RightRail.tsx @@ -1,7 +1,13 @@ import * as React from 'react' import { Link } from '@tanstack/react-router' import { twMerge } from 'tailwind-merge' -import { PartnerImage } from '~/utils/partners' +import { + PartnerImage, + partnerTierFlares, + partnerTierLabels, + partnerTierOrder, + type PartnerTier, +} from '~/utils/partners' import { trackEvent, useTrackedImpression } from '~/utils/analytics' type RailPartner = { @@ -9,6 +15,7 @@ type RailPartner = { name: string href: string score: number + tier?: PartnerTier image: Parameters[0]['config'] } @@ -38,7 +45,7 @@ export function RightRail({
*]:shrink-0', )} > {children} @@ -47,6 +54,39 @@ export function RightRail({ ) } +const railTierLayout: Record< + PartnerTier, + { + flexBasis: string + minHeight: string + logoMaxWidth: string + logoMaxHeight: string + padding: string + } +> = { + gold: { + flexBasis: 'basis-full', + minHeight: 'min-h-[72px]', + logoMaxWidth: 'max-w-[180px]', + logoMaxHeight: 'max-h-[40px]', + padding: 'px-4 py-3', + }, + silver: { + flexBasis: 'basis-1/2', + minHeight: 'min-h-[60px]', + logoMaxWidth: 'max-w-[100px]', + logoMaxHeight: 'max-h-[26px]', + padding: 'px-2.5 py-2.5', + }, + bronze: { + flexBasis: 'basis-1/3', + minHeight: 'min-h-[56px]', + logoMaxWidth: 'max-w-[70px]', + logoMaxHeight: 'max-h-[22px]', + padding: 'px-2 py-2', + }, +} + export function PartnersRail({ analyticsPlacement = 'partners_rail', analyticsProperties, @@ -60,25 +100,79 @@ export function PartnersRail({ title?: string titleTo?: '/partners' }) { + const tiers: Array = ['gold', 'silver', 'bronze'] + + const rowsByTier = tiers + .map((tier) => ({ + tier, + partners: partners + .filter((partner) => (partner.tier ?? 'bronze') === tier) + .sort((a, b) => b.score - a.score), + })) + .filter((row) => row.partners.length > 0) + .sort( + (a, b) => partnerTierOrder[a.tier] - partnerTierOrder[b.tier], + ) + + let slotIndex = 0 + return ( -
-
+
+ - {partners.map((partner, index) => ( - - ))} + {rowsByTier.map((row) => { + const flare = partnerTierFlares[row.tier] + return ( +
+ {/* Tier-colored top line */} +
+ {/* Absolute top-left tier label */} +
+ {flare.icon} + {partnerTierLabels[row.tier]} +
+ {row.partners.map((partner) => { + const index = slotIndex++ + return ( + + ) + })} +
+ ) + })}
) } @@ -94,7 +188,7 @@ function PartnersRailItem({ index: number partner: RailPartner }) { - const widthPercent = Math.round(partner.score * 100) + const layout = railTierLayout[partner.tier ?? 'bronze'] const ref = useTrackedImpression({ event: 'partner_impression', properties: { @@ -112,12 +206,12 @@ function PartnersRailItem({ href={partner.href} target="_blank" rel="noreferrer" - className="flex items-center justify-center px-3 py-2 border-r border-b border-gray-500/20 hover:bg-gray-500/10 transition-colors duration-150 ease-out" - style={{ - flexBasis: `${widthPercent}%`, - flexGrow: 1, - flexShrink: 0, - }} + className={twMerge( + 'group flex items-center justify-center overflow-hidden border-r border-b border-gray-500/20 hover:bg-gray-500/10 transition-colors duration-150 ease-out', + layout.flexBasis, + layout.minHeight, + layout.padding, + )} onClick={() => { trackEvent('partner_click', { partner_id: partner.id, @@ -130,11 +224,16 @@ function PartnersRailItem({ }} >
- +
) diff --git a/src/routes/blog.$.tsx b/src/routes/blog.$.tsx index e09ebc78a..6c79f4270 100644 --- a/src/routes/blog.$.tsx +++ b/src/routes/blog.$.tsx @@ -113,13 +113,7 @@ function BlogPost() { const repo = 'tanstack/tanstack.com' const branch = 'main' const activePartners = React.useMemo( - () => - partners.filter( - (d) => - d.status === 'active' && - d.name !== 'Nozzle.io' && - d.id !== 'fireship', - ), + () => partners.filter((d) => d.status === 'active'), [], ) diff --git a/src/routes/blog.index.tsx b/src/routes/blog.index.tsx index ef4a7dce5..cbf4ea028 100644 --- a/src/routes/blog.index.tsx +++ b/src/routes/blog.index.tsx @@ -70,16 +70,13 @@ export const Route = createFileRoute('/blog/')({ function BlogIndex() { const frontMatters = Route.useLoaderData() as BlogFrontMatter[] - const activePartners = partners.filter( - (d) => - d.status === 'active' && d.name !== 'Nozzle.io' && d.id !== 'fireship', - ) + const activePartners = partners.filter((d) => d.status === 'active') return ( -
-
-
-
+
+
+
+

Blog

@@ -153,19 +150,19 @@ function BlogIndex() { )}
- - -
- -
- - - -
+ + +
+ +
+ + + +
diff --git a/src/utils/partners.tsx b/src/utils/partners.tsx index 4f24309a8..736b91ff2 100644 --- a/src/utils/partners.tsx +++ b/src/utils/partners.tsx @@ -59,6 +59,75 @@ type PartnerApplicationStarterIcon = { type ApplicationStarterPartnerTier = 1 | 2 | 3 +export const partnerTiers = ['gold', 'silver', 'bronze'] as const +export type PartnerTier = (typeof partnerTiers)[number] + +export const partnerTierLabels: Record = { + gold: 'Gold', + silver: 'Silver', + bronze: 'Bronze', +} + +export const partnerTierOrder: Record = { + gold: 0, + silver: 1, + bronze: 2, +} + +const partnerTierToBuilderTier: Record = + { + gold: 1, + silver: 2, + bronze: 3, + } + +export const partnerTierFlares: Record< + PartnerTier, + { + gradientStops: string + iconColor: string + labelColor: string + icon: React.ReactNode + } +> = { + gold: { + gradientStops: + 'from-yellow-400 via-amber-500 to-orange-600 dark:from-yellow-300 dark:via-amber-400 dark:to-orange-400', + iconColor: 'text-amber-500 dark:text-amber-300', + labelColor: 'text-amber-600 dark:text-amber-300', + // 5-point star + icon: ( + + + + ), + }, + silver: { + gradientStops: + 'from-slate-200 via-zinc-400 to-slate-300 dark:from-slate-300 dark:via-zinc-400 dark:to-slate-400', + iconColor: 'text-slate-400 dark:text-slate-300', + labelColor: 'text-slate-500 dark:text-slate-300', + // 4-point sparkle + icon: ( + + + + ), + }, + bronze: { + gradientStops: + 'from-amber-700 via-amber-800 to-amber-950 dark:from-amber-600 dark:via-amber-800 dark:to-amber-950', + iconColor: 'text-amber-800 dark:text-amber-600', + labelColor: 'text-amber-800 dark:text-amber-600', + // diamond + icon: ( + + + + ), + }, +} + export function PartnerImage({ className, config, @@ -162,6 +231,7 @@ export type Partner = { startDate?: string endDate?: string score: number + tier?: PartnerTier brandColor?: string // Primary brand color for game elements tagline?: string // Short tagline for game info cards } @@ -175,7 +245,6 @@ export type ApplicationStarterPartnerSuggestion = { iconSrc?: string image: Partner['image'] label: string - sortOrder: number tags: Array tier: ApplicationStarterPartnerTier } @@ -259,6 +328,7 @@ const neon = (() => { libraries: ['start', 'router'], status: 'active' as const, score: 0.297, + tier: 'silver' as const, href, brandColor: '#00E599', tagline: 'Serverless Postgres', @@ -328,12 +398,13 @@ const clerk = (() => { libraries: ['start', 'router'], status: 'active' as const, score: 0.286, + tier: 'silver' as const, brandColor: '#6C47FF', tagline: 'Authentication', image: { light: clerkLightSvg, dark: clerkDarkSvg, - scale: 0.85, + scale: 0.72, }, llmDescription: 'Authentication and user management platform with prebuilt UI, sessions, organizations, and MFA. Clerk has official SDKs and quickstarts for TanStack React Start and React Router.', @@ -363,6 +434,7 @@ const workos = (() => { libraries: ['start', 'router'] as const, status: 'active' as const, score: 0.314, + tier: 'silver' as const, brandColor: '#6363F1', tagline: 'Enterprise Auth', applicationStarterIcon: { @@ -400,6 +472,7 @@ const agGrid = (() => { libraries: ['table'] as const, status: 'active' as const, score: 0.497, + tier: 'silver' as const, href, brandColor: '#FF8C00', tagline: 'Enterprise Data Grid', @@ -452,6 +525,7 @@ const netlify = (() => { libraries: ['start', 'router'], status: 'active' as const, score: 0.343, + tier: 'silver' as const, href, brandColor: '#00C7B7', tagline: 'Web Deployment', @@ -462,6 +536,7 @@ const netlify = (() => { image: { light: netlifyLightSvg, dark: netlifyDarkSvg, + scale: 1.25, }, llmDescription: 'Deployment platform for web applications with Deploy Previews, Functions, Edge Functions, and an official TanStack Start integration guide.', @@ -491,6 +566,7 @@ const cloudflare = (() => { libraries: libraries.map((l) => l.id), status: 'active' as const, score: 0.857, + tier: 'gold' as const, startDate: 'Sep 2025', brandColor: '#F6821F', tagline: 'Edge Deployment', @@ -523,6 +599,7 @@ const sentry = (() => { libraries: ['start', 'router'], status: 'active' as const, score: 0.229, + tier: 'bronze' as const, href, brandColor: '#362D59', tagline: 'Error Monitoring', @@ -554,7 +631,7 @@ const fireship = (() => { name: 'Fireship', id: 'fireship', libraries: [], - status: 'active' as const, + status: 'inactive' as const, score: 0.014, href, tagline: 'Dev Education', @@ -610,7 +687,7 @@ const nozzle = (() => { name: 'Nozzle.io', id: 'nozzle', href, - status: 'active' as const, + status: 'inactive' as const, score: 0.014, tagline: 'Enterprise SEO', image: { @@ -677,6 +754,7 @@ const unkey = (() => { libraries: ['pacer'] as const, status: 'active' as const, score: 0.051, + tier: 'bronze' as const, href, brandColor: '#222222', tagline: 'API Key Management', @@ -716,6 +794,7 @@ const serpApi = (() => { libraries: libraries.map((l) => l.id), status: 'active' as const, score: 0.41, + tier: 'silver' as const, href, brandColor: '#6361EC', tagline: 'Real-time SERP API', @@ -755,6 +834,7 @@ const electric = (() => { libraries: ['db'] as const, status: 'active' as const, score: 0.283, + tier: 'bronze' as const, href, brandColor: '#7e78db', tagline: 'Sync Engine', @@ -828,6 +908,7 @@ const prisma = (() => { libraries: ['db', 'start'] as const, startDate: 'Aug 2025', score: 0.143, + tier: 'bronze' as const, brandColor: '#2D3748', tagline: 'Database ORM', image: { @@ -863,6 +944,7 @@ const codeRabbit = (() => { libraries: libraries.map((l) => l.id), startDate: 'Aug 2025', score: 1, + tier: 'gold' as const, brandColor: '#FF6B2B', tagline: 'AI Code Review', applicationStarterPromptInstructions: [ @@ -900,6 +982,7 @@ const strapi = (() => { libraries: ['start', 'router'] as const, status: 'active' as const, score: 0.069, + tier: 'bronze' as const, href, brandColor: '#4945FF', tagline: 'Headless CMS', @@ -936,6 +1019,7 @@ const powerSync = (() => { status: 'active' as const, startDate: 'Jan 2026', score: 0.143, + tier: 'bronze' as const, href, tagline: 'Offline-first Sync', applicationStarterPromptInstructions: [ @@ -946,6 +1030,7 @@ const powerSync = (() => { image: { light: powersyncBlackSvg, dark: powersyncWhiteSvg, + scale: 1.2, }, llmDescription: 'Sync engine that keeps backend databases in sync with embedded client-side SQLite for offline-first and realtime applications. Postgres and MongoDB are supported, with MySQL and SQL Server in beta.', @@ -974,6 +1059,7 @@ const railway = (() => { libraries: libraries.map((l) => l.id), status: 'active' as const, score: 0.145, + tier: 'bronze' as const, href, brandColor: '#0B0D0E', tagline: 'Instant Deployment', @@ -1010,6 +1096,7 @@ const openRouter = (() => { status: 'active' as const, startDate: 'Mar 2026', score: 0.344, + tier: 'silver' as const, brandColor: '#7C3AED', tagline: 'Unified LLM API', applicationStarterPromptInstructions: [ @@ -1020,6 +1107,7 @@ const openRouter = (() => { image: { light: openrouterBlackSvg, dark: openrouterWhiteSvg, + scale: 1.25, }, llmDescription: 'Unified API for accessing hundreds of AI models from dozens of providers through a single OpenAI-compatible endpoint, with routing, fallbacks, and privacy controls.', @@ -1070,30 +1158,6 @@ export const partners: Partner[] = [ speakeasy, ] as Partner[] -const applicationStarterPartnerTierOrder = new Map< - string, - { sortOrder: number; tier: ApplicationStarterPartnerTier } ->([ - ['coderabbit', { tier: 1, sortOrder: 0 }], - ['cloudflare', { tier: 1, sortOrder: 1 }], - ['aggrid', { tier: 2, sortOrder: 0 }], - ['supabase', { tier: 2, sortOrder: 1 }], - ['serpapi', { tier: 2, sortOrder: 2 }], - ['netlify', { tier: 2, sortOrder: 3 }], - ['openrouter', { tier: 2, sortOrder: 4 }], - ['workos', { tier: 2, sortOrder: 5 }], - ['clerk', { tier: 2, sortOrder: 6 }], - ['electric', { tier: 2, sortOrder: 7 }], - ['railway', { tier: 2, sortOrder: 8 }], - ['sentry', { tier: 3, sortOrder: 0 }], - ['prisma', { tier: 3, sortOrder: 1 }], - ['powersync', { tier: 3, sortOrder: 2 }], - ['neon', { tier: 3, sortOrder: 3 }], - ['strapi', { tier: 3, sortOrder: 4 }], - ['unkey', { tier: 3, sortOrder: 5 }], - ['uidev', { tier: 3, sortOrder: 6 }], -]) - const applicationStarterBrandColorOverrides = new Map([ ['powersync', '#00D5FF'], ['prisma', '#10B981'], @@ -1193,19 +1257,9 @@ function normalizeApplicationStarterPartnerKey(value: string) { } function getApplicationStarterPartnerTier( - partner: Pick, -) { - return ( - applicationStarterPartnerTierOrder.get( - normalizeApplicationStarterPartnerKey(partner.id), - ) ?? - applicationStarterPartnerTierOrder.get( - normalizeApplicationStarterPartnerKey(partner.name), - ) ?? { - tier: 3 as const, - sortOrder: Number.MAX_SAFE_INTEGER, - } - ) + partner: Pick, +): ApplicationStarterPartnerTier { + return partner.tier ? partnerTierToBuilderTier[partner.tier] : 3 } function getApplicationStarterPartnerFaviconUrl(href: string) { @@ -1294,14 +1348,13 @@ export function getInferredApplicationStarterPartnerIdsFromUserInput( const applicationStarterPartnerSuggestions: Array = [...partners] .filter((partner) => partner.status === 'active') - .filter((partner) => partner.id !== 'fireship' && partner.id !== 'nozzle') .map((partner) => { - const tierConfig = getApplicationStarterPartnerTier(partner) + const tier = getApplicationStarterPartnerTier(partner) const normalizedPartnerKey = normalizeApplicationStarterPartnerKey( partner.id, ) const iconMode: ApplicationStarterPartnerSuggestion['iconMode'] = - tierConfig.tier === 2 + tier === 2 ? (partner.applicationStarterIcon?.mode ?? 'contain') : undefined @@ -1312,7 +1365,7 @@ const applicationStarterPartnerSuggestions: Array partner) From 3ed2e75f7ca37ba298c87e16f2ea04fd73ec3178 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 27 Apr 2026 23:34:35 -0600 Subject: [PATCH 2/7] fix(partners): avoid 2-col collision between gold and silver at sm At the sm breakpoint range (640-767px), both gold and silver were rendering as 2-col, making them visually identical in column count. Skip the 2-col phase for silver (jumps from 1-col to 3-col at sm) and likewise bump bronze from 3-col to 4-col so silver and bronze stay distinct too. Final cascade: - Gold: 1 / 2 / 2 / 2 / 2 (xs / sm / md / lg / xl) - Silver: 1 / 3 / 3 / 4 / 4 - Bronze: 2 / 4 / 4 / 5 / 6 --- src/components/PartnersGrid.tsx | 60 ++++++++++++++++----------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/components/PartnersGrid.tsx b/src/components/PartnersGrid.tsx index cf9650d5c..59d0616a7 100644 --- a/src/components/PartnersGrid.tsx +++ b/src/components/PartnersGrid.tsx @@ -36,14 +36,14 @@ const tierLayout: Record< padding: 'p-12', }, silver: { - flexBasis: 'basis-full sm:basis-1/2 md:basis-1/3 lg:basis-1/4', + flexBasis: 'basis-full sm:basis-1/3 lg:basis-1/4', minHeight: 'min-h-[130px]', logoMaxWidth: 'max-w-[180px]', logoMaxHeight: 'max-h-[56px]', padding: 'p-6', }, bronze: { - flexBasis: 'basis-1/2 sm:basis-1/3 md:basis-1/4 lg:basis-1/5 xl:basis-1/6', + flexBasis: 'basis-1/2 sm:basis-1/4 lg:basis-1/5 xl:basis-1/6', minHeight: 'min-h-[100px]', logoMaxWidth: 'max-w-[110px]', logoMaxHeight: 'max-h-[36px]', @@ -137,35 +137,35 @@ export function PartnersGrid({ {tiersWithPartners.map((row) => { const flare = partnerTierFlares[row.tier] return ( - -
-
- {flare.icon} - - {partnerTierLabels[row.tier]} - + +
+
+ {flare.icon} + + {partnerTierLabels[row.tier]} + +
+
+ {row.partners.map((partner) => { + const index = slotIndex++ + return ( + + ) + })} +
-
- {row.partners.map((partner) => { - const index = slotIndex++ - return ( - - ) - })} -
-
- + ) })}
From aba97fb70da95022582d0294d58b1a3fde61b618 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 27 Apr 2026 23:35:06 -0600 Subject: [PATCH 3/7] style: apply oxfmt formatting --- src/components/RightRail.tsx | 4 +--- src/utils/partners.tsx | 14 ++++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/RightRail.tsx b/src/components/RightRail.tsx index 041a2482a..f2973c512 100644 --- a/src/components/RightRail.tsx +++ b/src/components/RightRail.tsx @@ -110,9 +110,7 @@ export function PartnersRail({ .sort((a, b) => b.score - a.score), })) .filter((row) => row.partners.length > 0) - .sort( - (a, b) => partnerTierOrder[a.tier] - partnerTierOrder[b.tier], - ) + .sort((a, b) => partnerTierOrder[a.tier] - partnerTierOrder[b.tier]) let slotIndex = 0 diff --git a/src/utils/partners.tsx b/src/utils/partners.tsx index 736b91ff2..56be336df 100644 --- a/src/utils/partners.tsx +++ b/src/utils/partners.tsx @@ -74,12 +74,14 @@ export const partnerTierOrder: Record = { bronze: 2, } -const partnerTierToBuilderTier: Record = - { - gold: 1, - silver: 2, - bronze: 3, - } +const partnerTierToBuilderTier: Record< + PartnerTier, + ApplicationStarterPartnerTier +> = { + gold: 1, + silver: 2, + bronze: 3, +} export const partnerTierFlares: Record< PartnerTier, From ec3d67a3bc095d390bbe9115e38c3f227c10b4f6 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 28 Apr 2026 10:42:14 -0600 Subject: [PATCH 4/7] feat(partners): restore rail logo color on rail hover, not per-cell Switch from per-cell `group-hover` to a named `group/rail` on the PartnersRail outer container. Hovering anywhere within the rail now restores all logos to full color and brightness in one motion, rather than only the cell directly under the cursor. --- src/components/RightRail.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/RightRail.tsx b/src/components/RightRail.tsx index f2973c512..92551065a 100644 --- a/src/components/RightRail.tsx +++ b/src/components/RightRail.tsx @@ -115,7 +115,7 @@ export function PartnersRail({ let slotIndex = 0 return ( -
+
From 654dfc407b7d89806c23eafda304d82c4e39a198 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 28 Apr 2026 10:43:42 -0600 Subject: [PATCH 5/7] style(partners): slow rail logo color transition to 500ms --- src/components/RightRail.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/RightRail.tsx b/src/components/RightRail.tsx index 92551065a..92bca950e 100644 --- a/src/components/RightRail.tsx +++ b/src/components/RightRail.tsx @@ -223,7 +223,7 @@ function PartnersRailItem({ >
From 9b505f8191b90ce5b82e03610708e0a9a21f2324 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 28 Apr 2026 10:54:57 -0600 Subject: [PATCH 6/7] feat(partners): apply tier grouping to partners directory page Active partners on /partners now render as three sections (Gold, Silver, Bronze) with section dividers and a centered tier-color pill header. Each tier uses its own card sizing tailored for the dedicated partners page (Gold = 2-up large cards with full description, Silver = 3-up medium with description, Bronze = 4-up compact, name + tagline). When a library filter or the previous-partners view is active, the flat 3-col grid is preserved since tier grouping doesn't apply. Also: - Drop the Lifetime Support Share section and its NetlifyImage import --- src/routes/partners.index.tsx | 263 ++++++++++++++++++++++++---------- 1 file changed, 191 insertions(+), 72 deletions(-) diff --git a/src/routes/partners.index.tsx b/src/routes/partners.index.tsx index 511a983ab..5da23379f 100644 --- a/src/routes/partners.index.tsx +++ b/src/routes/partners.index.tsx @@ -1,14 +1,20 @@ import { createFileRoute } from '@tanstack/react-router' import { Footer } from '~/components/Footer' import { Card } from '~/components/Card' -import { partners, PartnerImage } from '~/utils/partners' +import { + partners, + PartnerImage, + partnerTierFlares, + partnerTierLabels, + partnerTierOrder, + type PartnerTier, +} from '~/utils/partners' import { seo } from '~/utils/seo' import { Library } from '~/libraries' import { useState } from 'react' import * as React from 'react' import { ListFilter, X } from 'lucide-react' import { Button } from '~/ui' -import { NetlifyImage } from '~/components/NetlifyImage' import { startProject } from '~/libraries/start' import { routerProject } from '~/libraries/router' import { queryProject } from '~/libraries/query' @@ -301,16 +307,60 @@ function getFilteredPartners(search: PartnersSearch) { }) } +type CardSize = 'gold' | 'silver' | 'bronze' | 'flat' + +const cardSizeLayout: Record< + CardSize, + { + padding: string + logoFrame: string + logoMaxHeight: string + titleSize: string + showDescription: boolean + } +> = { + gold: { + padding: 'p-8', + logoFrame: 'h-32', + logoMaxHeight: 'max-h-24', + titleSize: 'text-2xl', + showDescription: true, + }, + silver: { + padding: 'p-6', + logoFrame: 'h-24', + logoMaxHeight: 'max-h-16', + titleSize: 'text-lg', + showDescription: true, + }, + bronze: { + padding: 'p-4', + logoFrame: 'h-16', + logoMaxHeight: 'max-h-10', + titleSize: 'text-sm', + showDescription: false, + }, + flat: { + padding: 'p-6', + logoFrame: 'h-24', + logoMaxHeight: 'max-h-16', + titleSize: 'text-xl', + showDescription: true, + }, +} + function PartnerDirectoryCard({ filters, isShowingPrevious, partner, slotIndex, + size = 'flat', }: { filters: PartnersSearch isShowingPrevious: boolean partner: (typeof partners)[number] slotIndex: number + size?: CardSize }) { const ref = useTrackedImpression({ event: 'partner_impression', @@ -329,6 +379,8 @@ function PartnerDirectoryCard({ ? `${partner.startDate} - ${partner.endDate}` : null + const layout = cardSizeLayout[size] + return ( -
-
- +
+
+
-

+

{partner.name}

{partner.tagline && ( -

+

{partner.tagline}

)} -
- {isShowingPrevious ? ( - <> - {duration && ( -

- {duration} -

- )} - {partner.libraries && partner.libraries.length > 0 && ( -
- {partner.libraries.map((library) => ( - - {library} - - ))} -
- )} - - ) : ( -

- {partner.llmDescription} -

- )} -
+ {layout.showDescription && ( +
+ {isShowingPrevious ? ( + <> + {duration && ( +

+ {duration} +

+ )} + {partner.libraries && partner.libraries.length > 0 && ( +
+ {partner.libraries.map((library) => ( + + {library} + + ))} +
+ )} + + ) : ( +

+ {partner.llmDescription} +

+ )} +
+ )}
) } +const tierGridCols: Record = { + gold: 'grid grid-cols-1 lg:grid-cols-2 gap-6', + silver: 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6', + bronze: 'grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4', +} + +function TierSectionHeader({ tier }: { tier: PartnerTier }) { + const flare = partnerTierFlares[tier] + return ( +
+
+
+ {flare.icon} + + {partnerTierLabels[tier]} + +
+
+
+ ) +} + +function TieredPartnerSections({ + partners: allPartners, + filters, +}: { + partners: Array<(typeof partners)[number]> + filters: PartnersSearch +}) { + const tiers: Array = ['gold', 'silver', 'bronze'] + + const sections = tiers + .map((tier) => ({ + tier, + partners: allPartners + .filter((partner) => (partner.tier ?? 'bronze') === tier) + .sort((a, b) => b.score - a.score), + })) + .filter((section) => section.partners.length > 0) + .sort( + (a, b) => partnerTierOrder[a.tier] - partnerTierOrder[b.tier], + ) + + let slotIndex = 0 + + return ( +
+ {sections.map((section) => ( +
+ +
+ {section.partners.map((partner) => { + const index = slotIndex++ + return ( + + ) + })} +
+
+ ))} +
+ ) +} + function PartnersIndexPage() { const search = normalizePartnersSearch(Route.useSearch()) const navigate = Route.useNavigate() @@ -488,17 +627,24 @@ function PartnersIndexPage() {

)} -
- {filteredPartners.map((partner, slotIndex) => ( - - ))} -
+ {isShowingActive && !hasLibraryFilter ? ( + + ) : ( +
+ {filteredPartners.map((partner, slotIndex) => ( + + ))} +
+ )}
) : (
@@ -541,33 +687,6 @@ function PartnersIndexPage() {
-
- -

- Lifetime Support Share -

-
-

- This chart is a percentage-based visualization of the lifetime - support each partner has rendered to TanStack. It is updated every 6 - months. -

-
- -
-
From 674708ed5d70144aee8200fca4e5a50b5c6176a0 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:56:03 +0000 Subject: [PATCH 7/7] ci: apply automated fixes --- src/routes/partners.index.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/routes/partners.index.tsx b/src/routes/partners.index.tsx index 5da23379f..78cc7f7ca 100644 --- a/src/routes/partners.index.tsx +++ b/src/routes/partners.index.tsx @@ -408,9 +408,7 @@ function PartnerDirectoryCard({ alt={partner.name} />
-

+

{partner.name}

{partner.tagline && ( @@ -498,9 +496,7 @@ function TieredPartnerSections({ .sort((a, b) => b.score - a.score), })) .filter((section) => section.partners.length > 0) - .sort( - (a, b) => partnerTierOrder[a.tier] - partnerTierOrder[b.tier], - ) + .sort((a, b) => partnerTierOrder[a.tier] - partnerTierOrder[b.tier]) let slotIndex = 0 @@ -686,7 +682,6 @@ function PartnersIndexPage() { Get in Touch
-