-
- )
-}
-
function MerchMenuContent({
onNavigate,
variant,
@@ -1467,7 +1493,10 @@ function MerchMenuContent({
to="/shop"
onClick={onNavigate}
preload="intent"
- className="mx-auto inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 font-ds-mono text-ds-mono-xs uppercase tracking-wider text-text-secondary transition-colors hover:text-text-primary focus:text-text-primary focus:outline-none"
+ className={twMerge(
+ 'mx-auto inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 font-ds-mono text-ds-mono-xs uppercase tracking-wider text-text-secondary transition-colors hover:text-text-primary focus:text-text-primary focus:outline-none',
+ variant === 'mobile' && 'mobile-mega-menu-enter-item',
+ )}
>
View all
@@ -1492,7 +1521,7 @@ function MerchProductLink({
params={{ handle: product.handle }}
onClick={onNavigate}
preload="intent"
- className="group/merch block overflow-hidden rounded-xl border border-border-subtle bg-background-subtle transition-colors hover:border-border-strong focus:outline-none focus-visible:border-border-strong"
+ className="mobile-mega-menu-enter-item group/merch block overflow-hidden rounded-xl border border-border-subtle bg-background-subtle transition-colors hover:border-border-strong focus:outline-none focus-visible:border-border-strong"
title={`${product.title} · ${formatMoney(price.amount, price.currencyCode)}`}
>
@@ -1634,6 +1663,9 @@ function MenuItemLink({
}}
variant={variant}
compact={compact}
+ className={
+ variant === 'mobile' ? 'mobile-mega-menu-enter-item' : undefined
+ }
/>
)
}
@@ -1681,19 +1713,19 @@ function SocialStack() {
type="button"
aria-label="TanStack social channels"
title="Social channels"
- className="inline-flex h-9 items-center px-0"
+ className="group/social inline-flex h-8 items-center px-0"
>
{stackTop.map(({ label, Icon }, i) => (
0 && '-ml-3',
)}
style={{ zIndex: stackTop.length - i }}
>
-
+
))}
@@ -1708,7 +1740,7 @@ function SocialStack() {
rel="noopener noreferrer"
aria-label={`TanStack on ${label}`}
>
-
+
{label}
diff --git a/src/components/NavbarAuthControls.tsx b/src/components/NavbarAuthControls.tsx
index 01d0d13ca..ef3b652c6 100644
--- a/src/components/NavbarAuthControls.tsx
+++ b/src/components/NavbarAuthControls.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import { User } from '@phosphor-icons/react'
+import { SignIn } from '@phosphor-icons/react'
import { Link, useNavigate } from '@tanstack/react-router'
import { twMerge } from 'tailwind-merge'
import {
@@ -46,8 +46,8 @@ export function NavbarAuthControls({ className }: NavbarAuthControlsProps) {
className,
)}
>
-
Log In
+
)
diff --git a/src/components/NavbarCartButton.tsx b/src/components/NavbarCartButton.tsx
index 26d43da87..1ac831273 100644
--- a/src/components/NavbarCartButton.tsx
+++ b/src/components/NavbarCartButton.tsx
@@ -13,8 +13,8 @@ const badgeClasses = twMerge(
const buttonClasses = twMerge(
'relative flex items-center justify-center',
- 'h-9 w-9 rounded-lg transition-colors',
- 'hover:bg-gray-500/10 text-gray-700 dark:text-gray-300',
+ 'h-8 w-8 rounded-md border-0 p-0 shadow-none transition-colors',
+ 'text-icon-default hover:bg-surface-state-hover hover:text-text-primary',
)
/**
@@ -57,7 +57,7 @@ export function NavbarCartButton() {
aria-label={label}
className={buttonClasses}
>
-
+
{badge}
)
@@ -66,7 +66,7 @@ export function NavbarCartButton() {
// Everywhere else: navigate to the cart page
return (
-
+
{badge}
)
diff --git a/src/components/OpenSourceStats.tsx b/src/components/OpenSourceStats.tsx
index 44005cf8d..c07aa20eb 100644
--- a/src/components/OpenSourceStats.tsx
+++ b/src/components/OpenSourceStats.tsx
@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
-import { Download, Star, TrendUp } from '@phosphor-icons/react'
+import { CalendarDots, DownloadSimple, Star } from '@phosphor-icons/react'
import { type Library } from '~/libraries'
import {
homepageNpmStatsSummaryQuery,
@@ -97,7 +97,7 @@ export default function OssStats({
if (npmLoading || hasNpmDownloads) {
items.push({
key: 'total',
- icon:
,
+ icon:
,
value: hasNpmDownloads ? formatCompact(npmDownloads) : '',
placeholder: '00.0B',
label: 'Total Downloads',
@@ -107,7 +107,7 @@ export default function OssStats({
if (weeklyLoading || hasWeeklyDownloads) {
items.push({
key: 'weekly',
- icon:
,
+ icon:
,
value: hasWeeklyDownloads ? weeklyDownloads.toLocaleString() : '',
placeholder: '00,000,000',
label: 'Weekly Downloads',
diff --git a/src/components/PartnersSection.tsx b/src/components/PartnersSection.tsx
index cfbc91b7e..76544efe6 100644
--- a/src/components/PartnersSection.tsx
+++ b/src/components/PartnersSection.tsx
@@ -1,8 +1,9 @@
import * as React from 'react'
import { PartnersGrid } from './PartnersGrid'
import { PartnershipCallout } from './PartnershipCallout'
-import { Button } from '~/ui'
import { Link } from '@tanstack/react-router'
+import { ArrowRight } from '@phosphor-icons/react'
+import { Button } from '~/components/ds/ui'
type PartnersSectionProps = {
title?: string
@@ -22,7 +23,10 @@ export function PartnersSection({
{showPreviousLink ? (
-
+
) : null}
diff --git a/src/components/PartnersSponsorsSection.tsx b/src/components/PartnersSponsorsSection.tsx
index 2b7f91882..1cc4acd6c 100644
--- a/src/components/PartnersSponsorsSection.tsx
+++ b/src/components/PartnersSponsorsSection.tsx
@@ -62,8 +62,9 @@ export function PartnersSponsorsContent({
/>
-
diff --git a/src/components/SearchButton.tsx b/src/components/SearchButton.tsx
index c050a3530..69ef72edd 100644
--- a/src/components/SearchButton.tsx
+++ b/src/components/SearchButton.tsx
@@ -1,7 +1,7 @@
import * as React from 'react'
-import { Command, MagnifyingGlass } from '@phosphor-icons/react'
+import { Command, MagnifyingGlass, Sparkle } from '@phosphor-icons/react'
import { twMerge } from 'tailwind-merge'
-import { Button } from '~/ui'
+import { Button, Tooltip } from '~/ui'
import { useSearchContext } from '~/contexts/SearchContext'
interface SearchButtonProps {
@@ -9,16 +9,6 @@ interface SearchButtonProps {
iconOnly?: boolean
}
-function AiGlyph() {
- return (
-
-
- AI
-
-
- )
-}
-
export function SearchButton({
className,
iconOnly = false,
@@ -38,12 +28,15 @@ export function SearchButton({
title="Search"
className={twMerge(
iconOnly
- ? 'h-7 w-7 rounded-md p-0'
+ ? 'h-8 w-8 rounded-md border-0 p-0 text-icon-default shadow-none hover:bg-surface-state-hover hover:text-text-primary'
: 'gap-2 bg-gray-500/5 dark:bg-gray-500/30',
className,
)}
>
-
+
{iconOnly ? (
Search
) : (
@@ -109,26 +102,28 @@ export function AiDockButton({ className }: { className?: string }) {
}
return (
-
-
- Ask AI
-
+
+
+
+ Ask AI
+
+
)
}
diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx
index 0eb23bcf0..350527c36 100644
--- a/src/components/ThemeToggle.tsx
+++ b/src/components/ThemeToggle.tsx
@@ -22,7 +22,7 @@ export function ThemeToggle() {
const getIconClassName = (icon: typeof activeIcon) =>
[
- 'col-start-1 row-start-1 h-3.5 w-3.5 shrink-0 transition-opacity motion-reduce:transition-none',
+ 'col-start-1 row-start-1 size-[18px] shrink-0 transition-opacity motion-reduce:transition-none',
activeIcon === icon ? 'opacity-100' : 'opacity-0',
].join(' ')
@@ -35,15 +35,15 @@ export function ThemeToggle() {
onClick={handleToggleMode}
aria-label={`Theme: ${label}. Switch to ${nextLabel} mode.`}
title={`Theme: ${label}. Switch to ${nextLabel} mode.`}
- className="h-7 w-7 shrink-0 rounded-md p-0 leading-none"
+ className="h-8 w-8 shrink-0 rounded-md border-0 p-0 leading-none text-icon-default shadow-none hover:bg-surface-state-hover hover:text-text-primary"
>
-
-
-
+
+
+
)
diff --git a/src/components/application-builder/useApplicationBuilder.tsx b/src/components/application-builder/useApplicationBuilder.tsx
index 101bb5548..b93a4371c 100644
--- a/src/components/application-builder/useApplicationBuilder.tsx
+++ b/src/components/application-builder/useApplicationBuilder.tsx
@@ -235,7 +235,8 @@ export function useApplicationBuilder({
migrationRepositoryUrl,
)
const showMigrationRepositoryInput =
- isNextJsMigrationInput(input) || normalizedMigrationRepositoryUrl.length > 0
+ context !== 'home' &&
+ (isNextJsMigrationInput(input) || normalizedMigrationRepositoryUrl.length > 0)
const hasMigrationRepositoryUrlError =
normalizedMigrationRepositoryUrl.length > 0 &&
!isValidMigrationRepositoryUrl(normalizedMigrationRepositoryUrl)
diff --git a/src/components/ds/BrandAssets.tsx b/src/components/ds/BrandAssets.tsx
index 1f5edb7cf..2ec708be2 100644
--- a/src/components/ds/BrandAssets.tsx
+++ b/src/components/ds/BrandAssets.tsx
@@ -9,6 +9,16 @@ interface LogoAsset {
onDark: boolean
}
+interface GalleryAsset {
+ name: string
+ category: string
+ preview: string
+ alt: string
+ onDark: boolean
+ imgClass: string
+ downloads: Array<{ format: string; href: string }>
+}
+
const TONE_LABEL: Record
= {
black: 'Black',
charcoal: 'Charcoal',
@@ -36,94 +46,155 @@ const EMBLEM: Array = [
{ tone: 'white', file: 'tanstack-emblem-white.svg', onDark: true },
]
-function LogoCard({
- asset,
- lockup,
- imgClass,
-}: {
- asset: LogoAsset
- lockup: string
- imgClass: string
-}) {
- const src = `/images/brand/${asset.file}`
+function brandGalleryAssets(
+ lockup: string,
+ assets: Array,
+ imgClass: string,
+): Array {
+ return assets.map((asset) => {
+ const src = `/images/brand/${asset.file}`
+ const tone = TONE_LABEL[asset.tone]
+
+ return {
+ name: `${lockup} · ${tone}`,
+ category: 'Brand logo',
+ preview: src,
+ alt: `TanStack ${lockup.toLowerCase()} logo — ${tone}`,
+ onDark: asset.onDark,
+ imgClass,
+ downloads: [{ format: 'SVG', href: src }],
+ }
+ })
+}
+
+const SOCIAL_LOGOS: Array = [
+ ['Mark · Dark', 'mark-dark'],
+ ['Mark · Light', 'mark-light'],
+ ['Stacked · Dark', 'stacked-dark'],
+ ['Stacked · Light', 'stacked-light'],
+].map(([name, file]) => ({
+ name,
+ category: 'Social logo',
+ preview: `/images/brand/social/${file}.svg`,
+ alt: `TanStack social logo — ${name}`,
+ onDark: false,
+ imgClass: 'h-full w-full',
+ downloads: [
+ { format: 'SVG', href: `/images/brand/social/${file}.svg` },
+ { format: 'PNG · 2×', href: `/images/brand/social/${file}@2x.png` },
+ ],
+}))
+
+const FAVICONS: Array = [
+ {
+ name: 'Favicon · Light mode',
+ category: 'Favicon',
+ preview: '/favicon-light.svg',
+ alt: 'TanStack favicon for light browser themes',
+ onDark: false,
+ imgClass: 'h-24 w-24',
+ downloads: [{ format: 'SVG', href: '/favicon-light.svg' }],
+ },
+ {
+ name: 'Favicon · Dark mode',
+ category: 'Favicon',
+ preview: '/favicon-dark.svg',
+ alt: 'TanStack favicon for dark browser themes',
+ onDark: true,
+ imgClass: 'h-24 w-24',
+ downloads: [{ format: 'SVG', href: '/favicon-dark.svg' }],
+ },
+]
+
+const BRAND_LOGO_FORMATS = [
+ {
+ name: 'Stacked',
+ assets: brandGalleryAssets('Stacked', STACKED, 'h-20 max-w-full'),
+ },
+ {
+ name: 'Landscape',
+ assets: brandGalleryAssets('Landscape', LANDSCAPE, 'h-10 max-w-full'),
+ },
+ {
+ name: 'Emblem',
+ assets: brandGalleryAssets('Emblem', EMBLEM, 'h-16 max-w-full'),
+ },
+]
+function AssetCard({ asset }: { asset: GalleryAsset }) {
return (
-
+
-

+

+
-
-
- {TONE_LABEL[asset.tone]}
-
-
-
- SVG
-
+
+
+ {asset.name}
+
+
+ {asset.category}
+
-
+
)
}
-export function CurrentBrandAssets() {
+export function BrandAssetGallery() {
return (
<>
-
-
- {STACKED.map((asset) => (
-
- ))}
-
-
+
+ {BRAND_LOGO_FORMATS.map((format) => (
+
+
+ {format.name}
+
+
+ {format.assets.map((asset) => (
+
+ ))}
+
+
+ ))}
+
-
- {LANDSCAPE.map((asset) => (
-
+
+ {FAVICONS.map((asset) => (
+
))}
-
- {EMBLEM.map((asset) => (
-
+
+ {SOCIAL_LOGOS.map((asset) => (
+
))}
@@ -140,3 +211,5 @@ export function CurrentBrandAssets() {
>
)
}
+
+export { BrandAssetGallery as CurrentBrandAssets }
diff --git a/src/components/ds/DsKit.tsx b/src/components/ds/DsKit.tsx
index 33877c0c3..e4fe7d1b4 100644
--- a/src/components/ds/DsKit.tsx
+++ b/src/components/ds/DsKit.tsx
@@ -3,6 +3,31 @@ import { twMerge } from 'tailwind-merge'
import { Check, CaretDown, Code, Copy } from '@phosphor-icons/react'
import { copyTextToClipboard } from '~/utils/browser-effects'
+const descriptionStyles = {
+ page: 'mt-3 max-w-2xl text-ds-body-md text-text-secondary',
+ section: 'mt-1 max-w-2xl text-ds-body-sm text-text-secondary',
+ preview: 'truncate text-ds-body-sm text-text-muted',
+} as const
+
+/** Semantic explanatory copy used throughout the design-system catalog. */
+export function DsDescription({
+ children,
+ className,
+ role,
+}: {
+ children: React.ReactNode
+ className?: string
+ role: keyof typeof descriptionStyles
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
/**
* Presentational building blocks for the Design System pages (`/ds`).
*
@@ -28,9 +53,7 @@ export function DsPage({
{title}
{description ? (
-
- {description}
-
+
{description}
) : null}
{children}
@@ -54,9 +77,7 @@ export function DsSection({
{title}
{description ? (
-
- {description}
-
+
{description}
) : null}
{children}
@@ -110,9 +131,7 @@ export function ComponentPreview({
) : null}
{description ? (
-
- {description}
-
+ {description}
) : null}
{code ? (
diff --git a/src/components/ds/ds-nav.ts b/src/components/ds/ds-nav.ts
index 766c0a641..45a443bd6 100644
--- a/src/components/ds/ds-nav.ts
+++ b/src/components/ds/ds-nav.ts
@@ -45,6 +45,7 @@ export const dsNav: Array = [
{ label: 'Inputs', to: '/ds/inputs' },
{ label: 'Dropdown', to: '/ds/dropdown' },
{ label: 'Avatar', to: '/ds/avatar' },
+ { label: 'Maintainers', to: '/ds/maintainers' },
{ label: 'Spinner', to: '/ds/spinner' },
{ label: 'Collapsible', to: '/ds/collapsible' },
{ label: 'Breadcrumbs', to: '/ds/breadcrumbs' },
diff --git a/src/components/ds/ui/BlogPostCard.tsx b/src/components/ds/ui/BlogPostCard.tsx
new file mode 100644
index 000000000..e897c5922
--- /dev/null
+++ b/src/components/ds/ui/BlogPostCard.tsx
@@ -0,0 +1,63 @@
+import { Link } from '@tanstack/react-router'
+import { twMerge } from 'tailwind-merge'
+import { CoverFallback } from '~/components/CoverFallback'
+import { formatAuthors, formatPublishedDate } from '~/utils/blog-format'
+import type { RecentPost } from '~/utils/blog.functions'
+import { getOptimizedImageUrl } from '~/utils/optimizedImage'
+
+export function BlogPostCard({
+ className,
+ onNavigate,
+ post,
+}: {
+ className?: string
+ onNavigate?: () => void
+ post: RecentPost
+}) {
+ return (
+
+ {post.headerImage ? (
+
+

+
+ ) : (
+
+ )}
+
+
+ {post.title}
+
+
+ {post.excerpt}
+
+
+ {formatAuthors(post.authors)} · {formatPublishedDate(post.published)}
+
+
+
+ )
+}
diff --git a/src/components/ds/ui/StatsSection.tsx b/src/components/ds/ui/StatsSection.tsx
index 6f333d125..5e488017e 100644
--- a/src/components/ds/ui/StatsSection.tsx
+++ b/src/components/ds/ui/StatsSection.tsx
@@ -201,25 +201,25 @@ function HeroStat({ iconTop, stat }: { iconTop: boolean; stat: StatItem }) {
return (
{stat.icon ? (
-
+
{stat.icon}
) : null}
-
+
{stat.value}
-
+
{stat.label}
@@ -269,7 +269,7 @@ export function StatsSection({
className={twMerge(
layout === 'stacked'
? 'flex flex-col items-start gap-5'
- : 'flex flex-wrap items-center gap-x-8 gap-y-5',
+ : 'flex flex-col items-center gap-4 md:flex-row md:flex-wrap md:justify-center md:gap-x-3.5 md:gap-y-5 xl:gap-x-9',
className,
)}
>
diff --git a/src/components/ds/ui/index.tsx b/src/components/ds/ui/index.tsx
index ce144ea44..af7a951d8 100644
--- a/src/components/ds/ui/index.tsx
+++ b/src/components/ds/ui/index.tsx
@@ -25,6 +25,7 @@ type ButtonVariant =
| 'ghost'
| 'icon'
| 'link'
+ | 'subtle-link'
| 'gradient'
type ButtonColor =
| 'neutral'
@@ -68,46 +69,59 @@ type ButtonInnerProps = ButtonOwnProps & Record
// near-black on light, white on dark — via the inverse semantic tokens.
const primaryColorStyles: Record = {
neutral:
- 'bg-background-inverse text-text-inverse border-background-inverse hover:bg-background-inverse/90',
- blue: 'bg-ds-blue-500 text-white border-ds-blue-500 hover:bg-ds-blue-400',
- green: 'bg-ds-green-400 text-white border-ds-green-400 hover:bg-ds-green-300',
- red: 'bg-ds-terracotta-400 text-white border-ds-terracotta-400 hover:bg-ds-terracotta-300',
+ 'bg-background-inverse text-text-inverse border-background-inverse hover:bg-background-inverse/90 max-[899px]:bg-background-inverse/90',
+ blue: 'bg-ds-blue-500 text-white border-ds-blue-500 hover:bg-ds-blue-400 max-[899px]:bg-ds-blue-400',
+ green:
+ 'bg-ds-green-400 text-white border-ds-green-400 hover:bg-ds-green-300 max-[899px]:bg-ds-green-300',
+ red: 'bg-ds-terracotta-400 text-white border-ds-terracotta-400 hover:bg-ds-terracotta-300 max-[899px]:bg-ds-terracotta-300',
orange:
- 'bg-ds-terracotta-300 text-white border-ds-terracotta-300 hover:bg-ds-terracotta-200',
+ 'bg-ds-terracotta-300 text-white border-ds-terracotta-300 hover:bg-ds-terracotta-200 max-[899px]:bg-ds-terracotta-200',
purple:
- 'bg-ds-purple-400 text-white border-ds-purple-400 hover:bg-ds-purple-300',
- gray: 'bg-ds-neutral-400 text-white border-ds-neutral-400 hover:bg-ds-neutral-300',
+ 'bg-ds-purple-400 text-white border-ds-purple-400 hover:bg-ds-purple-300 max-[899px]:bg-ds-purple-300',
+ gray: 'bg-ds-neutral-400 text-white border-ds-neutral-400 hover:bg-ds-neutral-300 max-[899px]:bg-ds-neutral-300',
emerald:
- 'bg-ds-green-400 text-white border-ds-green-400 hover:bg-ds-green-300',
- cyan: 'bg-lib-start text-white border-lib-start hover:bg-lib-start/90',
+ 'bg-ds-green-400 text-white border-ds-green-400 hover:bg-ds-green-300 max-[899px]:bg-ds-green-300',
+ cyan: 'bg-lib-start text-white border-lib-start hover:bg-lib-start/90 max-[899px]:bg-lib-start/90',
yellow:
- 'bg-ds-amber-300 text-ds-neutral-500 border-ds-amber-300 hover:bg-ds-amber-200',
+ 'bg-ds-amber-300 text-ds-neutral-500 border-ds-amber-300 hover:bg-ds-amber-200 max-[899px]:bg-ds-amber-200',
}
const iconColorStyles: Record = {
- neutral: 'text-text-primary hover:bg-background-subtle',
- blue: 'text-ds-blue-500 hover:bg-ds-blue-500/10',
- green: 'text-ds-green-400 hover:bg-ds-green-400/10',
- red: 'text-ds-terracotta-400 hover:bg-ds-terracotta-400/10',
- orange: 'text-ds-terracotta-300 hover:bg-ds-terracotta-300/10',
- purple: 'text-ds-purple-400 hover:bg-ds-purple-400/10',
- gray: 'text-text-muted hover:bg-background-subtle',
- emerald: 'text-ds-green-400 hover:bg-ds-green-400/10',
- cyan: 'text-lib-start hover:bg-lib-start/10',
- yellow: 'text-ds-amber-400 hover:bg-ds-amber-400/10',
+ neutral:
+ 'text-text-primary hover:bg-surface-state-hover max-[899px]:bg-surface-state-hover',
+ blue: 'text-ds-blue-500 hover:bg-ds-blue-500/10 max-[899px]:bg-ds-blue-500/10',
+ green:
+ 'text-ds-green-400 hover:bg-ds-green-400/10 max-[899px]:bg-ds-green-400/10',
+ red: 'text-ds-terracotta-400 hover:bg-ds-terracotta-400/10 max-[899px]:bg-ds-terracotta-400/10',
+ orange:
+ 'text-ds-terracotta-300 hover:bg-ds-terracotta-300/10 max-[899px]:bg-ds-terracotta-300/10',
+ purple:
+ 'text-ds-purple-400 hover:bg-ds-purple-400/10 max-[899px]:bg-ds-purple-400/10',
+ gray: 'text-text-muted hover:bg-surface-state-hover max-[899px]:bg-surface-state-hover max-[899px]:text-text-primary',
+ emerald:
+ 'text-ds-green-400 hover:bg-ds-green-400/10 max-[899px]:bg-ds-green-400/10',
+ cyan: 'text-lib-start hover:bg-lib-start/10 max-[899px]:bg-lib-start/10',
+ yellow:
+ 'text-ds-amber-400 hover:bg-ds-amber-400/10 max-[899px]:bg-ds-amber-400/10',
}
const linkColorStyles: Record = {
- neutral: 'text-text-primary hover:text-text-primary/70',
- blue: 'text-ds-blue-500 hover:text-ds-blue-400',
- green: 'text-ds-green-400 hover:text-ds-green-300',
- red: 'text-ds-terracotta-400 hover:text-ds-terracotta-300',
- orange: 'text-ds-terracotta-300 hover:text-ds-terracotta-200',
- purple: 'text-ds-purple-400 hover:text-ds-purple-300',
- gray: 'text-text-secondary hover:text-text-primary',
- emerald: 'text-ds-green-400 hover:text-ds-green-300',
- cyan: 'text-lib-start hover:text-lib-start/80',
- yellow: 'text-ds-amber-400 hover:text-ds-amber-300',
+ neutral:
+ 'text-text-primary hover:text-text-primary/70 max-[899px]:text-text-primary/70',
+ blue: 'text-ds-blue-500 hover:text-ds-blue-400 max-[899px]:text-ds-blue-400',
+ green:
+ 'text-ds-green-400 hover:text-ds-green-300 max-[899px]:text-ds-green-300',
+ red: 'text-ds-terracotta-400 hover:text-ds-terracotta-300 max-[899px]:text-ds-terracotta-300',
+ orange:
+ 'text-ds-terracotta-300 hover:text-ds-terracotta-200 max-[899px]:text-ds-terracotta-200',
+ purple:
+ 'text-ds-purple-400 hover:text-ds-purple-300 max-[899px]:text-ds-purple-300',
+ gray: 'text-text-secondary hover:text-text-primary max-[899px]:text-text-primary',
+ emerald:
+ 'text-ds-green-400 hover:text-ds-green-300 max-[899px]:text-ds-green-300',
+ cyan: 'text-lib-start hover:text-lib-start/80 max-[899px]:text-lib-start/80',
+ yellow:
+ 'text-ds-amber-400 hover:text-ds-amber-300 max-[899px]:text-ds-amber-300',
}
// Gradient (landing-CTA) colors. Each color feeds the four --btn-grad-* vars
@@ -137,18 +151,20 @@ const gradientColorStyles: Record = {
const variantStyles: Record = {
primary:
- 'border font-medium shadow-[0_1px_2px_0_rgba(0,0,0,0.12),inset_0_1px_0_0_rgba(255,255,255,0.18)] hover:-translate-y-px hover:shadow-[0_6px_16px_-4px_rgba(0,0,0,0.28),inset_0_1px_0_0_rgba(255,255,255,0.25)] active:translate-y-0 active:shadow-[0_1px_2px_0_rgba(0,0,0,0.12)]',
+ 'border font-medium shadow-[0_1px_2px_0_rgba(0,0,0,0.12),inset_0_1px_0_0_rgba(255,255,255,0.18)] hover:-translate-y-px hover:shadow-[0_6px_16px_-4px_rgba(0,0,0,0.28),inset_0_1px_0_0_rgba(255,255,255,0.25)] max-[899px]:-translate-y-px max-[899px]:shadow-[0_6px_16px_-4px_rgba(0,0,0,0.28),inset_0_1px_0_0_rgba(255,255,255,0.25)] active:translate-y-0 active:shadow-[0_1px_2px_0_rgba(0,0,0,0.12)]',
secondary:
- 'bg-action-secondary text-text-primary hover:bg-action-secondary-hover border-transparent font-medium shadow-sm hover:-translate-y-px hover:shadow-md active:translate-y-0',
+ 'bg-action-secondary text-text-primary hover:bg-action-secondary-hover max-[899px]:bg-action-secondary-hover border-transparent font-medium shadow-sm hover:-translate-y-px hover:shadow-md max-[899px]:-translate-y-px max-[899px]:shadow-md active:translate-y-0',
ghost:
- 'border border-border-default text-text-primary hover:bg-background-subtle hover:border-border-strong font-medium hover:shadow-sm',
+ 'border border-border-default text-text-primary hover:bg-background-subtle hover:border-border-strong max-[899px]:bg-background-subtle max-[899px]:border-border-strong font-medium hover:shadow-sm max-[899px]:shadow-sm',
icon: 'border-transparent active:scale-90',
- link: 'border-transparent font-medium underline-offset-2 hover:underline',
+ link: 'border-transparent font-medium underline-offset-2 hover:underline max-[899px]:underline',
+ 'subtle-link':
+ 'border-transparent font-ds-mono uppercase tracking-wider no-underline hover:no-underline [&>svg:last-child]:size-3.5 [&>svg:last-child]:transition-transform hover:[&>svg:last-child]:translate-x-0.5 max-[899px]:[&>svg:last-child]:translate-x-0.5 motion-reduce:[&>svg:last-child]:transition-none',
// Library-landing primary CTA: accent→bright gradient with an inner highlight,
// a colored glow, ink text, and a hover lift. Colors come from
// gradientColorStyles (the --btn-grad-* vars).
gradient:
- 'border-transparent font-medium text-[var(--btn-grad-ink)] [background-image:linear-gradient(105deg,var(--btn-grad-accent),var(--btn-grad-bright))] shadow-[inset_-5px_-5px_7px_-5px_var(--btn-grad-tint),0_12px_35px_-6px_rgb(var(--btn-grad-glow)/0.35)] transition-[transform,box-shadow] duration-150 hover:-translate-y-0.5 hover:shadow-[inset_-5px_-5px_7px_-5px_var(--btn-grad-tint),0_18px_45px_-6px_rgb(var(--btn-grad-glow)/0.5)] active:translate-y-0 focus-visible:ring-[var(--btn-grad-bright)]',
+ 'border-transparent font-medium text-[var(--btn-grad-ink)] [background-image:linear-gradient(105deg,var(--btn-grad-accent),var(--btn-grad-bright))] shadow-[inset_-5px_-5px_7px_-5px_var(--btn-grad-tint),0_12px_35px_-6px_rgb(var(--btn-grad-glow)/0.35)] transition-[transform,box-shadow] duration-150 hover:-translate-y-0.5 hover:shadow-[inset_-5px_-5px_7px_-5px_var(--btn-grad-tint),0_18px_45px_-6px_rgb(var(--btn-grad-glow)/0.5)] max-[899px]:-translate-y-0.5 max-[899px]:shadow-[inset_-5px_-5px_7px_-5px_var(--btn-grad-tint),0_18px_45px_-6px_rgb(var(--btn-grad-glow)/0.5)] active:translate-y-0 focus-visible:ring-[var(--btn-grad-bright)]',
}
const sizeStyles: Record = {
@@ -205,7 +221,7 @@ export const Button: ButtonComponent = React.forwardRef<
? primaryColorStyles[color]
: variant === 'icon'
? iconColorStyles[color]
- : variant === 'link'
+ : variant === 'link' || variant === 'subtle-link'
? linkColorStyles[color]
: variant === 'gradient'
? gradientColorStyles[color]
@@ -240,6 +256,7 @@ type BadgeVariant =
| 'purple'
| 'teal'
| 'orange'
+type BadgeRounded = 'md' | 'full'
const badgeVariantStyles: Record = {
default: 'bg-background-subtle text-text-secondary',
@@ -255,16 +272,19 @@ const badgeVariantStyles: Record = {
export function Badge({
children,
variant = 'default',
+ rounded = 'full',
className,
}: {
children: React.ReactNode
variant?: BadgeVariant
+ rounded?: BadgeRounded
className?: string
}) {
return (
-
-
- {Array.from({ length: 6 }).map((_, index) => (
+
+
+ {Array.from({ length: 10 }).map((_, index) => (
))}
@@ -46,14 +47,20 @@ function HomeCommunityContent() {
Core Maintainers
-
+
{coreMaintainers.map((maintainer) => (
))}
-
+
View All Maintainers
+
diff --git a/src/components/home/HomeNewsletterSection.tsx b/src/components/home/HomeNewsletterSection.tsx
index b26f93c4e..c2bbbafc1 100644
--- a/src/components/home/HomeNewsletterSection.tsx
+++ b/src/components/home/HomeNewsletterSection.tsx
@@ -1,35 +1,44 @@
-import { Card } from '~/components/ds/ui'
+import { EnvelopeSimple } from '@phosphor-icons/react'
import { NewsletterSignup } from '~/components/NewsletterSignup'
import { Footer } from '~/components/Footer'
export function HomeNewsletterSection() {
return (
<>
-
-
-
-
-
+
+
+
+
+
+
+
+
+ Newsletter
+
+
+ Subscribe to TanStack News
+
+
+ New posts, releases, and ecosystem updates from TanStack.
+
-
-
- New posts, releases, and ecosystem updates from TanStack.
-
+
-
-
-
+
+
+
>
)
diff --git a/src/components/home/HomeSocialProofSection.tsx b/src/components/home/HomeSocialProofSection.tsx
index ad3b38b4c..18237c772 100644
--- a/src/components/home/HomeSocialProofSection.tsx
+++ b/src/components/home/HomeSocialProofSection.tsx
@@ -2,9 +2,9 @@ import { Link } from '@tanstack/react-router'
import { Hydrate } from '@tanstack/react-start'
import { visible } from '@tanstack/react-start/hydration'
import { ArrowRight } from '@phosphor-icons/react'
-import { Button, Card } from '~/components/ds/ui'
+import { BlogPostCard } from '~/components/ds/ui/BlogPostCard'
+import { Button } from '~/components/ds/ui'
import { PartnersSponsorsContent } from '~/components/PartnersSponsorsSection'
-import { formatAuthors, formatPublishedDate } from '~/utils/blog-format'
import type { RecentPost } from '~/utils/blog.functions'
type HomeSocialProofSectionProps = {
@@ -54,12 +54,12 @@ function SocialProofSkeleton() {
{Array.from({ length: 3 }).map((_, index) => (
-
-
-
+
+
@@ -67,7 +67,7 @@ function SocialProofSkeleton() {
-
+
))}
@@ -82,73 +82,26 @@ function HomeSocialProofContent({ recentPosts }: HomeSocialProofSectionProps) {
{recentPosts.length > 0 && (
-
+
-
- {recentPosts.map(
- ({ slug, title, published, excerpt, headerImage, authors }) => {
- return (
- // DS Card isn't polymorphic, so the Link wraps it and drives
- // the hover via group-hover.
-
-
- {headerImage ? (
-
-

-
- ) : null}
-
-
-
{title}
-
- by {formatAuthors(authors)}
- {published ? (
-
- ) : null}
-
- {excerpt ? (
-
- {excerpt}
-
- ) : null}
-
-
- Read More →
-
-
-
-
- )
- },
- )}
+
+ {recentPosts.map((post) => (
+
+ ))}
diff --git a/src/components/home/HomeStatsSection.tsx b/src/components/home/HomeStatsSection.tsx
index 4faae2e0c..244ccc8bf 100644
--- a/src/components/home/HomeStatsSection.tsx
+++ b/src/components/home/HomeStatsSection.tsx
@@ -1,5 +1,5 @@
import OpenSourceStats from '~/components/OpenSourceStats'
export function HomeStatsSection({ className }: { className?: string }) {
- return
+ return
}
diff --git a/src/components/landing/DevtoolsLanding.tsx b/src/components/landing/DevtoolsLanding.tsx
index ce8db4ac1..935f7b6f1 100644
--- a/src/components/landing/DevtoolsLanding.tsx
+++ b/src/components/landing/DevtoolsLanding.tsx
@@ -168,25 +168,25 @@ function DevtoolsCockpit() {
-
+
your app
setIsInspecting((current) => !current)}
>
Inspect
-
+
selectSource('src/components/AppHeader.tsx:18')}
>
@@ -204,7 +204,7 @@ function DevtoolsCockpit() {
className={
isInspecting
? 'rounded-lg border border-[var(--landing-accent)] bg-[color:rgb(var(--landing-glow)/0.08)] px-3 py-3 text-left text-ds-body-xs text-text-primary/70 hover:bg-[color:rgb(var(--landing-glow)/0.18)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--landing-accent-bright)]'
- : 'rounded-lg border border-border-subtle bg-background-surface px-3 py-3 text-left text-ds-body-xs text-text-primary/40'
+ : 'rounded-lg border border-border-default bg-background-surface px-3 py-3 text-left text-ds-body-xs text-text-secondary'
}
onClick={() => selectSource(nextSource)}
>
@@ -214,7 +214,7 @@ function DevtoolsCockpit() {
{source}
@@ -232,7 +232,7 @@ function DevtoolsCockpit() {
key={panel.id}
type="button"
aria-pressed={activeId === panel.id}
- className="shrink-0 rounded-lg border border-border-subtle bg-background-subtle px-3 py-2 text-ds-label-sm text-text-primary/35 hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary aria-pressed:bg-text-primary aria-pressed:text-background-default"
+ className="shrink-0 rounded-lg border border-border-default bg-background-subtle px-3 py-2 text-ds-label-sm text-text-secondary hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-text-primary aria-pressed:bg-text-primary aria-pressed:text-background-default"
onClick={() => setActiveId(panel.id)}
>
{panel.label}
@@ -246,7 +246,7 @@ function DevtoolsCockpit() {
key={label}
className="rounded-lg bg-background-subtle p-3"
>
-
+
{label}
@@ -259,16 +259,16 @@ function DevtoolsCockpit() {
{active.rows.map((row, index) => (
-
+
{row}
@@ -331,8 +331,8 @@ function PluginEventLab() {
-
-
+
+
{
"type JobEvents = {\n progress: { jobId: string; percent: number }\n}\nclass JobsClient extends EventClient {\n constructor() { super({ pluginId: 'jobs' }) }\n}\nconst jobs = new JobsClient()"
}
@@ -487,7 +487,7 @@ function SourceToBuild() {
Project summary
-
+
diff --git a/src/components/landing/HighlightLanding.tsx b/src/components/landing/HighlightLanding.tsx
index 0c639808e..6beb533b9 100644
--- a/src/components/landing/HighlightLanding.tsx
+++ b/src/components/landing/HighlightLanding.tsx
@@ -18,6 +18,7 @@ import { Footer } from '~/components/Footer'
import { LandingCommunitySection } from '~/components/LandingCommunitySection'
import LandingPageGad from '~/components/LandingPageGad'
import { LibraryDownloadsMicro } from '~/components/LibraryDownloadsMicro'
+import { LibraryStatusBadge } from '~/components/LibraryStatusBadge'
import { LibraryWordmark } from '~/components/LibraryWordmark'
import { PartnersSponsorsSection } from '~/components/PartnersSponsorsSection'
import { GithubIcon } from '~/components/icons/GithubIcon'
@@ -89,9 +90,7 @@ export default function HighlightLanding() {
{library.badge ? (
-
- {library.badge}
-
+
) : null}
diff --git a/src/components/landing/LibraryLanding.tsx b/src/components/landing/LibraryLanding.tsx
index cba737945..32cb435eb 100644
--- a/src/components/landing/LibraryLanding.tsx
+++ b/src/components/landing/LibraryLanding.tsx
@@ -16,6 +16,7 @@ import { libraryCategories, type LibraryCategory } from '~/libraries/categories'
import { getLibrary } from '~/libraries'
import type { LibraryId } from '~/libraries'
import { PartnersSponsorsSection } from '~/components/PartnersSponsorsSection'
+import { LibraryStatusBadge } from '~/components/LibraryStatusBadge'
import { ossStatsQuery, recentDownloadsQuery } from '~/queries/stats'
import { LandingCopyPromptButton } from './LandingCopyPromptButton'
@@ -269,9 +270,10 @@ export function LibraryLandingShell({
{library.badge ? (
-
- {library.badge}
-
+
) : null}
diff --git a/src/components/landing/MarkdownLanding.tsx b/src/components/landing/MarkdownLanding.tsx
index 495db12b8..87abc332f 100644
--- a/src/components/landing/MarkdownLanding.tsx
+++ b/src/components/landing/MarkdownLanding.tsx
@@ -28,6 +28,7 @@ import { Footer } from '~/components/Footer'
import { LandingCommunitySection } from '~/components/LandingCommunitySection'
import LandingPageGad from '~/components/LandingPageGad'
import { LibraryDownloadsMicro } from '~/components/LibraryDownloadsMicro'
+import { LibraryStatusBadge } from '~/components/LibraryStatusBadge'
import { LibraryWordmark } from '~/components/LibraryWordmark'
import { PartnersSponsorsSection } from '~/components/PartnersSponsorsSection'
import { GithubIcon } from '~/components/icons/GithubIcon'
@@ -186,9 +187,7 @@ export default function MarkdownLanding() {
{library.badge ? (
-
- {library.badge}
-
+
) : null}
diff --git a/src/components/stack/CategoryArticle.tsx b/src/components/stack/CategoryArticle.tsx
index 69324584c..dc22713e4 100644
--- a/src/components/stack/CategoryArticle.tsx
+++ b/src/components/stack/CategoryArticle.tsx
@@ -23,6 +23,7 @@ import {
} from '@phosphor-icons/react'
import { LibraryWordmark } from '~/components/LibraryWordmark'
+import { LibraryStatusBadge } from '~/components/LibraryStatusBadge'
import type { LibraryId, LibrarySlim } from '~/libraries'
import { formatPublishedDate } from '~/utils/blog-format'
import type { RelatedPost as RelatedPostData } from '~/utils/blog.functions'
@@ -1075,25 +1076,14 @@ function LibraryTitle({
) : null}
- {library.badge ? (
-
- {library.badge}
-
- ) : null}
+ {library.badge ? : null}
)
diff --git a/src/contexts/LibrariesOverlayContext.tsx b/src/contexts/LibrariesOverlayContext.tsx
index ee18781c4..71dc8a760 100644
--- a/src/contexts/LibrariesOverlayContext.tsx
+++ b/src/contexts/LibrariesOverlayContext.tsx
@@ -3,7 +3,7 @@ import { useLocation } from '@tanstack/react-router'
import { LibrariesOverlay } from '~/components/LibrariesOverlay'
interface LibrariesOverlayContextValue {
- openLibraries: () => void
+ openLibraries: (options?: { onBack?: () => void }) => void
closeLibraries: () => void
}
@@ -35,15 +35,35 @@ export function LibrariesOverlayProvider({
children: React.ReactNode
}) {
const [isOpen, setIsOpen] = React.useState(false)
+ const [returnTarget, setReturnTarget] = React.useState<{
+ onBack: () => void
+ } | null>(null)
- const openLibraries = React.useCallback(() => setIsOpen(true), [])
- const closeLibraries = React.useCallback(() => setIsOpen(false), [])
+ const openLibraries = React.useCallback(
+ (options?: { onBack?: () => void }) => {
+ setReturnTarget(options?.onBack ? { onBack: options.onBack } : null)
+ setIsOpen(true)
+ },
+ [],
+ )
+ const closeLibraries = React.useCallback(() => {
+ setIsOpen(false)
+ setReturnTarget(null)
+ }, [])
+
+ const returnToMenu = React.useCallback(() => {
+ const onBack = returnTarget?.onBack
+ setIsOpen(false)
+ setReturnTarget(null)
+ onBack?.()
+ }, [returnTarget])
// Any navigation (e.g. clicking a library card) closes the overlay so it
// never lingers over the destination page.
const pathname = useLocation({ select: (location) => location.pathname })
React.useEffect(() => {
setIsOpen(false)
+ setReturnTarget(null)
}, [pathname])
const value = React.useMemo(
@@ -54,7 +74,11 @@ export function LibrariesOverlayProvider({
return (
{children}
-
+
)
}
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index e67a7b9e0..894f370bc 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -71,6 +71,7 @@ import { Route as DsShadowsRouteImport } from './routes/ds.shadows'
import { Route as DsSemanticRouteImport } from './routes/ds.semantic'
import { Route as DsPaletteRouteImport } from './routes/ds.palette'
import { Route as DsNavbarRouteImport } from './routes/ds.navbar'
+import { Route as DsMaintainersRouteImport } from './routes/ds.maintainers'
import { Route as DsLogosRouteImport } from './routes/ds.logos'
import { Route as DsInputsRouteImport } from './routes/ds.inputs'
import { Route as DsIconographyRouteImport } from './routes/ds.iconography'
@@ -503,6 +504,11 @@ const DsNavbarRoute = DsNavbarRouteImport.update({
path: '/navbar',
getParentRoute: () => DsRoute,
} as any)
+const DsMaintainersRoute = DsMaintainersRouteImport.update({
+ id: '/maintainers',
+ path: '/maintainers',
+ getParentRoute: () => DsRoute,
+} as any)
const DsLogosRoute = DsLogosRouteImport.update({
id: '/logos',
path: '/logos',
@@ -1233,6 +1239,7 @@ export interface FileRoutesByFullPath {
'/ds/iconography': typeof DsIconographyRoute
'/ds/inputs': typeof DsInputsRoute
'/ds/logos': typeof DsLogosRoute
+ '/ds/maintainers': typeof DsMaintainersRoute
'/ds/navbar': typeof DsNavbarRoute
'/ds/palette': typeof DsPaletteRoute
'/ds/semantic': typeof DsSemanticRoute
@@ -1409,6 +1416,7 @@ export interface FileRoutesByTo {
'/ds/iconography': typeof DsIconographyRoute
'/ds/inputs': typeof DsInputsRoute
'/ds/logos': typeof DsLogosRoute
+ '/ds/maintainers': typeof DsMaintainersRoute
'/ds/navbar': typeof DsNavbarRoute
'/ds/palette': typeof DsPaletteRoute
'/ds/semantic': typeof DsSemanticRoute
@@ -1593,6 +1601,7 @@ export interface FileRoutesById {
'/ds/iconography': typeof DsIconographyRoute
'/ds/inputs': typeof DsInputsRoute
'/ds/logos': typeof DsLogosRoute
+ '/ds/maintainers': typeof DsMaintainersRoute
'/ds/navbar': typeof DsNavbarRoute
'/ds/palette': typeof DsPaletteRoute
'/ds/semantic': typeof DsSemanticRoute
@@ -1780,6 +1789,7 @@ export interface FileRouteTypes {
| '/ds/iconography'
| '/ds/inputs'
| '/ds/logos'
+ | '/ds/maintainers'
| '/ds/navbar'
| '/ds/palette'
| '/ds/semantic'
@@ -1956,6 +1966,7 @@ export interface FileRouteTypes {
| '/ds/iconography'
| '/ds/inputs'
| '/ds/logos'
+ | '/ds/maintainers'
| '/ds/navbar'
| '/ds/palette'
| '/ds/semantic'
@@ -2139,6 +2150,7 @@ export interface FileRouteTypes {
| '/ds/iconography'
| '/ds/inputs'
| '/ds/logos'
+ | '/ds/maintainers'
| '/ds/navbar'
| '/ds/palette'
| '/ds/semantic'
@@ -2780,6 +2792,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof DsNavbarRouteImport
parentRoute: typeof DsRoute
}
+ '/ds/maintainers': {
+ id: '/ds/maintainers'
+ path: '/maintainers'
+ fullPath: '/ds/maintainers'
+ preLoaderRoute: typeof DsMaintainersRouteImport
+ parentRoute: typeof DsRoute
+ }
'/ds/logos': {
id: '/ds/logos'
path: '/logos'
@@ -3881,6 +3900,7 @@ interface DsRouteChildren {
DsIconographyRoute: typeof DsIconographyRoute
DsInputsRoute: typeof DsInputsRoute
DsLogosRoute: typeof DsLogosRoute
+ DsMaintainersRoute: typeof DsMaintainersRoute
DsNavbarRoute: typeof DsNavbarRoute
DsPaletteRoute: typeof DsPaletteRoute
DsSemanticRoute: typeof DsSemanticRoute
@@ -3905,6 +3925,7 @@ const DsRouteChildren: DsRouteChildren = {
DsIconographyRoute: DsIconographyRoute,
DsInputsRoute: DsInputsRoute,
DsLogosRoute: DsLogosRoute,
+ DsMaintainersRoute: DsMaintainersRoute,
DsNavbarRoute: DsNavbarRoute,
DsPaletteRoute: DsPaletteRoute,
DsSemanticRoute: DsSemanticRoute,
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index 3648f1925..19c474216 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -205,6 +205,18 @@ export const Route = createRootRouteWithContext<{
sizes: '180x180',
href: '/apple-touch-icon.png',
},
+ {
+ rel: 'icon',
+ type: 'image/svg+xml',
+ href: '/favicon-light.svg',
+ media: '(prefers-color-scheme: light)',
+ },
+ {
+ rel: 'icon',
+ type: 'image/svg+xml',
+ href: '/favicon-dark.svg',
+ media: '(prefers-color-scheme: dark)',
+ },
{
rel: 'icon',
type: 'image/png',
diff --git a/src/routes/ds.badges.tsx b/src/routes/ds.badges.tsx
index 3c09ab731..37adefaab 100644
--- a/src/routes/ds.badges.tsx
+++ b/src/routes/ds.badges.tsx
@@ -52,6 +52,25 @@ function BadgesPage() {
+
+ Rounded
+Pill`}
+ >
+ Rounded
+ Pill
+
+ Stable
+
+
+ Beta
+
+
+
+
+
+ {children}
+ {children}
+
+
+ )
+}
+
+function ButtonMode({
+ children,
+ mode,
+}: {
+ children: React.ReactNode
+ mode: 'light' | 'dark'
+}) {
+ return (
+
+
+ {mode}
+
+ {children}
+
+ )
+}
+
function ButtonsPage() {
return (
- Primary
Secondary
Ghost
Link
+Subtle link
`}
>
Primary
Secondary
Ghost
Link
+
+ Subtle link
+
-
+
- Get started
Get started
Get started
@@ -100,14 +145,14 @@ function ButtonsPage() {
Copy prompt
-
+
- Blue
Green
Red
@@ -118,11 +163,11 @@ function ButtonsPage() {
{color[0].toUpperCase() + color.slice(1)}
))}
-
+
- Extra small
Small
Medium
@@ -132,33 +177,33 @@ function ButtonsPage() {
Small
Medium
Large
-
+
- Documentation
Learn more
-Read the guide `}
+View all posts `}
>
Documentation
Learn more
-
- Read the guide
+
+ View all posts
-
+
-
`}
@@ -175,11 +220,11 @@ function ButtonsPage() {
-
+
- None
Medium
Large
@@ -189,14 +234,14 @@ function ButtonsPage() {
Medium
Large
Full
-
+
- Download
Delete
Disabled`}
@@ -208,14 +253,14 @@ function ButtonsPage() {
Delete
Disabled
-
+
-
Day
Week
@@ -233,14 +278,14 @@ function ButtonsPage() {
Month
-
+
- New project
Settings
@@ -272,14 +317,14 @@ function ButtonsPage() {
Save
-
+
-
Deploy
@@ -317,7 +362,7 @@ function ButtonsPage() {
-
+
)
diff --git a/src/routes/ds.cards.tsx b/src/routes/ds.cards.tsx
index 057a81ccf..784427899 100644
--- a/src/routes/ds.cards.tsx
+++ b/src/routes/ds.cards.tsx
@@ -2,6 +2,7 @@ import { createFileRoute } from '@tanstack/react-router'
import { seo } from '~/utils/seo'
import { Tooltip } from '~/ui'
import { Button, Card, InlineCode } from '~/components/ds/ui'
+import { BlogPostCard } from '~/components/ds/ui/BlogPostCard'
import { ComponentPreview, DsPage, DsSection } from '~/components/ds/DsKit'
export const Route = createFileRoute('/ds/cards')({
@@ -60,6 +61,43 @@ function CardsPage() {
+
+ `}
+ >
+
+
+
+
+
+
+
-
+
)
}
diff --git a/src/routes/ds.maintainers.tsx b/src/routes/ds.maintainers.tsx
new file mode 100644
index 000000000..08040ec5d
--- /dev/null
+++ b/src/routes/ds.maintainers.tsx
@@ -0,0 +1,57 @@
+import { createFileRoute } from '@tanstack/react-router'
+import { MaintainerCard } from '~/components/MaintainerCard'
+import { ComponentPreview, DsPage, DsSection } from '~/components/ds/DsKit'
+import { coreMaintainers } from '~/libraries/maintainers'
+import { seo } from '~/utils/seo'
+
+export const Route = createFileRoute('/ds/maintainers')({
+ component: MaintainersPage,
+ head: () => ({
+ meta: seo({
+ title: 'Maintainers | TanStack Design System',
+ description:
+ 'The shared maintainer card used for core teams, instructors, and library maintainers across TanStack.',
+ }),
+ }),
+})
+
+function MaintainersPage() {
+ return (
+
+
+ `}
+ >
+
+
+
+
+
+
+
+
+ {maintainers.map((maintainer) => (
+
+ ))}
+ `}
+ >
+ {coreMaintainers.slice(0, 3).map((maintainer) => (
+
+ ))}
+
+
+
+ )
+}
diff --git a/src/routes/ds.typography.tsx b/src/routes/ds.typography.tsx
index bd516cc1d..078abe8ec 100644
--- a/src/routes/ds.typography.tsx
+++ b/src/routes/ds.typography.tsx
@@ -1,6 +1,7 @@
+import type * as React from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { seo } from '~/utils/seo'
-import { DsPage, DsSection } from '~/components/ds/DsKit'
+import { DsDescription, DsPage, DsSection } from '~/components/ds/DsKit'
export const Route = createFileRoute('/ds/typography')({
component: TypographyPage,
@@ -187,60 +188,168 @@ function TypographyPage() {
{GROUPS.map((group) => (
-
- {group.items.map((item) => (
-
-
- {group.sample}
-
-
-
- {item.name}
-
-
- {item.spec}
-
-
-
- ))}
+
+
+
))}
+
+
+
+ Introduces the purpose and scope of a design-system page.
+
+
+ Explains usage, behavior, and guidance for a component section.
+
+
+ Adds compact supporting context inside a component preview.
+
+
+
+
-
- {SEMANTIC_HEADINGS.map((h) => (
-
-
- Type-safe by default
-
-
-
- .{h.cls}
-
-
- {h.alias}
-
-
-
- ))}
+
+
+
)
}
+function TypeGroupMode({
+ group,
+ mode,
+}: {
+ group: TypeGroup
+ mode: 'light' | 'dark'
+}) {
+ return (
+
+
+ {mode}
+
+
+ {group.items.map((item) => (
+
+
+ {group.sample}
+
+
+
+ {item.name}
+
+
+ {item.spec}
+
+
+
+ ))}
+
+
+ )
+}
+
+function SemanticHeadingMode({ mode }: { mode: 'light' | 'dark' }) {
+ return (
+
+
+ {mode}
+
+
+ {SEMANTIC_HEADINGS.map((heading) => (
+
+
+ Type-safe by default
+
+
+
+ .{heading.cls}
+
+
+ {heading.alias}
+
+
+
+ ))}
+
+
+ )
+}
+
+function DescriptionRoleCard({
+ children,
+ colorToken,
+ name,
+ role,
+ typeToken,
+}: {
+ children: React.ReactNode
+ colorToken: string
+ name: string
+ role: React.ComponentProps
['role']
+ typeToken: string
+}) {
+ return (
+
+
+ {name}
+
+
+ {children}
+
+
+ {typeToken} · {colorToken}
+
+
+ )
+}
+
function FamilyCard({
name,
token,
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
index 277c2bb6f..0a743eaad 100644
--- a/src/routes/index.tsx
+++ b/src/routes/index.tsx
@@ -1,7 +1,6 @@
import * as React from 'react'
import { Link, createFileRoute } from '@tanstack/react-router'
-import discordImage from '~/images/discord-logo-white.svg'
import { twMerge } from 'tailwind-merge'
import {
@@ -10,10 +9,10 @@ import {
Stack,
Shield,
Lightning,
+ Pause,
Play,
type Icon,
} from '@phosphor-icons/react'
-import { YouTubeIcon } from '~/components/icons/YouTubeIcon'
import { HomeApplicationStarter } from '~/components/home/HomeApplicationStarter'
import { HomeCommunitySection } from '~/components/home/HomeCommunitySection'
import { HomeNewsletterSection } from '~/components/home/HomeNewsletterSection'
@@ -72,44 +71,24 @@ function Index() {
headline bottom-left, description + CTA bottom-right. The photo is
always light, so text uses a mode-stable dark token (neutral-500)
rather than a theme-flipping semantic. */}
-
-
- {/* Wrapper carries the 8px inset + squircle clip so the
![]()
- (which has intrinsic width/height) fills it exactly instead of
- overflowing the right/bottom. Plain
![]()
(not OptimizedImage):
+
+
+ {/* The parent supplies the 4px frame shared by the hero and stats.
+ This wrapper clips the image to the inner radius so the
![]()
+ fills it exactly instead of overflowing. Plain
![]()
(not OptimizedImage):
the Cloudflare transform resolves against the production origin,
so a newly-added asset 404s until deployed. */}
-
-
-
-
-
-
-
+
+
The{' '}
open source
- {' '}
- application stack for the web
+
+
the application stack
+
for the web
-
-
-
+
+
Headless, type-safe, composable tools for building modern web
applications that work naturally for developers and reliably
for agents
@@ -138,6 +117,9 @@ function Index() {
+
+
+
-
-
-
-

-
-
-
-
- The official TanStack community to ask questions, network and
- make new friends and get lightning fast news about what's coming
- next for TanStack!
-
-
-
-
- Join TanStack Discord
-
-
-
-
-
-
-
-
-
-
-
-
-
- The official TanStack YouTube channel. Tutorials, deep dives,
- release walkthroughs, and more — free for everyone!
-
-
-
-
-
- Subscribe on YouTube
-
-
-
-
-
@@ -254,6 +142,88 @@ function Index() {
)
}
+function HeroPalmMedia() {
+ const videoRef = React.useRef
(null)
+ const [isPlaying, setIsPlaying] = React.useState(true)
+
+ React.useEffect(() => {
+ const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)')
+
+ const syncMotionPreference = () => {
+ if (reducedMotion.matches) {
+ videoRef.current?.pause()
+ }
+ }
+
+ syncMotionPreference()
+ reducedMotion.addEventListener('change', syncMotionPreference)
+ return () =>
+ reducedMotion.removeEventListener('change', syncMotionPreference)
+ }, [])
+
+ const togglePlayback = () => {
+ const video = videoRef.current
+ if (!video) return
+
+ if (video.paused) {
+ void video.play()
+ } else {
+ video.pause()
+ }
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {isPlaying ? (
+
+ ) : (
+
+ )}
+
+ >
+ )
+}
+
type PrincipleProof = 'adapters' | 'types' | 'adoption' | 'portable'
type WhyTanStackPrinciple = {
diff --git a/src/routes/maintainers.tsx b/src/routes/maintainers.tsx
index 2914fe93b..7ecf3605f 100644
--- a/src/routes/maintainers.tsx
+++ b/src/routes/maintainers.tsx
@@ -27,7 +27,7 @@ const sortBySchema = v.picklist(['none', 'name', 'role', 'contributions'])
const searchSchema = v.object({
libraries: v.fallback(v.optional(v.array(libraryIdSchema)), undefined),
- viewMode: v.fallback(v.optional(viewModeSchema, 'compact'), 'compact'),
+ viewMode: v.fallback(v.optional(viewModeSchema, 'full'), 'full'),
groupBy: v.fallback(v.optional(groupBySchema, 'none'), 'none'),
sortBy: v.fallback(v.optional(sortBySchema, 'none'), 'none'),
})
diff --git a/src/routes/paid-support.tsx b/src/routes/paid-support.tsx
index 1a3892b43..48203684b 100644
--- a/src/routes/paid-support.tsx
+++ b/src/routes/paid-support.tsx
@@ -25,9 +25,7 @@ export const Route = createFileRoute('/paid-support')({
})
function PaidSupportComp() {
- const [viewMode, setViewMode] = useState<'compact' | 'full' | 'row'>(
- 'compact',
- )
+ const [viewMode, setViewMode] = useState<'compact' | 'full' | 'row'>('full')
return (
<>
diff --git a/src/styles/app.css b/src/styles/app.css
index ad7505425..b71d9108a 100644
--- a/src/styles/app.css
+++ b/src/styles/app.css
@@ -286,7 +286,7 @@ html.theme-switching *::after {
--color-border-error: var(--color-ds-terracotta-400);
--color-border-success: var(--color-ds-green-400);
- --color-icon-default: var(--color-ds-neutral-400);
+ --color-icon-default: var(--color-ds-neutral-500);
--color-icon-accent: var(--color-ds-blue-500);
--color-icon-muted: var(--color-ds-neutral-300);
--color-icon-inverse: #ffffff;
@@ -566,14 +566,67 @@ html.dark {
/* Interaction-state overlays — white tints for dark surfaces (Figma values
used directly where provided: input/background/hover, menu/item/pressed,
divider/subtle). */
- --color-surface-state-hover: #ffffff14; /* white 8% */
- --color-surface-state-pressed: #ffffff1f; /* white 12% */
+ --color-surface-state-hover: #ffffff1f; /* white 12% */
+ --color-surface-state-pressed: #ffffff29; /* white 16% */
--color-input-bg-hover: #ffffff0a; /* white 4% */
--color-menu-item-pressed: #ffffff1f; /* white 12% */
--color-divider-subtle: #1e1e1e;
--color-divider-on-inverse: #1e1e1e;
}
+/* Fixed theme scopes for side-by-side design-system specimens. These mirror
+ the semantic values above so previews can show both modes independently of
+ the active site theme. */
+.ds-mode-light {
+ color-scheme: light;
+ --color-text-primary: var(--color-ds-neutral-500);
+ --color-text-secondary: var(--color-ds-neutral-400);
+ --color-text-muted: var(--color-ds-neutral-300);
+ --color-text-inverse: #ffffff;
+ --color-background-default: #ffffff;
+ --color-background-surface: #ffffff;
+ --color-background-subtle: #fafafa;
+ --color-background-inverse: var(--color-ds-neutral-500);
+ --color-border-default: var(--color-ds-neutral-200);
+ --color-border-subtle: var(--color-ds-neutral-100);
+ --color-border-strong: var(--color-ds-neutral-400);
+ --color-border-focus: var(--color-ds-blue-400);
+ --color-action-secondary: var(--color-ds-neutral-100);
+ --color-action-secondary-hover: var(--color-ds-neutral-200);
+ --color-category-framework: var(--color-ds-green-400);
+ --color-category-data: var(--color-ds-terracotta-400);
+ --color-category-ui: var(--color-ds-blue-400);
+ --color-category-performance: var(--color-ds-amber-400);
+ --color-category-tooling: var(--color-ds-neutral-400);
+ --color-surface-state-hover: #1111110f;
+ --color-surface-state-pressed: #1111111f;
+}
+
+.ds-mode-dark {
+ color-scheme: dark;
+ --color-text-primary: #ffffff;
+ --color-text-secondary: #aea691;
+ --color-text-muted: #756c5b;
+ --color-text-inverse: #111111;
+ --color-background-default: #111111;
+ --color-background-surface: #1f1f1f;
+ --color-background-subtle: #1b1b1b;
+ --color-background-inverse: #ffffff;
+ --color-border-default: #2d2d2d;
+ --color-border-subtle: #232323;
+ --color-border-strong: #aea691;
+ --color-border-focus: #61adbf;
+ --color-action-secondary: #2b2b2b;
+ --color-action-secondary-hover: #3a3a3a;
+ --color-category-framework: var(--color-ds-green-300);
+ --color-category-data: var(--color-ds-terracotta-300);
+ --color-category-ui: var(--color-ds-blue-300);
+ --color-category-performance: var(--color-ds-amber-300);
+ --color-category-tooling: var(--color-ds-neutral-200);
+ --color-surface-state-hover: #ffffff1f;
+ --color-surface-state-pressed: #ffffff29;
+}
+
/* Safelist for dynamic colors */
@source inline('{bg-blue-{4,5,6}00,bg-emerald-500,bg-purple-500,bg-pink-500,bg-cyan-500,bg-slate-{4,5}00,bg-twine-700,bg-orange-{5,6,7}00,bg-yellow-500,bg-lime-700,bg-red-500,bg-green-500}/{20,30,40,80}');
@source inline('{hover:{bg-blue-{4,5,6}00,bg-emerald-500,bg-purple-500,bg-pink-500,bg-cyan-500,bg-slate-{4,5}00,bg-twine-700,bg-orange-{5,6,7}00,bg-yellow-500,bg-lime-700,bg-red-500,bg-green-500}/40}');
@@ -748,12 +801,51 @@ button {
transition-delay: 0s;
}
+@keyframes mobile-mega-menu-item-in {
+ from {
+ opacity: 0;
+ transform: translateY(6px);
+ filter: blur(2px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ filter: blur(0);
+ }
+}
+
+.mobile-mega-menu-enter .mobile-mega-menu-enter-item {
+ animation: mobile-mega-menu-item-in 160ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
+ animation-delay: 40ms;
+}
+
+.mobile-mega-menu-enter .mobile-mega-menu-enter-item:nth-child(2) {
+ animation-delay: 60ms;
+}
+
+.mobile-mega-menu-enter .mobile-mega-menu-enter-item:nth-child(3) {
+ animation-delay: 80ms;
+}
+
+.mobile-mega-menu-enter .mobile-mega-menu-enter-item:nth-child(4) {
+ animation-delay: 100ms;
+}
+
+.mobile-mega-menu-enter .mobile-mega-menu-enter-item:nth-child(5) {
+ animation-delay: 120ms;
+}
+
+.mobile-mega-menu-enter .mobile-mega-menu-enter-item:nth-child(n + 6) {
+ animation-delay: 140ms;
+}
+
.ts-mega-dropdown-panel {
position: relative;
isolation: isolate;
background-color: color-mix(
in srgb,
- var(--color-background-subtle) 95%,
+ var(--color-background-default) 95%,
transparent
);
backdrop-filter: blur(6px);
@@ -958,6 +1050,10 @@ button {
.ts-mega-dropdown {
transition: none;
}
+
+ .mobile-mega-menu-enter .mobile-mega-menu-enter-item {
+ animation: none;
+ }
}
@layer base {
diff --git a/src/ui/Badge.tsx b/src/ui/Badge.tsx
index 5b4556c92..a5b70f4f9 100644
--- a/src/ui/Badge.tsx
+++ b/src/ui/Badge.tsx
@@ -13,6 +13,7 @@ type BadgeVariant =
type BadgeProps = {
children: React.ReactNode
variant?: BadgeVariant
+ rounded?: 'md' | 'full'
className?: string
}
@@ -32,12 +33,14 @@ const variantStyles: Record = {
export function Badge({
children,
variant = 'default',
+ rounded = 'full',
className,
}: BadgeProps) {
return (