diff --git a/src/components/LibraryStatsSection.tsx b/src/components/LibraryStatsSection.tsx
index a08cb375e..f2eb6581c 100644
--- a/src/components/LibraryStatsSection.tsx
+++ b/src/components/LibraryStatsSection.tsx
@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
-import { Download, Star } from 'lucide-react'
+import { Download, Star } from '@phosphor-icons/react'
import type * as React from 'react'
import type { Library } from '~/libraries'
import { ossStatsQuery } from '~/queries/stats'
diff --git a/src/components/LibraryTestimonials.tsx b/src/components/LibraryTestimonials.tsx
index dfb1c27a5..607786d3b 100644
--- a/src/components/LibraryTestimonials.tsx
+++ b/src/components/LibraryTestimonials.tsx
@@ -1,4 +1,4 @@
-import { Star } from 'lucide-react'
+import { Star } from '@phosphor-icons/react'
import type { Testimonial } from '~/libraries/types'
import { Card } from './Card'
diff --git a/src/components/LoginModal.tsx b/src/components/LoginModal.tsx
index 3bb29aba0..fb7356d06 100644
--- a/src/components/LoginModal.tsx
+++ b/src/components/LoginModal.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
-import { X } from 'lucide-react'
+import { X } from '@phosphor-icons/react'
import { GithubIcon } from '~/components/icons/GithubIcon'
import { GoogleIcon } from '~/components/icons/GoogleIcon'
import { authClient } from '~/auth/client'
diff --git a/src/components/MegaMenuItem.tsx b/src/components/MegaMenuItem.tsx
new file mode 100644
index 000000000..d153cfae7
--- /dev/null
+++ b/src/components/MegaMenuItem.tsx
@@ -0,0 +1,120 @@
+import * as React from 'react'
+import { Link } from '@tanstack/react-router'
+import { twMerge } from 'tailwind-merge'
+import { ArrowSquareOut } from '@phosphor-icons/react'
+
+type IconComponent = React.ComponentType<{ className?: string }>
+
+export interface MegaMenuItemProps {
+ /** Leading icon, shown in a bordered rounded square. */
+ icon?: IconComponent
+ title: React.ReactNode
+ description?: React.ReactNode
+ /** Internal route or external (http/mailto) URL. */
+ to: string
+ hash?: string
+ badge?: string
+ onNavigate?: () => void
+ variant?: 'desktop' | 'mobile'
+ /** Elevated variant used for standalone / featured rows. */
+ compact?: boolean
+ className?: string
+}
+
+function isExternal(to: string) {
+ return to.startsWith('http') || to.startsWith('mailto:')
+}
+
+/**
+ * A single mega-menu row — icon + title + description with rest / hover /
+ * pressed states. Modeled on the Figma "Mega Menu Item" component: bordered
+ * icon square, Bricolage-bold title (heading-5), muted body-xs description, and
+ * a mode-adaptive overlay on hover/press. Used by the site Navbar and shown in
+ * the design system at /ds/navbar.
+ */
+export function MegaMenuItem({
+ icon: Icon,
+ title,
+ description,
+ to,
+ hash,
+ badge,
+ onNavigate,
+ variant = 'desktop',
+ compact,
+ className,
+}: MegaMenuItemProps) {
+ const external = isExternal(to)
+
+ const rootClassName = twMerge(
+ // Figma "Mega Menu Item": gap 10px, pl 8 / pr 16 / py 8, radius 12px.
+ 'group/mmi flex items-center gap-2.5 rounded-xl py-2 pl-2 pr-4 text-left transition-colors',
+ // States differ only by row background: hover white/4%, pressed white/12%
+ // (mode-adaptive via text-primary so it also works on light menu panels).
+ 'hover:bg-text-primary/[0.04] focus:bg-text-primary/[0.04] focus:outline-none active:bg-text-primary/[0.12]',
+ compact && 'border border-border-subtle bg-background-surface',
+ variant === 'desktop' && !compact && 'w-[260px]',
+ variant === 'mobile' && 'py-2.5',
+ className,
+ )
+
+ const content = (
+ <>
+ {Icon ? (
+
+
+
+ ) : null}
+
+
+
+ {title}
+
+ {badge ? (
+
+ {badge}
+
+ ) : null}
+ {external && !to.startsWith('mailto:') ? (
+
+ ) : null}
+
+ {description ? (
+ // Plain string (not twMerge) — the DS text-size and text-color
+ // utilities both start with `text-`, and twMerge would drop the color.
+
+ {description}
+
+ ) : null}
+
+ >
+ )
+
+ if (external) {
+ return (
+
+ {content}
+
+ )
+ }
+
+ return (
+
+ {content}
+
+ )
+}
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 817ea5e86..ecf6f0b94 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -9,27 +9,27 @@ const LazyNavbarAuthControls = React.lazy(() =>
})),
)
import { NavbarCartButton } from './NavbarCartButton'
+import { MegaMenuItem } from './MegaMenuItem'
import { Link, useLocation, useMatches } from '@tanstack/react-router'
import {
BookOpen,
Code,
- ExternalLink,
- Grid2X2,
+ GridFour,
Hammer,
Heart,
- HelpCircle,
- Mail,
- Menu,
+ Question,
+ Envelope,
+ List,
Newspaper,
- Paintbrush,
+ PaintBrush,
ShieldCheck,
ShoppingBag,
- Sparkles,
- TrendingUp,
+ Sparkle,
+ TrendUp,
User,
Users,
X,
-} from 'lucide-react'
+} from '@phosphor-icons/react'
import { ThemeToggle } from './ThemeToggle'
import { AiDockButton, SearchButton } from './SearchButton'
import { BrandContextMenu } from './BrandContextMenu'
@@ -50,7 +50,6 @@ import {
} from '~/components/Dropdown'
import { DiscordIcon } from '~/components/icons/DiscordIcon'
import { InstagramIcon } from '~/components/icons/InstagramIcon'
-import { OptimizedImage } from '~/components/OptimizedImage'
import { BSkyIcon } from '~/components/icons/BSkyIcon'
import { BrandXIcon } from '~/components/icons/BrandXIcon'
import { YouTubeIcon } from '~/components/icons/YouTubeIcon'
@@ -72,31 +71,23 @@ const LogoSection = ({ title }: LogoProps) => {
return (
-
-
-

-

-
-
TanStack
+

+

)
}
@@ -239,7 +230,7 @@ const NAV_GROUPS = [
label: 'Showcase',
to: '/showcase',
description: 'Products and teams building with TanStack.',
- icon: Sparkles,
+ icon: Sparkle,
},
],
},
@@ -263,7 +254,7 @@ const NAV_GROUPS = [
label: 'Stats',
to: '/stats/npm',
description: 'NPM and ecosystem usage data.',
- icon: TrendingUp,
+ icon: TrendUp,
},
],
},
@@ -286,7 +277,7 @@ const NAV_GROUPS = [
label: 'Support Overview',
to: '/support',
description: 'Find the right support path.',
- icon: HelpCircle,
+ icon: Question,
},
{
label: 'Partners',
@@ -311,7 +302,7 @@ const NAV_GROUPS = [
label: 'Contact',
to: 'mailto:support@tanstack.com',
description: 'Get in touch with the TanStack team.',
- icon: Mail,
+ icon: Envelope,
},
],
},
@@ -334,7 +325,13 @@ const NAV_GROUPS = [
label: 'Brand Guide',
to: '/brand-guide',
description: 'Logos, colors, and brand usage.',
- icon: Paintbrush,
+ icon: PaintBrush,
+ },
+ {
+ label: 'Design System',
+ to: '/ds',
+ description: 'Design tokens and components for TanStack surfaces.',
+ icon: GridFour,
},
],
},
@@ -346,7 +343,7 @@ const NAV_GROUPS = [
item: {
label: 'Partnership Inquiry',
to: 'mailto:partners@tanstack.com?subject=TanStack Partnership Inquiry',
- icon: Mail,
+ icon: Envelope,
},
},
},
@@ -362,10 +359,6 @@ function getLibraryDisplayName(library: LibrarySlim) {
return library.name.replace(/^TanStack\s+/, '')
}
-function isExternalLink(to: string) {
- return to.startsWith('http') || to.startsWith('mailto:')
-}
-
function getLibraryDocsTo(library: NavigationLibrary) {
return `${library.to}/latest/docs`
}
@@ -570,8 +563,8 @@ export function Navbar({ children }: { children: React.ReactNode }) {
const navbar = (
) : null}
-
-
-
- {Icon ? (
-
-
-
- ) : null}
-
-
-
- {item.label}
-
- {item.badge ? (
-
- {item.badge}
-
- ) : null}
- {isExternal && !item.to.startsWith('mailto:') ? (
-
- ) : null}
-
- {item.description ? (
-
- {item.description}
-
- ) : null}
-
- >
- )
-
- if (isExternal) {
- return (
-
- {content}
-
- )
- }
-
return (
-
- {content}
-
+ badge={item.badge}
+ onNavigate={onNavigate}
+ variant={variant}
+ compact={compact}
+ />
)
}
diff --git a/src/components/NavbarAuthControls.tsx b/src/components/NavbarAuthControls.tsx
index a311a30e0..01d0d13ca 100644
--- a/src/components/NavbarAuthControls.tsx
+++ b/src/components/NavbarAuthControls.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import { User } from 'lucide-react'
+import { User } from '@phosphor-icons/react'
import { Link, useNavigate } from '@tanstack/react-router'
import { twMerge } from 'tailwind-merge'
import {
diff --git a/src/components/NavbarCartButton.tsx b/src/components/NavbarCartButton.tsx
index 4d97c6f9b..26d43da87 100644
--- a/src/components/NavbarCartButton.tsx
+++ b/src/components/NavbarCartButton.tsx
@@ -1,5 +1,5 @@
import { Link, useLocation } from '@tanstack/react-router'
-import { ShoppingCart } from 'lucide-react'
+import { ShoppingCart } from '@phosphor-icons/react'
import { twMerge } from 'tailwind-merge'
import { useCart } from '~/hooks/useCart'
import { useCartDrawerStore } from '~/components/shop/cartDrawerStore'
diff --git a/src/components/NewsletterSignup.tsx b/src/components/NewsletterSignup.tsx
index 5be7aba99..a7e24e715 100644
--- a/src/components/NewsletterSignup.tsx
+++ b/src/components/NewsletterSignup.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
-import { Check, LogIn, Mail } from 'lucide-react'
+import { Check, SignIn, Envelope } from '@phosphor-icons/react'
import { twMerge } from 'tailwind-merge'
import { useToast } from '~/components/ToastProvider'
import { useLoginModal } from '~/contexts/LoginModalContext'
@@ -94,7 +94,11 @@ export function NewsletterSignup({
disabled={isPending}
className={twMerge('w-full', buttonClassName)}
>
- {user ?
:
}
+ {user ? (
+
+ ) : (
+
+ )}
{isPending
? 'Subscribing...'
: user
diff --git a/src/components/NotFound.tsx b/src/components/NotFound.tsx
index 87b880986..af8f3772d 100644
--- a/src/components/NotFound.tsx
+++ b/src/components/NotFound.tsx
@@ -3,16 +3,16 @@ import { Link, useRouterState } from '@tanstack/react-router'
import {
ArrowRight,
BookOpen,
- Boxes,
+ Stack,
Compass,
- HelpCircle,
- Home,
- LifeBuoy,
+ Question,
+ House,
+ Lifebuoy,
Newspaper,
ShoppingBag,
- Sparkles,
- type LucideIcon,
-} from 'lucide-react'
+ Sparkle,
+ type Icon,
+} from '@phosphor-icons/react'
import { twMerge } from 'tailwind-merge'
import { libraries, type LibrarySlim } from '~/libraries'
import { Button } from '~/ui'
@@ -24,7 +24,7 @@ type NotFoundDestination = {
label: string
description: string
href: string
- icon: LucideIcon
+ icon: Icon
accent: Accent
score: number
}
@@ -46,7 +46,7 @@ const fallbackDestinations: NotFoundDestination[] = [
label: 'All libraries',
description: 'Browse TanStack Query, Router, Table, Start, and more.',
href: '/libraries',
- icon: Boxes,
+ icon: Stack,
accent: 'emerald',
score: 1,
},
@@ -64,7 +64,7 @@ const fallbackDestinations: NotFoundDestination[] = [
label: 'Support',
description: 'Find paid support, community help, and project resources.',
href: '/support',
- icon: LifeBuoy,
+ icon: Lifebuoy,
accent: 'amber',
score: 1,
},
@@ -85,7 +85,7 @@ const sectionDestinations: NotFoundDestination[] = [
label: 'Showcase',
description: 'Explore projects built with TanStack.',
href: '/showcase',
- icon: Sparkles,
+ icon: Sparkle,
accent: 'cyan',
score: 0,
},
@@ -258,7 +258,7 @@ function getSectionDestinations(pathname: string) {
label: 'Support',
description: 'Get help when the missing page was supposed to unblock you.',
href: '/support',
- icon: HelpCircle,
+ icon: Question,
accent: 'amber',
score: 0,
}
@@ -364,15 +364,15 @@ export function NotFound({ children }: { children?: React.ReactNode }) {
diff --git a/src/components/NotesModerationList.tsx b/src/components/NotesModerationList.tsx
index 13154bb68..2a304e3ce 100644
--- a/src/components/NotesModerationList.tsx
+++ b/src/components/NotesModerationList.tsx
@@ -14,7 +14,7 @@ import { PaginationControls } from './PaginationControls'
import { Spinner } from './Spinner'
import type { DocFeedback } from '~/db/types'
import { calculatePoints } from '~/utils/docFeedback.shared'
-import { ExternalLink, TriangleAlert } from 'lucide-react'
+import { ArrowSquareOut, Warning } from '@phosphor-icons/react'
import { Badge } from '~/ui'
interface NotesModerationListProps {
@@ -186,7 +186,7 @@ export function NotesModerationList({
{feedback.isDetached && (
-
+
Detached
)}
@@ -218,7 +218,7 @@ export function NotesModerationList({
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 hover:underline"
>
-
+
View Doc
diff --git a/src/components/OpenSourceStats.tsx b/src/components/OpenSourceStats.tsx
index ad58a92d8..846182152 100644
--- a/src/components/OpenSourceStats.tsx
+++ b/src/components/OpenSourceStats.tsx
@@ -8,7 +8,7 @@ import {
recentDownloadsQuery,
} from '~/queries/stats'
import { useNpmDownloadCounter } from '~/hooks/useNpmDownloadCounter'
-import { Download, Star, TrendingUp } from 'lucide-react'
+import { Download, Star, TrendUp } from '@phosphor-icons/react'
import {
tanStackTotalNpmStatsLibrary,
tanStackTotalNpmStatsSearch,
@@ -191,7 +191,7 @@ export default function OssStats({ library }: { library?: Library }) {
gradientClassName="bg-linear-to-r from-transparent to-cyan-500/5"
>
-
+
diff --git a/src/components/PaginationControls.tsx b/src/components/PaginationControls.tsx
index ece3cca9e..ffce37c47 100644
--- a/src/components/PaginationControls.tsx
+++ b/src/components/PaginationControls.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import { ChevronLeft, ChevronRight } from 'lucide-react'
+import { CaretLeft, CaretRight } from '@phosphor-icons/react'
interface PaginationControlsProps {
currentPage: number
@@ -145,7 +145,7 @@ export function PaginationControls({
disabled={!canGoPrevious}
className="flex items-center px-2 py-1 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50 hover:text-gray-900 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-white disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-gray-800 gap-1 transition-colors"
>
-
+
Prev
@@ -188,7 +188,7 @@ export function PaginationControls({
className="flex items-center px-2 py-1 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50 hover:text-gray-900 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-white disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-gray-800 gap-1 transition-colors"
>
Next
-
+
diff --git a/src/components/PartnershipCallout.tsx b/src/components/PartnershipCallout.tsx
index e91cab77c..820f505d3 100644
--- a/src/components/PartnershipCallout.tsx
+++ b/src/components/PartnershipCallout.tsx
@@ -1,5 +1,5 @@
import { getLibrary, LibraryId } from '~/libraries'
-import { HeartHandshake } from 'lucide-react'
+import { HandHeart } from '@phosphor-icons/react'
import { Card } from './Card'
import { trackEvent } from '~/utils/analytics'
@@ -17,7 +17,7 @@ export function PartnershipCallout({ libraryId }: PartnershipCalloutProps) {
w-[500px] max-w-full mx-auto"
>
- {library.name.replace('TanStack ', '')} You?
+ {library.name.replace('TanStack ', '')} You?
diff --git a/src/components/SearchButton.tsx b/src/components/SearchButton.tsx
index 96bcf0d5a..c050a3530 100644
--- a/src/components/SearchButton.tsx
+++ b/src/components/SearchButton.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import { Command, Search } from 'lucide-react'
+import { Command, MagnifyingGlass } from '@phosphor-icons/react'
import { twMerge } from 'tailwind-merge'
import { Button } from '~/ui'
import { useSearchContext } from '~/contexts/SearchContext'
@@ -43,7 +43,7 @@ export function SearchButton({
className,
)}
>
-
+
{iconOnly ? (
Search
) : (
diff --git a/src/components/SearchModal.tsx b/src/components/SearchModal.tsx
index 48302cbe2..ff7e1432a 100644
--- a/src/components/SearchModal.tsx
+++ b/src/components/SearchModal.tsx
@@ -20,21 +20,21 @@ import {
import { liteClient } from 'algoliasearch/lite'
import {
X,
- Search,
- SearchSlash,
- ChevronDown,
- CornerDownLeft,
+ MagnifyingGlass,
+ MagnifyingGlassMinus,
+ CaretDown,
+ ArrowElbowDownLeft,
ArrowUp,
Check,
Copy,
- ExternalLink,
- History,
- MessageSquarePlus,
+ ArrowSquareOut,
+ ClockCounterClockwise,
+ ChatCenteredDots,
ThumbsDown,
ThumbsUp,
- Maximize2,
- Minimize2,
-} from 'lucide-react'
+ ArrowsOutSimple,
+ ArrowsInSimple,
+} from '@phosphor-icons/react'
import {
DefaultKapaApiService,
KapaProvider,
@@ -1193,9 +1193,9 @@ function DockMaximizeButton({
)}
>
{isMaximized ? (
-
+
) : (
-
+
)}
{label}
@@ -1318,7 +1318,7 @@ function KapaHistoryButton({
: 'gap-1 px-1.5 sm:px-2 py-1 rounded-md bg-white/80 dark:bg-black/80',
)}
>
-
+
{compact ? (
Chat history
) : (
@@ -1412,9 +1412,15 @@ function AIMessageHeader({ action }: { action?: React.ReactNode }) {

+
@@ -1682,7 +1688,7 @@ function KapaAnswer({
>
{!hasSourceScope ? (
-
+
) : null}
{label}
@@ -1988,9 +1994,9 @@ function KapaChatPanel({
aria-label={isFullHeight ? 'Collapse search' : 'Expand search'}
>
{isFullHeight ? (
-
+
) : (
-
+
)}
>
@@ -2035,7 +2041,7 @@ function KapaChatPanel({
: 'gap-1 px-2 py-1 rounded-md bg-white/80 dark:bg-black/80',
)}
>
-
+
{isDock ? (
New chat
) : (
@@ -2212,9 +2218,9 @@ function KapaUnavailablePanel({
aria-label={isFullHeight ? 'Collapse search' : 'Expand search'}
>
{isFullHeight ? (
-
+
) : (
-
+
)}
>
@@ -2388,7 +2394,7 @@ function CommandSearchInput({
diff --git a/src/components/ShowcaseGallery.tsx b/src/components/ShowcaseGallery.tsx
index d6489634f..46e7ea800 100644
--- a/src/components/ShowcaseGallery.tsx
+++ b/src/components/ShowcaseGallery.tsx
@@ -11,7 +11,7 @@ import { SubmitShowcasePlaceholder } from './ShowcaseSection'
import { PaginationControls } from './PaginationControls'
import { ShowcaseTopBarFilters } from './ShowcaseTopBarFilters'
import type { ShowcaseUseCase } from '~/db/types'
-import { Plus } from 'lucide-react'
+import { Plus } from '@phosphor-icons/react'
import { Button } from '~/ui'
import { useCurrentUser } from '~/hooks/useCurrentUser'
import { useLoginModal } from '~/contexts/LoginModalContext'
diff --git a/src/components/ShowcaseModerationList.tsx b/src/components/ShowcaseModerationList.tsx
index 6b48f866e..b447d838b 100644
--- a/src/components/ShowcaseModerationList.tsx
+++ b/src/components/ShowcaseModerationList.tsx
@@ -16,11 +16,11 @@ import {
Check,
X,
Star,
- ExternalLink,
- Trash2,
+ ArrowSquareOut,
+ Trash,
ThumbsUp,
ThumbsDown,
-} from 'lucide-react'
+} from '@phosphor-icons/react'
import { libraries } from '~/libraries'
import { Badge, Button } from '~/ui'
import { getRowFieldId } from '~/utils/route-encoding'
@@ -405,7 +405,7 @@ export function ShowcaseModerationList({
}}
title="Delete"
>
-
+
)}
@@ -443,7 +443,7 @@ export function ShowcaseModerationList({
className="text-sm text-blue-600 dark:text-blue-400 hover:underline flex items-center gap-1"
>
{showcase.url}
-
+
diff --git a/src/components/ShowcaseSection.tsx b/src/components/ShowcaseSection.tsx
index 05008ba9d..48273349a 100644
--- a/src/components/ShowcaseSection.tsx
+++ b/src/components/ShowcaseSection.tsx
@@ -9,7 +9,7 @@ import {
import { voteShowcase } from '~/utils/showcase.functions'
import { ShowcaseCard, ShowcaseCardSkeleton } from './ShowcaseCard'
import { Button } from '~/ui'
-import { ArrowRight, Plus } from 'lucide-react'
+import { ArrowRight, Plus } from '@phosphor-icons/react'
import { useCurrentUser } from '~/hooks/useCurrentUser'
import { useLoginModal } from '~/contexts/LoginModalContext'
import type { LibraryId } from '~/libraries'
diff --git a/src/components/ShowcaseSubmitForm.tsx b/src/components/ShowcaseSubmitForm.tsx
index 1b33ccdb2..ba9e90e2a 100644
--- a/src/components/ShowcaseSubmitForm.tsx
+++ b/src/components/ShowcaseSubmitForm.tsx
@@ -12,7 +12,7 @@ import {
USE_CASE_LABELS,
} from '~/utils/showcase.shared'
import { useToast } from './ToastProvider'
-import { Check, AlertCircle } from 'lucide-react'
+import { Check, WarningCircle } from '@phosphor-icons/react'
import { Button, FormInput } from '~/ui'
import { ImageUpload } from './ImageUpload'
import { FormEvent, useMemo, useState } from 'react'
@@ -377,7 +377,7 @@ export function ShowcaseSubmitForm({ showcase }: ShowcaseSubmitFormProps) {
{selectedLibraries.length === 0 && (
-
+
Select at least one library
)}
diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx
index ebb491e6c..1121a7b3c 100644
--- a/src/components/Spinner.tsx
+++ b/src/components/Spinner.tsx
@@ -1,5 +1,5 @@
import { twMerge } from 'tailwind-merge'
-import { Loader2 } from 'lucide-react'
+import { CircleNotch } from '@phosphor-icons/react'
interface SpinnerProps {
className?: string
@@ -7,7 +7,7 @@ interface SpinnerProps {
export function Spinner({ className }: SpinnerProps) {
return (
-
{children}
- {sortDirection === 'asc' && }
- {sortDirection === 'desc' && }
+ {sortDirection === 'asc' && }
+ {sortDirection === 'desc' && }
{!sortDirection && (
-
+
)}
diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx
index 1209f67fa..0eb23bcf0 100644
--- a/src/components/ThemeToggle.tsx
+++ b/src/components/ThemeToggle.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
import { useTheme } from './ThemeProvider'
-import { Moon, Sun, SunMoon } from 'lucide-react'
+import { Moon, Sun, SunHorizon } from '@phosphor-icons/react'
import { Button } from '~/ui'
export function ThemeToggle() {
@@ -41,7 +41,7 @@ export function ThemeToggle() {
aria-hidden="true"
className="grid h-3.5 w-3.5 shrink-0 place-items-center"
>
-
+
diff --git a/src/components/TocMobile.tsx b/src/components/TocMobile.tsx
index 0b88e2122..12c53131c 100644
--- a/src/components/TocMobile.tsx
+++ b/src/components/TocMobile.tsx
@@ -1,7 +1,7 @@
import React from 'react'
import { Link } from '@tanstack/react-router'
import type { MarkdownHeading } from '~/utils/markdown'
-import { ChevronDown, ChevronRight } from 'lucide-react'
+import { CaretDown, CaretRight } from '@phosphor-icons/react'
interface TocMobileProps {
headings: Array
@@ -26,7 +26,7 @@ export function TocMobile({ headings }: TocMobileProps) {
rounded-b-xl"
aria-expanded={isOpen}
>
- {isOpen ? : }
+ {isOpen ? : }
On this page
diff --git a/src/components/UserFeedbackSection.tsx b/src/components/UserFeedbackSection.tsx
index f2e9e8205..d9fa6da02 100644
--- a/src/components/UserFeedbackSection.tsx
+++ b/src/components/UserFeedbackSection.tsx
@@ -4,7 +4,12 @@ import { getUserDocFeedbackQueryOptions } from '~/queries/docFeedback'
import { PaginationControls } from './PaginationControls'
import { Spinner } from './Spinner'
import { calculatePoints } from '~/utils/docFeedback.shared'
-import { Award, ExternalLink, Lightbulb, MessageSquare } from 'lucide-react'
+import {
+ Medal,
+ ArrowSquareOut,
+ Lightbulb,
+ ChatCentered,
+} from '@phosphor-icons/react'
import { Badge } from '~/ui'
import { useState } from 'react'
@@ -38,7 +43,7 @@ export function UserFeedbackSection(_props: UserFeedbackSectionProps) {
-
+
Feedback Points
@@ -82,7 +87,7 @@ export function UserFeedbackSection(_props: UserFeedbackSectionProps) {
className="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 flex items-center gap-1"
>
View Leaderboard
-
+
@@ -120,7 +125,7 @@ export function UserFeedbackSection(_props: UserFeedbackSectionProps) {
{/* Icon */}
{item.type === 'note' ? (
-
+
) : (
)}
diff --git a/src/components/VersionSelect.tsx b/src/components/VersionSelect.tsx
index f6d79fed7..11a77cf2e 100644
--- a/src/components/VersionSelect.tsx
+++ b/src/components/VersionSelect.tsx
@@ -1,7 +1,7 @@
import * as React from 'react'
import { create } from 'zustand'
import { useNavigate, useParams } from '@tanstack/react-router'
-import { Tag } from 'lucide-react'
+import { Tag } from '@phosphor-icons/react'
import { Select, SelectOption } from './Select'
import { getLibrary, LibraryId } from '~/libraries'
import {
diff --git a/src/components/account/AccountProfilePictureSection.client.tsx b/src/components/account/AccountProfilePictureSection.client.tsx
index 87e40815c..d713fd038 100644
--- a/src/components/account/AccountProfilePictureSection.client.tsx
+++ b/src/components/account/AccountProfilePictureSection.client.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
import { useQueryClient } from '@tanstack/react-query'
-import { Camera, RotateCcw, Trash2 } from 'lucide-react'
+import { Camera, ArrowCounterClockwise, Trash } from '@phosphor-icons/react'
import { Avatar } from '~/components/Avatar'
import { AvatarCropModal } from '~/components/AvatarCropModal'
import { useToast } from '~/components/ToastProvider'
@@ -204,7 +204,7 @@ export function AccountProfilePictureSection({
onClick={handleRevertToOAuth}
disabled={isReverting}
>
-
+
{isReverting ? 'Reverting...' : 'Revert to original'}
)}
@@ -215,7 +215,7 @@ export function AccountProfilePictureSection({
onClick={handleRemovePhoto}
disabled={isRemoving}
>
-
+
{isRemoving ? 'Removing...' : 'Remove photo'}
)}
diff --git a/src/components/admin/AdminAccessDenied.tsx b/src/components/admin/AdminAccessDenied.tsx
index 48a6d6876..9ba7ab6da 100644
--- a/src/components/admin/AdminAccessDenied.tsx
+++ b/src/components/admin/AdminAccessDenied.tsx
@@ -1,5 +1,5 @@
import { Link } from '@tanstack/react-router'
-import { Lock } from 'lucide-react'
+import { Lock } from '@phosphor-icons/react'
import { Button } from '~/ui'
export function AdminAccessDenied() {
diff --git a/src/components/admin/DocsCacheTab.tsx b/src/components/admin/DocsCacheTab.tsx
index b0911cd23..e90c71404 100644
--- a/src/components/admin/DocsCacheTab.tsx
+++ b/src/components/admin/DocsCacheTab.tsx
@@ -1,5 +1,11 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
-import { BookOpen, Clock3, Database, GitBranch, RefreshCw } from 'lucide-react'
+import {
+ BookOpen,
+ Clock,
+ Database,
+ GitBranch,
+ ArrowsClockwise,
+} from '@phosphor-icons/react'
import {
AdminEmptyState,
AdminLoading,
@@ -124,7 +130,7 @@ export function DocsCacheTab() {
}
title="Mark every cached docs row stale so the next docs request repopulates it"
>
-
{invalidatingAll ? 'Invalidating...' : 'Invalidate All Docs'}
@@ -200,7 +206,7 @@ export function DocsCacheTab() {
}
+ icon={
}
/>
@@ -307,7 +313,7 @@ export function DocsCacheTab() {
}
title={`Mark ${repo.repo} docs cache entries stale`}
>
-
-
+
TanStack {library.label}
@@ -438,7 +438,7 @@ function StarterPartnerTooltip({
return (
-
+
{partner.label}
{partner.tags.length > 0 ? (
diff --git a/src/components/builder/BuilderSummary.tsx b/src/components/builder/BuilderSummary.tsx
index f73c639f0..ac2627e95 100644
--- a/src/components/builder/BuilderSummary.tsx
+++ b/src/components/builder/BuilderSummary.tsx
@@ -7,9 +7,9 @@ import {
FileText,
Palette,
Rocket,
- Settings2,
+ GearSix,
Terminal,
-} from 'lucide-react'
+} from '@phosphor-icons/react'
import {
useAvailableExamples,
useAvailableFeatures,
@@ -270,7 +270,7 @@ export function BuilderSummaryPanel({
) : null}
{summary.toolingFeatures.length > 0 ? (
feature.name)
.join(', ')}
diff --git a/src/components/builder/BuilderWorkspace.tsx b/src/components/builder/BuilderWorkspace.tsx
index c8b216bd6..750e5aed3 100644
--- a/src/components/builder/BuilderWorkspace.tsx
+++ b/src/components/builder/BuilderWorkspace.tsx
@@ -1,5 +1,5 @@
import { useCallback, useMemo, useState } from 'react'
-import { HelpCircle, X } from 'lucide-react'
+import { Question, X } from '@phosphor-icons/react'
import { useBuilderStore } from './store'
import { useBuilderUrl } from './useBuilderUrl'
import { BuilderSummaryPanel, useBuilderSummaryData } from './BuilderSummary'
@@ -153,7 +153,7 @@ export function BuilderWorkspace() {
{showIntro ? (
) : (
-
+
)}
}
diff --git a/src/components/builder/CustomAddonDialog.tsx b/src/components/builder/CustomAddonDialog.tsx
index c7465cc9a..9f86b2fee 100644
--- a/src/components/builder/CustomAddonDialog.tsx
+++ b/src/components/builder/CustomAddonDialog.tsx
@@ -9,12 +9,12 @@ import { Link } from '@tanstack/react-router'
import { twMerge } from 'tailwind-merge'
import {
Plus,
- Loader2,
- AlertCircle,
+ CircleNotch,
+ WarningCircle,
X,
- ExternalLink,
- HelpCircle,
-} from 'lucide-react'
+ ArrowSquareOut,
+ Question,
+} from '@phosphor-icons/react'
import { useBuilderStore, useTailwind } from './store'
import type { IntegrationCompiled } from '~/builder/api'
@@ -117,7 +117,11 @@ export function CustomAddonDialog({ onClose }: CustomAddonDialogProps) {
: 'bg-cyan-500 text-white hover:bg-cyan-600',
)}
>
- {isLoading ? : 'Load'}
+ {isLoading ? (
+
+ ) : (
+ 'Load'
+ )}
@@ -129,7 +133,7 @@ export function CustomAddonDialog({ onClose }: CustomAddonDialogProps) {
{/* Error */}
{error && (
)}
@@ -153,7 +157,7 @@ export function CustomAddonDialog({ onClose }: CustomAddonDialogProps) {
rel="noopener noreferrer"
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
-
+
)}
@@ -231,7 +235,7 @@ export function CustomAddonButton() {
onClick={(e) => e.stopPropagation()}
className="p-0.5 hover:text-blue-600 dark:hover:text-cyan-400"
>
-
+
>
)}
diff --git a/src/components/builder/CustomTemplateDialog.tsx b/src/components/builder/CustomTemplateDialog.tsx
index 2aa1c417c..bee907d8f 100644
--- a/src/components/builder/CustomTemplateDialog.tsx
+++ b/src/components/builder/CustomTemplateDialog.tsx
@@ -8,12 +8,12 @@ import { useState } from 'react'
import { Link } from '@tanstack/react-router'
import { twMerge } from 'tailwind-merge'
import {
- Loader2,
- AlertCircle,
+ CircleNotch,
+ WarningCircle,
Link as LinkIcon,
X,
- HelpCircle,
-} from 'lucide-react'
+ Question,
+} from '@phosphor-icons/react'
import { useBuilderStore, useCustomTemplate } from './store'
import type { CustomTemplateCompiled } from '~/builder/api'
@@ -108,7 +108,11 @@ export function CustomTemplateDialog({ onClose }: CustomTemplateDialogProps) {
: 'bg-cyan-500 text-white hover:bg-cyan-600',
)}
>
- {isLoading ? : 'Load'}
+ {isLoading ? (
+
+ ) : (
+ 'Load'
+ )}
@@ -119,7 +123,7 @@ export function CustomTemplateDialog({ onClose }: CustomTemplateDialogProps) {
{/* Error */}
{error && (
)}
@@ -227,7 +231,7 @@ export function CustomTemplateSection() {
onClick={(e) => e.stopPropagation()}
className="p-0.5 hover:text-blue-600 dark:hover:text-cyan-400"
>
-
+
>
)}
@@ -323,7 +327,7 @@ export function CustomTemplateItem({
onClick={(e) => e.stopPropagation()}
className="p-0.5 text-gray-400 hover:text-blue-600 dark:hover:text-cyan-400"
>
-
+
diff --git a/src/components/builder/DeployDialog.tsx b/src/components/builder/DeployDialog.tsx
index f52a6cd59..682f750e9 100644
--- a/src/components/builder/DeployDialog.tsx
+++ b/src/components/builder/DeployDialog.tsx
@@ -6,14 +6,14 @@
import { useState, useEffect, useCallback } from 'react'
import {
- Loader2,
- ExternalLink,
- AlertCircle,
+ CircleNotch,
+ ArrowSquareOut,
+ WarningCircle,
Check,
Lock,
Globe,
Rocket,
-} from 'lucide-react'
+} from '@phosphor-icons/react'
import { twMerge } from 'tailwind-merge'
import { useAsyncDebouncer } from '@tanstack/react-pacer'
import { Button, GitHub } from '~/ui'
@@ -343,7 +343,7 @@ export function DeployDialog({
{state.step === 'auth-check' && (
-
+
Checking authentication...
@@ -400,14 +400,14 @@ export function DeployDialog({
/>
{repoNameStatus === 'checking' && (
-
+
)}
{repoNameStatus === 'available' && (
)}
{(repoNameStatus === 'taken' ||
repoNameStatus === 'invalid') && (
-
+
)}
@@ -483,7 +483,7 @@ export function DeployDialog({
{state.step === 'deploying' && (
-
+
{state.message}
@@ -508,7 +508,7 @@ export function DeployDialog({
className="text-sm text-blue-600 dark:text-cyan-400 hover:underline flex items-center gap-1 mb-4"
>
{state.owner}/{state.repoName}
-
+
{providerInfo ? (
<>
@@ -554,7 +554,7 @@ export function DeployDialog({
{state.step === 'error' && (
Deployment Failed
diff --git a/src/components/builder/FeaturePicker.tsx b/src/components/builder/FeaturePicker.tsx
index 883c7e48f..75d09b1b8 100644
--- a/src/components/builder/FeaturePicker.tsx
+++ b/src/components/builder/FeaturePicker.tsx
@@ -10,7 +10,13 @@
import { useState, useMemo } from 'react'
import { twMerge } from 'tailwind-merge'
-import { Star, ExternalLink, ChevronDown, Sparkles, X } from 'lucide-react'
+import {
+ Star,
+ ArrowSquareOut,
+ CaretDown,
+ Sparkle,
+ X,
+} from '@phosphor-icons/react'
import { matchSorter } from 'match-sorter'
import {
useBuilderStore,
@@ -340,7 +346,7 @@ function FeatureSection({
{description}
-
-
+
)}
@@ -576,7 +582,7 @@ function ExamplePicker() {
Start with a complete demo app
-
-
+
{/* Content */}
diff --git a/src/components/charts/ChartControls.tsx b/src/components/charts/ChartControls.tsx
index 22d13bc2a..3044cda0e 100644
--- a/src/components/charts/ChartControls.tsx
+++ b/src/components/charts/ChartControls.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import { ChevronDown } from 'lucide-react'
+import { CaretDown } from '@phosphor-icons/react'
import {
DropdownMenu,
DropdownMenuContent,
@@ -47,7 +47,7 @@ export function ChartControls({
@@ -76,7 +76,7 @@ export function ChartControls({
)}
>
{binningOptions.find((b) => b.value === binType)?.label}
-
+
diff --git a/src/components/ds/DsKit.tsx b/src/components/ds/DsKit.tsx
new file mode 100644
index 000000000..5b4b8b7ec
--- /dev/null
+++ b/src/components/ds/DsKit.tsx
@@ -0,0 +1,232 @@
+import * as React from 'react'
+import { twMerge } from 'tailwind-merge'
+import { Check, CaretDown, Code, Copy } from '@phosphor-icons/react'
+import { copyTextToClipboard } from '~/utils/browser-effects'
+
+/**
+ * Presentational building blocks for the Design System pages (`/ds`).
+ *
+ * These are intentionally generic so that adding a new section or component
+ * demo is just a matter of composing `DsPage` > `DsSection` > `ComponentPreview`.
+ * Previews follow the global site theme (toggle it from the navbar) so what you
+ * see is exactly what ships.
+ */
+
+export function DsPage({
+ title,
+ description,
+ children,
+}: {
+ title: string
+ description?: React.ReactNode
+ children: React.ReactNode
+}) {
+ return (
+
+
+
+ {title}
+
+ {description ? (
+
+ {description}
+
+ ) : null}
+
+
{children}
+
+ )
+}
+
+export function DsSection({
+ title,
+ description,
+ children,
+}: {
+ title: string
+ description?: React.ReactNode
+ children: React.ReactNode
+}) {
+ return (
+
+
+
+ {title}
+
+ {description ? (
+
+ {description}
+
+ ) : null}
+
+ {children}
+
+ )
+}
+
+/**
+ * A live preview surface with an optional, copyable code snippet — the
+ * copy-paste ("registry") mechanism for Phase 1. Each demo shows the rendered
+ * component on a contrasting surface and lets maintainers grab the source.
+ */
+export function ComponentPreview({
+ title,
+ description,
+ code,
+ children,
+ className,
+}: {
+ title?: string
+ description?: string
+ code?: string
+ children: React.ReactNode
+ className?: string
+}) {
+ const [showCode, setShowCode] = React.useState(false)
+ const [copied, setCopied] = React.useState(false)
+
+ const handleCopy = React.useCallback(async () => {
+ if (!code) return
+ await copyTextToClipboard(code)
+ setCopied(true)
+ window.setTimeout(() => setCopied(false), 1500)
+ }, [code])
+
+ const hasHeader = Boolean(title || description || code)
+
+ return (
+
+ {hasHeader ? (
+
+
+ {title ? (
+
+ {title}
+
+ ) : null}
+ {description ? (
+
+ {description}
+
+ ) : null}
+
+ {code ? (
+
+
+
+
+ ) : null}
+
+ ) : null}
+
+
+ {children}
+
+
+ {code && showCode ? (
+
+ {code}
+
+ ) : null}
+
+ )
+}
+
+/** A small labeled chunk inside a preview, e.g. to caption a row of variants. */
+export function PreviewLabel({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
+
+/**
+ * A color swatch driven entirely by a CSS custom property token (e.g.
+ * `twine-500` → `var(--color-twine-500)`). The hex label is read from the live
+ * computed value so swatches never drift from `@theme` in app.css. Clicking
+ * copies the CSS variable reference.
+ */
+function rgbToHex(value: string): string {
+ const match = value.match(/rgba?\(([^)]+)\)/)
+ if (!match) return value
+ const parts = match[1].split(',').map((p) => p.trim())
+ const [r, g, b] = parts
+ const toHex = (n: string) => Number(n).toString(16).padStart(2, '0')
+ return `#${toHex(r)}${toHex(g)}${toHex(b)}`
+}
+
+export function Swatch({ token }: { token: string }) {
+ const [hex, setHex] = React.useState('')
+ const [copied, setCopied] = React.useState(false)
+ const swatchRef = React.useRef(null)
+
+ React.useEffect(() => {
+ if (!swatchRef.current) return
+ // Read the *computed* background so var()-referencing semantic tokens
+ // resolve to a real color, not the literal "var(--…)" declaration.
+ const resolved = getComputedStyle(swatchRef.current).backgroundColor
+ setHex(rgbToHex(resolved))
+ }, [token])
+
+ const handleCopy = React.useCallback(async () => {
+ await copyTextToClipboard(`var(--color-${token})`)
+ setCopied(true)
+ window.setTimeout(() => setCopied(false), 1200)
+ }, [token])
+
+ return (
+
+ )
+}
diff --git a/src/components/ds/ds-nav.ts b/src/components/ds/ds-nav.ts
new file mode 100644
index 000000000..f36c263d2
--- /dev/null
+++ b/src/components/ds/ds-nav.ts
@@ -0,0 +1,53 @@
+export interface DsNavItem {
+ label: string
+ to: string
+}
+
+export interface DsNavSection {
+ title: string
+ items: Array
+}
+
+/**
+ * Left-hand tree navigation for the Design System (`/ds`).
+ *
+ * This is the single source of truth for the sidebar. Adding a new page is a
+ * two-step change: drop a `ds..tsx` route in `src/routes` and add an
+ * entry here. Keep `Styles` (design tokens) above `Components` (rendered UI).
+ */
+export const dsNav: Array = [
+ {
+ title: 'Styles',
+ items: [
+ { label: 'Overview', to: '/ds' },
+ { label: 'Colors', to: '/ds/colors' },
+ { label: 'Typography', to: '/ds/typography' },
+ { label: 'Iconography', to: '/ds/iconography' },
+ { label: 'Icon Migration', to: '/ds/icon-migration' },
+ { label: 'Shadows', to: '/ds/shadows' },
+ { label: 'Effects', to: '/ds/effects' },
+ ],
+ },
+ {
+ title: 'Figma Tokens',
+ items: [
+ { label: 'Palette', to: '/ds/palette' },
+ { label: 'Semantic Tokens', to: '/ds/semantic' },
+ ],
+ },
+ {
+ title: 'Components',
+ items: [
+ { label: 'Buttons', to: '/ds/buttons' },
+ { label: 'Badges', to: '/ds/badges' },
+ { label: 'Inputs', to: '/ds/inputs' },
+ { label: 'Dropdown', to: '/ds/dropdown' },
+ { label: 'Avatar', to: '/ds/avatar' },
+ { label: 'Spinner', to: '/ds/spinner' },
+ { label: 'Collapsible', to: '/ds/collapsible' },
+ { label: 'Breadcrumbs', to: '/ds/breadcrumbs' },
+ { label: 'Cards & Surfaces', to: '/ds/cards' },
+ { label: 'Navbar', to: '/ds/navbar' },
+ ],
+ },
+]
diff --git a/src/components/ds/ui/PalmSpinner.tsx b/src/components/ds/ui/PalmSpinner.tsx
new file mode 100644
index 000000000..b8788f4eb
--- /dev/null
+++ b/src/components/ds/ui/PalmSpinner.tsx
@@ -0,0 +1,117 @@
+import { twMerge } from 'tailwind-merge'
+
+/**
+ * A branded, pixel-art palm-tree loading indicator that sways like a palm in
+ * the wind (anchored at the trunk base). Unlike the generic `Spinner`, this is
+ * intentionally multi-color — it does not inherit `currentColor`. Size it with
+ * w-/h- utilities. The sway keyframes live in app.css (`.animate-palm-sway`).
+ */
+export function PalmSpinner({ className }: { className?: string }) {
+ return (
+
+ )
+}
diff --git a/src/components/ds/ui/index.tsx b/src/components/ds/ui/index.tsx
new file mode 100644
index 000000000..8184a2dd4
--- /dev/null
+++ b/src/components/ds/ui/index.tsx
@@ -0,0 +1,523 @@
+import * as React from 'react'
+import { twMerge } from 'tailwind-merge'
+import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
+import { Link } from '@tanstack/react-router'
+import { CaretDown, CircleNotch, User } from '@phosphor-icons/react'
+import type { MarkdownHeading } from '~/utils/markdown'
+
+/**
+ * New-system DS components — built on the Figma semantic tokens (action-*,
+ * background-*, text-*, border-*, status-*) so they respond to the theme and
+ * carry the new brand palette. Used by the /ds style book and /staging pages.
+ *
+ * These intentionally MIRROR the production components' APIs (~/ui, ~/components)
+ * so pages can swap imports 1:1, but they do NOT touch production — the live
+ * site keeps its current components until these are promoted.
+ */
+
+/* ----------------------------------------------------------------- Button -- */
+
+type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'icon' | 'link'
+type ButtonColor =
+ | 'blue'
+ | 'green'
+ | 'red'
+ | 'orange'
+ | 'purple'
+ | 'gray'
+ | 'emerald'
+ | 'cyan'
+ | 'yellow'
+type ButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'icon-sm' | 'icon-md'
+type ButtonRounded = 'none' | 'md' | 'lg' | 'full'
+
+interface ButtonProps extends React.ButtonHTMLAttributes {
+ variant?: ButtonVariant
+ color?: ButtonColor
+ size?: ButtonSize
+ rounded?: ButtonRounded
+}
+
+// Color set mapped onto the new palette (brand blue = the teal #013e53).
+const primaryColorStyles: Record = {
+ 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',
+ orange:
+ 'bg-ds-terracotta-300 text-white border-ds-terracotta-300 hover: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',
+ 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',
+ yellow:
+ 'bg-ds-amber-300 text-ds-neutral-500 border-ds-amber-300 hover:bg-ds-amber-200',
+}
+
+const iconColorStyles: Record = {
+ 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',
+}
+
+const linkColorStyles: Record = {
+ 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',
+}
+
+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)]',
+ 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',
+ ghost:
+ 'border border-border-default text-text-primary hover:bg-background-subtle hover:border-border-strong font-medium hover:shadow-sm',
+ icon: 'border-transparent active:scale-90',
+ link: 'border-transparent font-medium underline-offset-2 hover:underline',
+}
+
+const sizeStyles: Record = {
+ xs: 'px-2 py-1.5 text-xs',
+ sm: 'px-3 py-1 text-sm',
+ md: 'px-4 py-2 text-sm',
+ lg: 'px-6 py-3 text-base',
+ 'icon-sm': 'p-1.5',
+ 'icon-md': 'p-2',
+}
+
+const roundedStyles: Record = {
+ none: 'rounded-none',
+ md: 'rounded-md',
+ lg: 'rounded-lg',
+ full: 'rounded-full',
+}
+
+const baseStyles =
+ 'inline-flex items-center justify-center gap-2 cursor-pointer transition-all duration-150 ease-out disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none disabled:hover:translate-y-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-1 focus-visible:ring-offset-background-default'
+
+function getDefaultSize(variant: ButtonVariant): ButtonSize {
+ if (variant === 'icon') return 'icon-md'
+ return 'md'
+}
+
+function getDefaultRounded(size: ButtonSize): ButtonRounded {
+ if (size === 'xs' || size === 'sm') return 'md'
+ return 'lg'
+}
+
+export const Button = React.forwardRef(
+ function Button(
+ {
+ children,
+ variant = 'primary',
+ color = 'blue',
+ size,
+ rounded,
+ className,
+ ...rest
+ },
+ ref,
+ ) {
+ const resolvedSize = size ?? getDefaultSize(variant)
+ const resolvedRounded = rounded ?? getDefaultRounded(resolvedSize)
+ const colorStyles =
+ variant === 'primary'
+ ? primaryColorStyles[color]
+ : variant === 'icon'
+ ? iconColorStyles[color]
+ : variant === 'link'
+ ? linkColorStyles[color]
+ : ''
+
+ return (
+
+ )
+ },
+)
+
+/* ------------------------------------------------------------------ Badge -- */
+
+type BadgeVariant =
+ | 'default'
+ | 'success'
+ | 'warning'
+ | 'error'
+ | 'info'
+ | 'purple'
+ | 'teal'
+ | 'orange'
+
+const badgeVariantStyles: Record = {
+ default: 'bg-background-subtle text-text-secondary',
+ success: 'bg-status-success-bg text-text-success',
+ warning: 'bg-status-warning-bg text-text-warning',
+ error: 'bg-status-error-bg text-text-error',
+ info: 'bg-status-info-bg text-text-info',
+ purple: 'bg-accent-creative/15 text-accent-creative',
+ teal: 'bg-lib-start/15 text-lib-start',
+ orange: 'bg-accent-warm/20 text-accent-warm',
+}
+
+export function Badge({
+ children,
+ variant = 'default',
+ className,
+}: {
+ children: React.ReactNode
+ variant?: BadgeVariant
+ className?: string
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+/* -------------------------------------------------------------- FormInput -- */
+
+type FormInputProps = React.InputHTMLAttributes & {
+ focusRing?: 'blue' | 'orange' | 'purple'
+}
+
+const ringStyles: Record, string> = {
+ blue: 'focus:border-border-focus focus:ring-border-focus/40',
+ orange: 'focus:border-accent-warm focus:ring-accent-warm/40',
+ purple: 'focus:border-accent-creative focus:ring-accent-creative/40',
+}
+
+export const FormInput = React.forwardRef(
+ function FormInput({ className, focusRing = 'blue', ...props }, ref) {
+ return (
+
+ )
+ },
+)
+
+/* ------------------------------------------------------------------- Card -- */
+
+export function Card({
+ children,
+ className,
+}: {
+ children: React.ReactNode
+ className?: string
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+/* ------------------------------------------------------------- InlineCode -- */
+
+export function InlineCode({
+ className,
+ ...rest
+}: React.HTMLProps) {
+ return (
+
+ )
+}
+
+/* ---------------------------------------------------------------- Spinner -- */
+
+export function Spinner({ className }: { className?: string }) {
+ return (
+
+ )
+}
+
+/* ----------------------------------------------------------------- Avatar -- */
+
+type AvatarSize = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'
+
+const avatarSizeClasses: Record<
+ AvatarSize,
+ { container: string; text: string }
+> = {
+ '2xs': { container: 'w-4 h-4', text: 'text-[8px]' },
+ xs: { container: 'w-6 h-6', text: 'text-xs' },
+ sm: { container: 'w-8 h-8', text: 'text-sm' },
+ md: { container: 'w-10 h-10', text: 'text-base' },
+ lg: { container: 'w-14 h-14', text: 'text-lg' },
+ xl: { container: 'w-20 h-20', text: 'text-xl' },
+}
+
+function getInitials(name?: string | null, email?: string | null): string {
+ if (name) {
+ const parts = name.trim().split(/\s+/)
+ if (parts.length >= 2) {
+ return `${parts[0]![0]}${parts[parts.length - 1]![0]}`.toUpperCase()
+ }
+ return name.slice(0, 2).toUpperCase()
+ }
+ if (email) return email.slice(0, 2).toUpperCase()
+ return ''
+}
+
+export function Avatar({
+ image,
+ oauthImage,
+ name,
+ email,
+ size = 'md',
+ className = '',
+}: {
+ image?: string | null
+ oauthImage?: string | null
+ name?: string | null
+ email?: string | null
+ size?: AvatarSize
+ className?: string
+}) {
+ const displayImage = image || oauthImage
+ const initials = getInitials(name, email)
+ const { container, text } = avatarSizeClasses[size]
+
+ if (displayImage) {
+ return (
+
+ )
+ }
+
+ return (
+
+ {initials || }
+
+ )
+}
+
+/* --------------------------------------------------------------- Dropdown -- */
+
+export function Dropdown({
+ children,
+ open,
+ onOpenChange,
+ modal = false,
+}: {
+ children: React.ReactNode
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ modal?: boolean
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function DropdownTrigger({
+ children,
+ className,
+ asChild = true,
+}: {
+ children: React.ReactNode
+ className?: string
+ asChild?: boolean
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function DropdownContent({
+ children,
+ className,
+ align = 'end',
+ sideOffset = 6,
+}: {
+ children: React.ReactNode
+ className?: string
+ align?: 'start' | 'center' | 'end'
+ sideOffset?: number
+}) {
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+export function DropdownItem({
+ children,
+ className,
+ onSelect,
+ asChild,
+}: {
+ children: React.ReactNode
+ className?: string
+ onSelect?: () => void
+ asChild?: boolean
+}) {
+ return (
+
+ {children}
+
+ )
+}
+
+export function DropdownSeparator({ className }: { className?: string }) {
+ return (
+
+ )
+}
+
+/* ------------------------------------------------------------ Breadcrumbs -- */
+
+export function Breadcrumbs({
+ section,
+ sectionTo,
+ headings,
+ tocHiddenBreakpoint = 'lg',
+}: {
+ section: string
+ sectionTo?: string
+ headings?: Array
+ tocHiddenBreakpoint?: 'md' | 'lg' | 'xl'
+}) {
+ const showTocToggle = headings && headings.length > 1
+ const hiddenClass =
+ tocHiddenBreakpoint === 'md'
+ ? 'md:hidden'
+ : tocHiddenBreakpoint === 'xl'
+ ? 'xl:hidden'
+ : 'lg:hidden'
+
+ return (
+
+ {sectionTo ? (
+
+ {section}
+
+ ) : (
+ {section}
+ )}
+ {showTocToggle ? (
+
+
+
+
+
+ {headings.map((heading) => (
+
+
+
+
+
+ ))}
+
+
+ ) : null}
+
+ )
+}
+
+export { PalmSpinner } from './PalmSpinner'
diff --git a/src/components/game/engine/entities/Coins.ts b/src/components/game/engine/entities/Coins.ts
index 0cd01ae09..edf378228 100644
--- a/src/components/game/engine/entities/Coins.ts
+++ b/src/components/game/engine/entities/Coins.ts
@@ -132,7 +132,7 @@ export class Coins {
resolve([goldMaterial, goldMaterial, goldMaterial])
}
- img.src = '/images/logos/logo-black.svg'
+ img.src = '/images/brand/tanstack-emblem-black.svg'
})
}
diff --git a/src/components/game/ui/CompleteOverlay.tsx b/src/components/game/ui/CompleteOverlay.tsx
index 94e213c21..3470713d2 100644
--- a/src/components/game/ui/CompleteOverlay.tsx
+++ b/src/components/game/ui/CompleteOverlay.tsx
@@ -1,6 +1,6 @@
import { Link } from '@tanstack/react-router'
import { useGameStore } from '../hooks/useGameStore'
-import { Trophy, RotateCcw, Home } from 'lucide-react'
+import { Trophy, ArrowCounterClockwise, House } from '@phosphor-icons/react'
export function CompleteOverlay() {
const { phase, reset, setPhase } = useGameStore()
@@ -35,14 +35,14 @@ export function CompleteOverlay() {
onClick={handlePlayAgain}
className="flex items-center justify-center gap-2 px-6 py-3 bg-white text-orange-600 font-bold rounded-xl shadow-lg hover:bg-orange-50 hover:scale-105 transition-all duration-200"
>
-
+
Explore Again
-
+
Back to TanStack
diff --git a/src/components/game/ui/GameHUD.tsx b/src/components/game/ui/GameHUD.tsx
index a72c26aac..9c9c18657 100644
--- a/src/components/game/ui/GameHUD.tsx
+++ b/src/components/game/ui/GameHUD.tsx
@@ -1,7 +1,12 @@
/* eslint-disable react-hooks/set-state-in-effect -- game animation state sync */
import { useEffect, useState } from 'react'
import { useGameStore } from '../hooks/useGameStore'
-import { Map, Coins, ShoppingBag, Zap } from 'lucide-react'
+import {
+ MapTrifold,
+ Coins,
+ ShoppingBag,
+ Lightning,
+} from '@phosphor-icons/react'
export function GameHUD() {
const {
@@ -163,7 +168,7 @@ export function GameHUD() {
{/* Island counter */}
-
+
{discoveredIslands.size} / {totalIslands}
@@ -184,7 +189,7 @@ export function GameHUD() {
className="md:hidden pointer-events-auto bg-red-500/80 backdrop-blur-sm rounded-full p-4 flex items-center justify-center text-white hover:bg-red-600/80 active:scale-95 transition-all shadow-lg"
aria-label="Fire cannons"
>
-
+
{/* Health bar */}
setShowConfirm(true)}
className="w-full flex items-center justify-center gap-1.5 text-[10px] text-white/30 hover:text-white/60 transition-colors"
>
-
+
Reset
) : (
diff --git a/src/components/home/HomeSocialProofSection.tsx b/src/components/home/HomeSocialProofSection.tsx
index 3f3210ec4..2e0341bf8 100644
--- a/src/components/home/HomeSocialProofSection.tsx
+++ b/src/components/home/HomeSocialProofSection.tsx
@@ -1,7 +1,7 @@
import { Link } from '@tanstack/react-router'
import { Hydrate } from '@tanstack/react-start'
import { visible } from '@tanstack/react-start/hydration'
-import { ArrowRight } from 'lucide-react'
+import { ArrowRight } from '@phosphor-icons/react'
import { Card } from '~/components/Card'
import { PartnersGrid } from '~/components/PartnersGrid'
import { Button } from '~/ui'
diff --git a/src/components/icons/CheckCircleIcon.tsx b/src/components/icons/CheckCircleIcon.tsx
deleted file mode 100644
index 58ef1372a..000000000
--- a/src/components/icons/CheckCircleIcon.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import * as React from 'react'
-
-export function CheckCircleIcon({
- className,
- width = '1em',
- height = '1em',
- ...props
-}: React.SVGProps
) {
- return (
-
- )
-}
diff --git a/src/components/icons/CogsIcon.tsx b/src/components/icons/CogsIcon.tsx
deleted file mode 100644
index 5ad4b159f..000000000
--- a/src/components/icons/CogsIcon.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import * as React from 'react'
-
-export function CogsIcon({
- className,
- width = '1em',
- height = '1em',
- ...props
-}: React.SVGProps) {
- return (
-
- )
-}
diff --git a/src/components/landing/AiLanding.tsx b/src/components/landing/AiLanding.tsx
index a54410668..9ef72e450 100644
--- a/src/components/landing/AiLanding.tsx
+++ b/src/components/landing/AiLanding.tsx
@@ -2,18 +2,18 @@ import * as React from 'react'
import { Link, useParams } from '@tanstack/react-router'
import {
ArrowRight,
- Bot,
+ Robot,
BookOpen,
GitBranch,
- Mic,
- MonitorCog,
+ Microphone,
+ MonitorArrowUp,
Plug,
Radio,
- ScanSearch,
- Sparkles,
- Split,
- WandSparkles,
-} from 'lucide-react'
+ MagnifyingGlass,
+ Sparkle,
+ ArrowsSplit,
+ MagicWand,
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { Footer } from '~/components/Footer'
@@ -126,12 +126,12 @@ const featureCards = [
{
title: 'Tools stay typed where they run.',
body: 'Define client, server, isomorphic, and provider-native tools with input/output types, approvals, and runtime boundaries that remain visible.',
- icon: ,
+ icon: ,
},
{
title: 'Media is part of the same SDK story.',
body: 'Text, structured output, reasoning streams, image, speech, transcription, realtime voice, and video can share provider-aware primitives.',
- icon: ,
+ icon: ,
},
]
@@ -191,7 +191,7 @@ export default function AiLanding() {
-
}>
+
}>
Open AI application SDK
@@ -254,7 +254,7 @@ export default function AiLanding() {
-
}>Why AI
+
}>Why AI
AI apps need protocols and boundaries, not another black box.
@@ -296,7 +296,7 @@ export default function AiLanding() {
-
}>
+
}>
Observable runtime
@@ -316,7 +316,7 @@ export default function AiLanding() {
-
}>
+
}>
Framework adapters
@@ -344,7 +344,7 @@ export default function AiLanding() {
-
}>
+
}>
Product control
diff --git a/src/components/landing/CliLanding.tsx b/src/components/landing/CliLanding.tsx
index bc787962d..b9303759d 100644
--- a/src/components/landing/CliLanding.tsx
+++ b/src/components/landing/CliLanding.tsx
@@ -2,16 +2,16 @@ import * as React from 'react'
import { Link, useParams } from '@tanstack/react-router'
import {
ArrowRight,
- Bot,
+ Robot,
BookOpen,
- FileCode2,
+ FileCode,
GitBranch,
- Layers,
- PackagePlus,
- Puzzle,
- Sparkles,
+ Stack,
+ Package,
+ PuzzlePiece,
+ Sparkle,
Terminal,
-} from 'lucide-react'
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { Footer } from '~/components/Footer'
@@ -62,17 +62,17 @@ const featureCards = [
{
title: 'CLI introspection turns docs into agent context.',
body: 'Use JSON commands for docs, libraries, add-ons, and ecosystem data so generated work can reference current TanStack conventions.',
- icon: ,
+ icon: ,
},
{
title: 'Integrations become selectable building blocks.',
body: 'Auth, databases, styling, deployment, and more can be composed into a Start-ready app without burying every choice in hand-written setup.',
- icon: ,
+ icon: ,
},
{
title: 'The Builder makes the stack visible.',
body: 'Use the web UI to select libraries and partners, preview generated choices, and export a plan the CLI or agent can execute.',
- icon: ,
+ icon: ,
},
]
@@ -167,7 +167,7 @@ export default function CliLanding() {
}
+ icon={}
/>
-
}>Why CLI
+
}>Why CLI
Project setup is where documentation meets the filesystem.
@@ -229,7 +229,7 @@ export default function CliLanding() {
-
}>
+
}>
Builder output
@@ -249,7 +249,7 @@ export default function CliLanding() {
-
}>
+
}>
Developer ergonomics
diff --git a/src/components/landing/ConfigLanding.tsx b/src/components/landing/ConfigLanding.tsx
index 5771d257d..98ee42411 100644
--- a/src/components/landing/ConfigLanding.tsx
+++ b/src/components/landing/ConfigLanding.tsx
@@ -3,16 +3,16 @@ import { Link, useParams } from '@tanstack/react-router'
import {
ArrowRight,
BookOpen,
- Boxes,
- ClipboardCheck,
- FileCheck2,
+ Stack,
+ ClipboardText,
+ FileText,
GitBranch,
- PackageCheck,
- ScanLine,
+ Package,
+ Scan,
ShieldCheck,
- Sparkles,
- TerminalSquare,
-} from 'lucide-react'
+ Sparkle,
+ Terminal,
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { Footer } from '~/components/Footer'
@@ -59,17 +59,17 @@ const featureCards = [
{
title: 'Opinionated where packages are repetitive.',
body: 'Linting, package builds, CI tasks, publishing, and release hygiene should not become bespoke work in every package repo.',
- icon: ,
+ icon: ,
},
{
title: 'Vite ecosystem without a hand-built pipeline.',
body: 'Use modern build primitives and package output conventions without rebuilding the same config stack for every library.',
- icon: ,
+ icon: ,
},
{
title: 'Publishing rules stay visible.',
body: 'Exports, package metadata, release branches, and npm publish behavior can be reviewed as part of the same workflow.',
- icon: ,
+ icon: ,
},
{
title: 'Minimal config, consistent results.',
@@ -125,7 +125,7 @@ export default function ConfigLanding() {
-
}>
+
}>
Package maintenance tooling
@@ -179,7 +179,7 @@ export default function ConfigLanding() {
-
}>
+
}>
Why Config
@@ -223,7 +223,7 @@ export default function ConfigLanding() {
-
}>
+
}>
Package audit
@@ -243,7 +243,7 @@ export default function ConfigLanding() {
-
}>
+
}>
Maintainer ergonomics
diff --git a/src/components/landing/DbLanding.tsx b/src/components/landing/DbLanding.tsx
index 2b39437c0..932c3ed49 100644
--- a/src/components/landing/DbLanding.tsx
+++ b/src/components/landing/DbLanding.tsx
@@ -3,17 +3,16 @@ import { Link, useParams } from '@tanstack/react-router'
import {
ArrowRight,
BookOpen,
- DatabaseZap,
- GalleryVerticalEnd,
+ Database,
+ Stack,
GitBranch,
- Layers,
Network,
- PlugZap,
- RefreshCcw,
- Sparkles,
- Split,
- Zap,
-} from 'lucide-react'
+ Plugs,
+ ArrowsCounterClockwise,
+ Sparkle,
+ ArrowsSplit,
+ Lightning,
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { Footer } from '~/components/Footer'
@@ -90,22 +89,22 @@ const featureCards = [
{
title: 'Collections make API data queryable.',
body: 'Load, sync, or persist typed records into collections, then query the data your UI actually needs instead of spreading derived state through components.',
- icon: ,
+ icon: ,
},
{
title: 'Live queries update the result, not the whole app.',
body: 'DB uses differential dataflow to recompute only the changed parts of joins, filters, and aggregates, so large local graphs still feel instant.',
- icon: ,
+ icon: ,
},
{
title: 'Local writes are first-class.',
body: 'Optimistic mutations can stage transactions across collections, update the UI immediately, then commit or rollback with lifecycle support.',
- icon: ,
+ icon: ,
},
{
title: 'Sync strategy can evolve with the product.',
body: 'Start with REST, GraphQL, tRPC, TanStack Query, Electric, PowerSync, Trailbase, or your own collection creator without changing how components query.',
- icon: ,
+ icon: ,
},
]
@@ -158,7 +157,7 @@ export default function DbLanding() {
-
}>
+
}>
Reactive client store
@@ -226,7 +225,7 @@ export default function DbLanding() {
-
}>Why DB
+
}>Why DB
Server state gets awkward when the UI needs a data graph.
@@ -289,7 +288,7 @@ export default function DbLanding() {
-
}>
+
}>
Framework adapters
@@ -316,7 +315,7 @@ export default function DbLanding() {
-
}>
+
}>
Product shape
diff --git a/src/components/landing/DevtoolsLanding.tsx b/src/components/landing/DevtoolsLanding.tsx
index e5d612503..396f81a02 100644
--- a/src/components/landing/DevtoolsLanding.tsx
+++ b/src/components/landing/DevtoolsLanding.tsx
@@ -3,17 +3,17 @@ import { Link, useParams } from '@tanstack/react-router'
import {
ArrowRight,
BookOpen,
- Dock,
+ AppWindow,
Eye,
Gauge,
- Layers,
- PanelRight,
+ Stack,
+ Sidebar,
Plug,
- Puzzle,
- ScanSearch,
- Sparkles,
- Workflow,
-} from 'lucide-react'
+ PuzzlePiece,
+ MagnifyingGlass,
+ Sparkle,
+ FlowArrow,
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { Footer } from '~/components/Footer'
@@ -67,12 +67,12 @@ const featureCards = [
{
title: 'A shared place for runtime truth.',
body: 'TanStack apps already have rich runtime state. Devtools gives that state a consistent panel instead of scattering inspectors across the UI.',
- icon: ,
+ icon: ,
},
{
title: 'Custom panels belong next to library panels.',
body: 'Register product-specific devtools beside Query and Router so teams can inspect the state that matters for their own workflow.',
- icon: ,
+ icon: ,
},
{
title: 'The shell stays light.',
@@ -82,7 +82,7 @@ const featureCards = [
{
title: 'Frameworks get adapters, not rewrites.',
body: 'Use the shell from React, Vue, Solid, Preact, Angular, or vanilla integration points while panels keep their own internal state model.',
- icon: ,
+ icon: ,
},
]
@@ -123,7 +123,7 @@ export default function DevtoolsLanding() {
-
}>
+
}>
Unified devtools shell
@@ -185,7 +185,7 @@ export default function DevtoolsLanding() {
-
}>
+
}>
Why Devtools
@@ -210,7 +210,7 @@ export default function DevtoolsLanding() {
-
}>
+
}>
Panel workflow
@@ -248,7 +248,7 @@ export default function DevtoolsLanding() {
-
}>
+
}>
Framework adapters
diff --git a/src/components/landing/FormLanding.tsx b/src/components/landing/FormLanding.tsx
index db4265610..ccd16c038 100644
--- a/src/components/landing/FormLanding.tsx
+++ b/src/components/landing/FormLanding.tsx
@@ -2,18 +2,18 @@ import * as React from 'react'
import { Link, useParams } from '@tanstack/react-router'
import {
ArrowRight,
- BadgeCheck,
+ SealCheck,
BookOpen,
- FileCheck2,
+ FileText,
Fingerprint,
Keyboard,
ListChecks,
- Loader2,
+ CircleNotch,
ShieldCheck,
SlidersHorizontal,
- Split,
- WandSparkles,
-} from 'lucide-react'
+ ArrowsSplit,
+ MagicWand,
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { Footer } from '~/components/Footer'
@@ -105,17 +105,17 @@ const featureCards = [
{
title: 'Headless composition keeps forms honest.',
body: 'Use components or hooks, but keep the real controls in your product UI. Labels, hints, validation states, layout, and accessibility remain yours.',
- icon: ,
+ icon: ,
},
{
title: 'Subscriptions make big forms feel small.',
body: 'A checkout, onboarding wizard, or admin editor can subscribe to narrow field and form state instead of repainting the whole surface on every keystroke.',
- icon: ,
+ icon: ,
},
{
title: 'Async validation is built for users.',
body: 'Debounced async checks, validation events, and pending states let the form stay responsive while slow business rules happen in the background.',
- icon: ,
+ icon: ,
},
]
@@ -224,7 +224,7 @@ export default function FormLanding() {
-
}>
+
}>
Why Form
@@ -321,7 +321,7 @@ export default function FormLanding() {
-
}>
+
}>
Field notes
diff --git a/src/components/landing/HotkeysLanding.tsx b/src/components/landing/HotkeysLanding.tsx
index a181db7a9..471001f98 100644
--- a/src/components/landing/HotkeysLanding.tsx
+++ b/src/components/landing/HotkeysLanding.tsx
@@ -5,12 +5,12 @@ import {
BookOpen,
Crosshair,
Keyboard,
- ListOrdered,
+ ListNumbers,
Monitor,
- Pointer,
+ Cursor,
Radio,
- Sparkles,
-} from 'lucide-react'
+ Sparkle,
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { Footer } from '~/components/Footer'
@@ -73,7 +73,7 @@ const featureCards = [
{
title: 'Sequences and holds unlock richer UI.',
body: 'Support Vim-style sequences, multi-step commands, key hold detection, and contextual command flows.',
- icon: ,
+ icon: ,
},
{
title: 'Recording belongs in the product.',
@@ -186,7 +186,7 @@ export default function HotkeysLanding() {
-
}>
+
}>
Why Hotkeys
@@ -229,7 +229,7 @@ export default function HotkeysLanding() {
-
}>
+
}>
Framework adapters
diff --git a/src/components/landing/IntentLanding.tsx b/src/components/landing/IntentLanding.tsx
index 7b94130a4..d67699001 100644
--- a/src/components/landing/IntentLanding.tsx
+++ b/src/components/landing/IntentLanding.tsx
@@ -4,16 +4,15 @@ import { useQuery } from '@tanstack/react-query'
import {
ArrowRight,
BookOpen,
- Bot,
- FileSearch,
+ Robot,
+ FileMagnifyingGlass,
GitBranch,
Package,
- PackageCheck,
- RefreshCw,
- ScanLine,
- Sparkles,
- WandSparkles,
-} from 'lucide-react'
+ ArrowsClockwise,
+ Scan,
+ Sparkle,
+ MagicWand,
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { Footer } from '~/components/Footer'
@@ -72,17 +71,17 @@ const featureCards = [
{
title: 'Discovery happens from node_modules.',
body: 'Install the package and compatible agents can find the skill metadata where the code already lives.',
- icon: ,
+ icon: ,
},
{
title: 'Source docs keep skills accountable.',
body: 'Skills declare the docs they depend on, so stale checks can flag them when the source material changes.',
- icon: ,
+ icon: ,
},
{
title: 'The registry makes the ecosystem visible.',
body: 'Packages, skills, versions, download signals, and history become browsable instead of hidden inside package tarballs.',
- icon: ,
+ icon: ,
},
]
@@ -114,7 +113,7 @@ export default function IntentLanding() {
-
}>
+
}>
Agent skills in npm
@@ -159,7 +158,7 @@ export default function IntentLanding() {
}
+ icon={
}
/>
-
}>
+
}>
Why Intent
@@ -227,7 +226,7 @@ export default function IntentLanding() {
-
}>
+
}>
Staleness checks
@@ -376,7 +375,7 @@ function IntentRegistryPreview() {
-
}>
+
}>
Skills Registry
diff --git a/src/components/landing/LandingCopyPromptButton.tsx b/src/components/landing/LandingCopyPromptButton.tsx
index e610a4a58..898ac066c 100644
--- a/src/components/landing/LandingCopyPromptButton.tsx
+++ b/src/components/landing/LandingCopyPromptButton.tsx
@@ -1,5 +1,5 @@
import * as React from 'react'
-import { CheckCircle2, Copy } from 'lucide-react'
+import { CheckCircle, Copy } from '@phosphor-icons/react'
import { copyTextToClipboard } from '~/utils/browser-effects'
type LandingCopyPromptButtonProps = {
@@ -54,7 +54,7 @@ export function LandingCopyPromptButton({
}}
>
{status === 'copied' ? (
-
+
) : (
)}
diff --git a/src/components/landing/PacerLanding.tsx b/src/components/landing/PacerLanding.tsx
index 027fb1f29..f093e9ebd 100644
--- a/src/components/landing/PacerLanding.tsx
+++ b/src/components/landing/PacerLanding.tsx
@@ -3,17 +3,16 @@ import { Link, useParams } from '@tanstack/react-router'
import {
ArrowRight,
BookOpen,
- CircleGauge,
Clock,
Gauge,
- Layers,
+ Stack,
ListChecks,
PauseCircle,
- RefreshCcw,
- Rows3,
- Split,
- WandSparkles,
-} from 'lucide-react'
+ ArrowsCounterClockwise,
+ Rows,
+ ArrowsSplit,
+ MagicWand,
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { Footer } from '~/components/Footer'
@@ -86,12 +85,12 @@ const featureCards = [
{
title: 'Queue the work that needs order.',
body: 'Run async tasks with FIFO, LIFO, priority, pause/resume, cancellation, retries, and concurrency control.',
- icon: ,
+ icon: ,
},
{
title: 'Batch the work that should travel together.',
body: 'Collect writes, logs, telemetry, or cache operations into sensible flushes instead of shipping every item alone.',
- icon: ,
+ icon: ,
},
]
@@ -125,7 +124,7 @@ export default function PacerLanding() {
-
}>
+
}>
Timing and pressure control
@@ -186,7 +185,7 @@ export default function PacerLanding() {
-
}>
+
}>
Why Pacer
@@ -211,7 +210,7 @@ export default function PacerLanding() {
-
}>
+
}>
Control lifecycle
@@ -249,7 +248,7 @@ export default function PacerLanding() {
-
}>
+
}>
Framework adapters
diff --git a/src/components/landing/QueryLanding.tsx b/src/components/landing/QueryLanding.tsx
index d74a76883..cc78f1717 100644
--- a/src/components/landing/QueryLanding.tsx
+++ b/src/components/landing/QueryLanding.tsx
@@ -2,20 +2,20 @@ import * as React from 'react'
import { Link, useParams } from '@tanstack/react-router'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import {
- Activity,
+ Pulse,
ArrowRight,
BookOpen,
- DatabaseZap,
+ Database,
Eye,
Flame,
GitBranch,
Infinity as InfinityIcon,
Network,
Plus,
- RefreshCcw,
- RotateCcw,
- Sparkles,
-} from 'lucide-react'
+ ArrowsCounterClockwise,
+ ArrowCounterClockwise,
+ Sparkle,
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { Footer } from '~/components/Footer'
@@ -131,7 +131,7 @@ const featureCards = [
{
title: 'Important defaults do the boring work.',
body: 'Caching, request dedupe, retries, background refetching, window-focus updates, and garbage collection are already wired for the shape of real apps.',
- icon: ,
+ icon: ,
},
{
title: 'Query keys become the cache contract.',
@@ -141,7 +141,7 @@ const featureCards = [
{
title: 'Mutations have a real lifecycle.',
body: 'Handle pending UI, optimistic writes, invalidation, rollback, and follow-up refetches without inventing an ad hoc client-state machine.',
- icon: ,
+ icon: ,
},
{
title: 'Devtools make the cache visible.',
@@ -188,7 +188,7 @@ export default function QueryLanding() {
-
}>
+
}>
Server-state manager
@@ -247,9 +247,7 @@ export default function QueryLanding() {
-
}>
- Why Query
-
+
}>Why Query
Server state is not the same problem as client state.
@@ -273,7 +271,7 @@ export default function QueryLanding() {
-
}>
+
}>
Cache lifecycle
@@ -564,7 +562,7 @@ function QueryCachePanel() {
type="button"
onClick={() => projectsQuery.refetch()}
>
- ,
+ icon: ,
},
{
title: 'Ticks and labels can be product-specific.',
@@ -99,7 +98,7 @@ export default function RangerLanding() {
-
}>
+
}>
Headless range controls
@@ -153,7 +152,7 @@ export default function RangerLanding() {
-
}>
+
}>
Why Ranger
diff --git a/src/components/landing/RouterLanding.tsx b/src/components/landing/RouterLanding.tsx
index c8d61c6b6..a94f7d55e 100644
--- a/src/components/landing/RouterLanding.tsx
+++ b/src/components/landing/RouterLanding.tsx
@@ -3,15 +3,15 @@ import { Link, useParams } from '@tanstack/react-router'
import {
ArrowRight,
BookOpen,
- Boxes,
- DatabaseZap,
+ Stack,
+ Database,
Link as LinkIcon,
- ListTree,
- MousePointerClick,
- Route,
- Search,
- Sparkles,
-} from 'lucide-react'
+ TreeStructure,
+ CursorClick,
+ Path,
+ MagnifyingGlass,
+ Sparkle,
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { ApplicationStarter } from '~/components/ApplicationStarter'
@@ -83,19 +83,19 @@ const routeContractCards = [
eyebrow: 'search',
title: 'Search params behave like state.',
body: 'Parse, validate, inherit, update, and share URL state with the same confidence you expect from a state manager.',
- icon: ,
+ icon: ,
},
{
eyebrow: 'loaders',
title: 'Data work starts at the route.',
body: 'Route loaders run in parallel, preload on intent, cache results, and hand typed data to the component before render.',
- icon: ,
+ icon: ,
},
{
eyebrow: 'boundaries',
title: 'Every route owns its lifecycle.',
body: 'Pending UI, errors, not-found states, code splitting, and context can live where the product route actually changes.',
- icon: ,
+ icon: ,
},
]
@@ -159,7 +159,7 @@ export default function RouterLanding() {
-
}>
+
}>
Type-safe router
@@ -219,7 +219,7 @@ export default function RouterLanding() {
-
}>
+
}>
Application builder
@@ -250,7 +250,7 @@ export default function RouterLanding() {
-
}>
+
}>
Route contract
@@ -275,7 +275,9 @@ export default function RouterLanding() {
-
}>URL state
+
}>
+ URL state
+
Search params without the URLSearchParams ceremony.
@@ -292,7 +294,7 @@ export default function RouterLanding() {
-
}>
+
}>
Loaders and preload
diff --git a/src/components/landing/StartLanding.tsx b/src/components/landing/StartLanding.tsx
index 51325926b..a5f498285 100644
--- a/src/components/landing/StartLanding.tsx
+++ b/src/components/landing/StartLanding.tsx
@@ -3,15 +3,15 @@ import { Link, useParams } from '@tanstack/react-router'
import {
ArrowRight,
BookOpen,
- CheckCircle2,
- ExternalLink,
+ CheckCircle,
+ ArrowSquareOut,
GitBranch,
- Layers,
+ Stack,
Network,
Rocket,
- Server,
- Sparkles,
-} from 'lucide-react'
+ HardDrives,
+ Sparkle,
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { ApplicationStarter } from '~/components/ApplicationStarter'
@@ -48,12 +48,12 @@ const featureCards = [
{
title: 'Server work stays explicit',
body: 'createServerFn gives loaders, components, hooks, and handlers access to server-only work with validation, serializable boundaries, and same-origin RPC semantics.',
- icon: ,
+ icon: ,
},
{
title: 'SSR keeps the app model',
body: 'Start can render the full document, stream useful UI, or opt routes into SPA/selective SSR modes while preserving the interactive client-side Router experience.',
- icon: ,
+ icon: ,
},
]
@@ -156,7 +156,7 @@ export default function StartLanding() {
-
+
Full-stack framework
@@ -235,7 +235,7 @@ export default function StartLanding() {
-
}>
+
}>
Application builder
@@ -308,7 +308,7 @@ export default function StartLanding() {
key={item.label}
className="grid grid-cols-[auto_1fr] gap-2 text-sm leading-6 text-cyan-950 dark:text-cyan-100"
>
-
@@ -583,7 +583,7 @@ function FieldNoteCard({
{source}
-
diff --git a/src/components/landing/StoreLanding.tsx b/src/components/landing/StoreLanding.tsx
index 1fb76e5db..8e6079ae0 100644
--- a/src/components/landing/StoreLanding.tsx
+++ b/src/components/landing/StoreLanding.tsx
@@ -5,14 +5,14 @@ import {
BookOpen,
Cpu,
Fingerprint,
- Layers,
+ Stack,
Radio,
- RefreshCcw,
- ScanLine,
- Sparkles,
- Split,
- WandSparkles,
-} from 'lucide-react'
+ ArrowsCounterClockwise,
+ Scan,
+ Sparkle,
+ ArrowsSplit,
+ MagicWand,
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { Footer } from '~/components/Footer'
@@ -66,7 +66,7 @@ const featureCards = [
{
title: 'Framework adapters are a layer, not the store.',
body: 'Use the adapter for your renderer while the core store stays portable across apps, packages, and UI runtimes.',
- icon:
,
+ icon:
,
},
]
@@ -127,7 +127,7 @@ export default function StoreLanding() {
-
}>
+
}>
Immutable reactive store
@@ -189,7 +189,7 @@ export default function StoreLanding() {
-
}>
+
}>
Why Store
@@ -215,7 +215,7 @@ export default function StoreLanding() {
-
}>
+
}>
Store lifecycle
@@ -233,7 +233,7 @@ export default function StoreLanding() {
-
}>
+
}>
Subscriptions
@@ -253,7 +253,7 @@ export default function StoreLanding() {
-
}>
+
}>
Framework adapters
diff --git a/src/components/landing/TableLanding.tsx b/src/components/landing/TableLanding.tsx
index 56e95aec1..6e114e501 100644
--- a/src/components/landing/TableLanding.tsx
+++ b/src/components/landing/TableLanding.tsx
@@ -17,21 +17,20 @@ import {
import {
ArrowRight,
BookOpen,
- Boxes,
- ChevronDown,
- ChevronUp,
- Columns3,
- EyeOff,
- Filter,
- Grid3X3,
- Layers,
- MoveHorizontal,
- Rows3,
- Scaling,
- Search,
+ Stack,
+ CaretDown,
+ CaretUp,
+ Columns,
+ EyeSlash,
+ Funnel,
+ GridNine,
+ ArrowsHorizontal,
+ Rows,
+ Resize,
+ MagnifyingGlass,
SlidersHorizontal,
- Sparkles,
-} from 'lucide-react'
+ Sparkle,
+} from '@phosphor-icons/react'
import { BottomCTA } from '~/components/BottomCTA'
import { Footer } from '~/components/Footer'
@@ -154,12 +153,12 @@ const pipelineSteps = [
{
label: 'Columns',
body: 'Column defs describe accessors, headers, cells, metadata, and feature behavior without owning your DOM.',
- icon: ,
+ icon: ,
},
{
label: 'Rows',
body: 'Core, filtered, sorted, grouped, expanded, and paginated row models compose into the exact data shape you need.',
- icon: ,
+ icon: ,
},
{
label: 'State',
@@ -169,7 +168,7 @@ const pipelineSteps = [
{
label: 'Markup',
body: 'Render semantic tables, card grids, virtualized panes, or spreadsheet-like layouts from the same engine.',
- icon: ,
+ icon: ,
},
]
@@ -177,22 +176,22 @@ const featureCards = [
{
title: 'Headless means the designer still wins.',
body: 'Table gives you the math and state. Your app keeps the elements, classes, interactions, density, empty states, and brand-specific details.',
- icon: ,
+ icon: ,
},
{
title: 'Feature power without a grid tax.',
body: 'Sorting, filtering, faceting, grouping, aggregation, expansion, selection, sizing, pinning, visibility, ordering, and pagination are opt-in row models.',
- icon: ,
+ icon: ,
},
{
title: 'Server-side data is not an afterthought.',
body: 'Pagination, sorting, and filters can be local, controlled, URL-driven, or backed by your API. Table does not assume where the data lives.',
- icon: ,
+ icon: ,
},
{
title: 'Virtualization stays your choice.',
body: 'Pair with TanStack Virtual when the table needs huge rows or columns, without turning the table engine into a scroll container framework.',
- icon: ,
+ icon: ,
},
]
@@ -236,7 +235,7 @@ export default function TableLanding() {
-
}>
+
}>
Headless table engine
@@ -296,7 +295,7 @@ export default function TableLanding() {
-
}>Why Table
+
}>Why Table
A data grid should not decide your UI system.
@@ -320,7 +319,7 @@ export default function TableLanding() {
-
}>
+
}>
Row model pipeline
@@ -360,7 +359,7 @@ export default function TableLanding() {
-
}>
+
}>
Framework adapters
@@ -392,7 +391,7 @@ export default function TableLanding() {
-
}>
+
}>
Field notes
@@ -584,7 +583,7 @@ function TableWorkbenchPanel() {