From 42696d4cce5fbcae5116294c2bb71d63984e08e4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 31 Aug 2026 17:01:13 +0000 Subject: [PATCH 01/17] feat(partners): add Vercel and Render as Gold partners - Reactivate Vercel as Gold tier partner (was inactive Previous Partner) - Add Render as new Gold tier partner - Add Render logo SVGs (render-black.svg, render-white.svg) - Both partners have hosting unique constraint - UTMs: utm_source=tanstack&utm_medium=referral&utm_campaign=gold-launch --- src/images/render-black.svg | 9 ++++ src/images/render-white.svg | 9 ++++ src/utils/partners.tsx | 89 ++++++++++++++++++++++++++++++++----- 3 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 src/images/render-black.svg create mode 100644 src/images/render-white.svg diff --git a/src/images/render-black.svg b/src/images/render-black.svg new file mode 100644 index 000000000..2571e2353 --- /dev/null +++ b/src/images/render-black.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/images/render-white.svg b/src/images/render-white.svg new file mode 100644 index 000000000..c068b8ab1 --- /dev/null +++ b/src/images/render-white.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/utils/partners.tsx b/src/utils/partners.tsx index a8f1a4ab6..93be3202a 100644 --- a/src/utils/partners.tsx +++ b/src/utils/partners.tsx @@ -41,6 +41,8 @@ import powersyncBlackSvg from '~/images/powersync-black.svg' import powersyncWhiteSvg from '~/images/powersync-white.svg' import railwayBlackSvg from '~/images/railway-black.svg' import railwayWhiteSvg from '~/images/railway-white.svg' +import renderBlackSvg from '~/images/render-black.svg' +import renderWhiteSvg from '~/images/render-white.svg' import openrouterBlackSvg from '~/images/openrouter-black.svg' import openrouterWhiteSvg from '~/images/openrouter-white.svg' import { @@ -1162,32 +1164,48 @@ const electric = ((): Partner => { })() const vercel = ((): Partner => { - const href = 'https://vercel.com?utm_source=tanstack' + const href = + 'https://vercel.com?utm_source=tanstack&utm_medium=referral&utm_campaign=gold-launch' return { name: 'Vercel', id: 'vercel', href, + canonicalHref: 'https://vercel.com/', + resources: [ + { + kind: 'documentation', + label: 'TanStack Start hosting guide', + href: '/start/latest/docs/framework/react/guide/hosting', + }, + ], relatedProducts: ['start', 'router'] as const, - status: 'inactive' as const, - startDate: 'May 2024', - endDate: 'Oct 2024', - score: 0, + status: 'active' as const, + lastReviewedAt: currentPartnerReviewDate, + score: 0.543, + tier: 'gold' as const, uniqueConstraints: ['hosting'] satisfies Array, + brandColor: '#000000', + tagline: 'Frontend Cloud', + applicationStarterIcon: { + mode: 'contain', + src: vercelLightSvg, + }, image: { light: vercelLightSvg, dark: vercelDarkSvg, }, llmDescription: - 'Cloud platform for deploying and scaling web applications with Git-based workflows, preview environments, global delivery, and Vercel Functions.', + 'Frontend cloud platform for deploying and scaling web applications with Git-based workflows, preview environments, global delivery, v0 AI app generation, and Vercel Functions.', category: 'deployment', content: ( <>
Vercel provides Git-based deployments, preview - environments, global delivery, and server-side compute through Vercel - Functions. That makes it a familiar deployment option for TanStack - Start and Router teams building full-stack apps. + environments, global delivery, v0 for AI-generated apps, and + server-side compute through Vercel Functions. That makes it a familiar + deployment option for TanStack Start and Router teams building + full-stack apps.
@@ -1427,6 +1445,56 @@ const railway = ((): Partner => { } })() +const render = ((): Partner => { + const href = + 'https://render.com?utm_source=tanstack&utm_medium=referral&utm_campaign=gold-launch' + + return { + name: 'Render', + id: 'render', + relatedProducts: ['start'], + status: 'active' as const, + lastReviewedAt: currentPartnerReviewDate, + score: 0.429, + tier: 'gold' as const, + uniqueConstraints: ['hosting'] satisfies Array, + href, + canonicalHref: 'https://render.com/', + resources: [ + { + kind: 'documentation', + label: 'TanStack Start hosting guide', + href: '/start/latest/docs/framework/react/guide/hosting', + }, + ], + brandColor: '#000000', + tagline: 'Intuitive Cloud Infrastructure', + applicationStarterIcon: { + mode: 'contain', + src: renderBlackSvg, + }, + image: { + light: renderBlackSvg, + dark: renderWhiteSvg, + }, + llmDescription: + 'Cloud platform for deploying and scaling apps and agents with intuitive infrastructure, managed databases, autoscaling, pull request previews, and zero-ops deployment from GitHub.', + category: 'deployment', + content: ( + <> +
+ Render provides intuitive cloud infrastructure for + deploying apps, APIs, databases, and background services. With + autoscaling, PR previews, and managed infrastructure, it is a + practical fit for TanStack teams that want to ship without ops + overhead. +
+ + + ), + } +})() + const openRouter = ((): Partner => { const href = 'https://openrouter.ai?utm_source=tanstack' @@ -1498,6 +1566,8 @@ export const partners = [ codeRabbit, cloudflare, lovable, + vercel, + render, agGrid, serpApi, netlify, @@ -1515,7 +1585,6 @@ export const partners = [ unkey, fireship, nozzle, - vercel, speakeasy, ] satisfies Array From 61f8f2fa69b388f08c25d32f6fad997892e3937c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 31 Aug 2026 17:19:55 +0000 Subject: [PATCH 02/17] Remove partner score field from types and objects - Remove score from PartnerBase, RailPartner, PartnerForPlacement, DocsPartner types - Remove score from all partner objects in partners.tsx - Update compareLegacyPartnerPriority to use identity/name comparison - Update useDeploymentProviderPlacement to remove score mapping Tier remains as the ranking mechanism. --- src/components/LibraryLayout.tsx | 1 - src/utils/partner-placement.ts | 8 +------ src/utils/partners.tsx | 25 --------------------- src/utils/useDeploymentProviderPlacement.ts | 3 +-- 4 files changed, 2 insertions(+), 35 deletions(-) diff --git a/src/components/LibraryLayout.tsx b/src/components/LibraryLayout.tsx index 33effcedf..fe3763d8c 100644 --- a/src/components/LibraryLayout.tsx +++ b/src/components/LibraryLayout.tsx @@ -57,7 +57,6 @@ const docsPartnerTierWeights: Record = { type DocRecency = 'new' | 'updated' | null type DocsPartner = { category: Partner['category'] - score: Partner['score'] tier?: Partner['tier'] id: string name: string diff --git a/src/utils/partner-placement.ts b/src/utils/partner-placement.ts index 6f5931e01..1223a99ae 100644 --- a/src/utils/partner-placement.ts +++ b/src/utils/partner-placement.ts @@ -24,7 +24,7 @@ export type PartnerPlacementAnalyticsMetadata = { type PartnerForPlacement = Pick< Partner, - 'category' | 'id' | 'name' | 'score' | 'tier' + 'category' | 'id' | 'name' | 'tier' > & { placementWeight?: number } @@ -240,12 +240,6 @@ function compareLegacyPartnerPriority( left: TPartner, right: TPartner, ) { - const scoreComparison = right.score - left.score - - if (scoreComparison !== 0) { - return scoreComparison - } - return comparePartnerIdentity(left, right) } diff --git a/src/utils/partners.tsx b/src/utils/partners.tsx index 93be3202a..560951c5c 100644 --- a/src/utils/partners.tsx +++ b/src/utils/partners.tsx @@ -270,7 +270,6 @@ export type RailPartner = { id: string name: string href: string - score: number tier?: PartnerTier image: PartnerImageConfig } @@ -315,7 +314,6 @@ type PartnerBase = { llmDescription: string category: PartnerCategory lastReviewedAt?: string - score: number uniqueConstraints?: ReadonlyArray brandColor?: string // Primary brand color for game elements tagline?: string // Short tagline for game info cards @@ -434,7 +432,6 @@ const neon = ((): Partner => { status: 'inactive' as const, startDate: null, endDate: 'Apr 2026', - score: 0.297, href, brandColor: '#00E599', tagline: 'Serverless Postgres', @@ -469,7 +466,6 @@ const convex = ((): Partner => { status: 'inactive' as const, startDate: 'May 2024', endDate: 'Mar 2026', - score: 0.286, href, brandColor: '#F3A712', tagline: 'Real-time Database', @@ -517,7 +513,6 @@ const clerk = ((): Partner => { relatedProducts: ['start', 'router'], status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.286, tier: 'silver' as const, uniqueConstraints: [ 'auth-provider', @@ -574,7 +569,6 @@ const workos = ((): Partner => { relatedProducts: ['start', 'router'] as const, status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.314, tier: 'silver' as const, uniqueConstraints: [ 'auth-provider', @@ -616,7 +610,6 @@ const agGrid = ((): Partner => { relatedProducts: ['table'] as const, status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.497, tier: 'silver' as const, href, canonicalHref: 'https://www.ag-grid.com/', @@ -683,7 +676,6 @@ const netlify = ((): Partner => { relatedProducts: ['start', 'router'], status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.343, tier: 'gold' as const, uniqueConstraints: ['hosting'] satisfies Array, href, @@ -751,7 +743,6 @@ const cloudflare = ((): Partner => { relatedProducts: ['start'], status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.857, tier: 'gold' as const, uniqueConstraints: ['hosting'] satisfies Array, startDate: 'Sep 2025', @@ -800,7 +791,6 @@ const lovable = ((): Partner => { relatedProducts: ['start', 'router'] as const, status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.714, tier: 'gold' as const, uniqueConstraints: ['hosting'] satisfies Array, brandColor: '#FF7EB0', @@ -842,7 +832,6 @@ const sentry = ((): Partner => { relatedProducts: ['start', 'router'], status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.229, tier: 'bronze' as const, href, canonicalHref: 'https://sentry.io/', @@ -886,7 +875,6 @@ const fireship = ((): Partner => { status: 'inactive' as const, startDate: null, endDate: null, - score: 0.014, href, tagline: 'Dev Education', image: { @@ -944,7 +932,6 @@ const nozzle = ((): Partner => { status: 'inactive' as const, startDate: null, endDate: null, - score: 0.014, tagline: 'Enterprise SEO', image: { src: nozzleImage, @@ -978,7 +965,6 @@ const speakeasy = ((): Partner => { status: 'inactive' as const, startDate: 'Feb 2025', endDate: 'Jul 2025', - score: 0, image: { light: speakeasyLightSvg, dark: speakeasyDarkSvg, @@ -1011,7 +997,6 @@ const unkey = ((): Partner => { relatedProducts: ['start'] as const, status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.051, tier: 'bronze' as const, href, canonicalHref: 'https://www.unkey.com/', @@ -1065,7 +1050,6 @@ const serpApi = ((): Partner => { relatedProducts: ['start', 'ai', 'mcp'], status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.41, tier: 'silver' as const, href, canonicalHref: 'https://serpapi.com/', @@ -1123,7 +1107,6 @@ const electric = ((): Partner => { relatedProducts: ['db'] as const, status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.283, tier: 'bronze' as const, href, canonicalHref: 'https://electric.ax/', @@ -1182,7 +1165,6 @@ const vercel = ((): Partner => { relatedProducts: ['start', 'router'] as const, status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.543, tier: 'gold' as const, uniqueConstraints: ['hosting'] satisfies Array, brandColor: '#000000', @@ -1232,7 +1214,6 @@ const prisma = ((): Partner => { lastReviewedAt: currentPartnerReviewDate, relatedProducts: ['db', 'start'] as const, startDate: 'Aug 2025', - score: 0.143, tier: 'bronze' as const, brandColor: '#2D3748', tagline: 'Database ORM', @@ -1277,7 +1258,6 @@ const codeRabbit = ((): Partner => { lastReviewedAt: currentPartnerReviewDate, relatedProducts: [], startDate: 'Aug 2025', - score: 1, tier: 'gold' as const, brandColor: '#FF6B2B', tagline: 'AI Code Review', @@ -1317,7 +1297,6 @@ const strapi = ((): Partner => { status: 'inactive' as const, startDate: null, endDate: null, - score: 0.069, tier: 'bronze' as const, href, brandColor: '#4945FF', @@ -1355,7 +1334,6 @@ const powerSync = ((): Partner => { status: 'inactive' as const, startDate: 'Jan 2026', endDate: 'Jun 2026', - score: 0.143, tier: 'bronze' as const, href, canonicalHref: 'https://www.powersync.com/', @@ -1409,7 +1387,6 @@ const railway = ((): Partner => { relatedProducts: ['start'], status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.145, tier: 'gold' as const, uniqueConstraints: ['hosting'] satisfies Array, href, @@ -1455,7 +1432,6 @@ const render = ((): Partner => { relatedProducts: ['start'], status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, - score: 0.429, tier: 'gold' as const, uniqueConstraints: ['hosting'] satisfies Array, href, @@ -1519,7 +1495,6 @@ const openRouter = ((): Partner => { status: 'active' as const, lastReviewedAt: currentPartnerReviewDate, startDate: 'Mar 2026', - score: 0.344, tier: 'silver' as const, brandColor: '#7C3AED', tagline: 'Unified LLM API', diff --git a/src/utils/useDeploymentProviderPlacement.ts b/src/utils/useDeploymentProviderPlacement.ts index 847c938e2..51f383097 100644 --- a/src/utils/useDeploymentProviderPlacement.ts +++ b/src/utils/useDeploymentProviderPlacement.ts @@ -13,7 +13,7 @@ export const deploymentProviderIds: ReadonlyArray = [ type DeploymentProviderPlacementPartner = Pick< Partner, - 'category' | 'id' | 'name' | 'score' | 'tier' + 'category' | 'id' | 'name' | 'tier' > & { provider: DeploymentProviderId } @@ -41,7 +41,6 @@ function getDeploymentProviderPlacementPartner( id: partner.id, name: partner.name, provider, - score: partner.score, tier: partner.tier, } } From b7ce5fd6566e3551c9506bd17ea643c58851d75a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 31 Aug 2026 17:23:42 +0000 Subject: [PATCH 03/17] Replace Render SVGs with official Brand Kit wordmarks - Replace 110x21 scrapes with official wordmarks (viewBox 0 0 2909 1200) - 7 path elements: logomark + RENDER letters - Black fill for render-black.svg, white fill for render-white.svg --- src/images/render-black.svg | 16 ++++++++-------- src/images/render-white.svg | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/images/render-black.svg b/src/images/render-black.svg index 2571e2353..d0d7888e4 100644 --- a/src/images/render-black.svg +++ b/src/images/render-black.svg @@ -1,9 +1,9 @@ - - - - - - - - + + + + + + + + diff --git a/src/images/render-white.svg b/src/images/render-white.svg index c068b8ab1..0a4dd43f8 100644 --- a/src/images/render-white.svg +++ b/src/images/render-white.svg @@ -1,9 +1,9 @@ - - - - - - - - + + + + + + + + From 4824a9f902d5ed25922ba114402e35eaf657cfb0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 31 Aug 2026 18:15:59 +0000 Subject: [PATCH 04/17] Crop Render SVG viewBox to wordmark bounds Remove ~400px whitespace padding from official Brand Kit SVGs. viewBox 400 400 2106.6 400 crops to actual path content bbox. --- src/images/render-black.svg | 2 +- src/images/render-white.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/images/render-black.svg b/src/images/render-black.svg index d0d7888e4..931758f1a 100644 --- a/src/images/render-black.svg +++ b/src/images/render-black.svg @@ -1,4 +1,4 @@ - + diff --git a/src/images/render-white.svg b/src/images/render-white.svg index 0a4dd43f8..b222057c8 100644 --- a/src/images/render-white.svg +++ b/src/images/render-white.svg @@ -1,4 +1,4 @@ - + From c1eaa30da6b57d9740e3b0bdbe2e84348c19ef7d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 31 Aug 2026 18:23:58 +0000 Subject: [PATCH 05/17] Wire Render per-placement UTM content URLs Add utm_content parameter for Render partner on approved surfaces: - home_grid - library_grid - docs_rail - docs_strip Other placements and partners use their default href unchanged. Implemented via getPartnerHref helper that checks partner id and placement. --- src/components/LibraryLayout.tsx | 6 ++-- src/components/PartnersGrid.tsx | 6 ++-- src/components/ds/ui/PartnerRail.tsx | 7 ++-- src/utils/partners.tsx | 24 ++++++++++++++ tests/application-starter-partners.test.ts | 37 ++++++++++++++++++++++ 5 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/components/LibraryLayout.tsx b/src/components/LibraryLayout.tsx index fe3763d8c..aaf76ddfa 100644 --- a/src/components/LibraryLayout.tsx +++ b/src/components/LibraryLayout.tsx @@ -18,6 +18,7 @@ import { frameworkOptions } from '~/libraries/frameworks' import { fallbackLibraryIcon, libraryIcons } from '~/libraries/icons' import { twMerge } from 'tailwind-merge' import { + getPartnerHref, partners, PartnerImage, partnerTiers, @@ -191,11 +192,12 @@ function DocsPartnerSlotLink({ }, }) const compactImageConfig = getCompactPartnerImageConfig(partner.image) + const href = getPartnerHref(partner, 'docs_strip') const onClick = () => { let destinationHost: string | undefined try { - destinationHost = new URL(partner.href).host + destinationHost = new URL(href).host } catch { // Bad/relative href — track without host rather than dropping. } @@ -212,7 +214,7 @@ function DocsPartnerSlotLink({ return ( { let destinationHost: string | undefined try { - destinationHost = new URL(partner.href).host + destinationHost = new URL(href).host } catch { // Bad/relative href — track without host rather than dropping. } diff --git a/src/components/ds/ui/PartnerRail.tsx b/src/components/ds/ui/PartnerRail.tsx index 2f0f857ad..bc350ae9a 100644 --- a/src/components/ds/ui/PartnerRail.tsx +++ b/src/components/ds/ui/PartnerRail.tsx @@ -1,6 +1,7 @@ import { Link } from '@tanstack/react-router' import { twMerge } from 'tailwind-merge' import { + getPartnerHref, partnerTierFlares, partnerTierLabels, type PartnerTier, @@ -176,10 +177,12 @@ function PartnerRailLogo({ }, }) + const href = getPartnerHref(partner, analyticsPlacement) + return ( { let destinationHost: string | undefined try { - destinationHost = new URL(partner.href).host + destinationHost = new URL(href).host } catch { // Bad/relative href — track without host rather than dropping. } diff --git a/src/utils/partners.tsx b/src/utils/partners.tsx index 560951c5c..6816f9822 100644 --- a/src/utils/partners.tsx +++ b/src/utils/partners.tsx @@ -50,6 +50,7 @@ import { getPartnersForPlacement, type PartnerPlacementContext, } from '~/utils/partner-placement' +import type { PartnerPlacement } from '~/utils/analytics' function LearnMoreButton() { return ( @@ -2088,3 +2089,26 @@ export function composeApplicationStarterInput( export function getPartnerById(partnerId: string) { return partners.find((partner) => partner.id === partnerId) } + +const renderPlacementUtmContent: Partial> = { + home_grid: 'home_grid', + library_grid: 'library_grid', + docs_rail: 'docs_rail', + docs_strip: 'docs_strip', +} + +export function getPartnerHref( + partner: Pick, + placement?: PartnerPlacement, +): string { + if (partner.id !== 'render' || !placement) { + return partner.href + } + + const utmContent = renderPlacementUtmContent[placement] + if (!utmContent) { + return partner.href + } + + return `https://render.com/?utm_source=tanstack&utm_medium=referral&utm_campaign=gold-launch&utm_content=${utmContent}` +} diff --git a/tests/application-starter-partners.test.ts b/tests/application-starter-partners.test.ts index 56516e73a..6a132c59f 100644 --- a/tests/application-starter-partners.test.ts +++ b/tests/application-starter-partners.test.ts @@ -17,6 +17,7 @@ const { const { composeApplicationStarterInput, getInferredApplicationStarterPartnerIdsFromUserInput, + getPartnerHref, partners, }: typeof import('../src/utils/partners') = require('../src/utils/partners') const { @@ -357,3 +358,39 @@ test('OpenRouter guidance prefers the TanStack AI adapter', async () => { assert.match(result.prompt, /@tanstack\/ai-openrouter/) }) + +test('Render uses per-placement UTM content for approved surfaces', () => { + const renderPartner = partners.find((p) => p.id === 'render') + assert.ok(renderPartner, 'Render partner should exist') + + const placements = ['home_grid', 'library_grid', 'docs_rail', 'docs_strip'] as const + for (const placement of placements) { + const href = getPartnerHref(renderPartner, placement) + assert.match( + href, + new RegExp(`utm_content=${placement}`), + `Render href for ${placement} should include utm_content=${placement}`, + ) + assert.match(href, /render\.com/, 'Should point to render.com') + assert.match(href, /utm_source=tanstack/, 'Should include utm_source') + assert.match(href, /utm_campaign=gold-launch/, 'Should include utm_campaign') + } + + const defaultHref = getPartnerHref(renderPartner, 'directory') + assert.doesNotMatch( + defaultHref, + /utm_content/, + 'Render href for other placements should not include utm_content', + ) +}) + +test('other partners use their default href regardless of placement', () => { + const vercel = partners.find((p) => p.id === 'vercel') + assert.ok(vercel, 'Vercel partner should exist') + + const placements = ['home_grid', 'library_grid', 'docs_rail', 'docs_strip', 'directory'] as const + for (const placement of placements) { + const href = getPartnerHref(vercel, placement) + assert.equal(href, vercel.href, `Vercel href should be unchanged for ${placement}`) + } +}) From f56bc7914176fb5c94f606f61a15b73a9ad8d136 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 18:26:12 +0000 Subject: [PATCH 06/17] ci: apply automated fixes --- tests/application-starter-partners.test.ts | 27 ++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/application-starter-partners.test.ts b/tests/application-starter-partners.test.ts index 6a132c59f..2f6790c90 100644 --- a/tests/application-starter-partners.test.ts +++ b/tests/application-starter-partners.test.ts @@ -363,7 +363,12 @@ test('Render uses per-placement UTM content for approved surfaces', () => { const renderPartner = partners.find((p) => p.id === 'render') assert.ok(renderPartner, 'Render partner should exist') - const placements = ['home_grid', 'library_grid', 'docs_rail', 'docs_strip'] as const + const placements = [ + 'home_grid', + 'library_grid', + 'docs_rail', + 'docs_strip', + ] as const for (const placement of placements) { const href = getPartnerHref(renderPartner, placement) assert.match( @@ -373,7 +378,11 @@ test('Render uses per-placement UTM content for approved surfaces', () => { ) assert.match(href, /render\.com/, 'Should point to render.com') assert.match(href, /utm_source=tanstack/, 'Should include utm_source') - assert.match(href, /utm_campaign=gold-launch/, 'Should include utm_campaign') + assert.match( + href, + /utm_campaign=gold-launch/, + 'Should include utm_campaign', + ) } const defaultHref = getPartnerHref(renderPartner, 'directory') @@ -388,9 +397,19 @@ test('other partners use their default href regardless of placement', () => { const vercel = partners.find((p) => p.id === 'vercel') assert.ok(vercel, 'Vercel partner should exist') - const placements = ['home_grid', 'library_grid', 'docs_rail', 'docs_strip', 'directory'] as const + const placements = [ + 'home_grid', + 'library_grid', + 'docs_rail', + 'docs_strip', + 'directory', + ] as const for (const placement of placements) { const href = getPartnerHref(vercel, placement) - assert.equal(href, vercel.href, `Vercel href should be unchanged for ${placement}`) + assert.equal( + href, + vercel.href, + `Vercel href should be unchanged for ${placement}`, + ) } }) From 8d9a405beeae9206ac0c28069db77b752d2aedf1 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 31 Aug 2026 12:45:39 -0600 Subject: [PATCH 07/17] Fix Gold partner layout in builder --- src/components/application-starter/prompt-parts.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/application-starter/prompt-parts.tsx b/src/components/application-starter/prompt-parts.tsx index e1653a57e..b0d5e2e25 100644 --- a/src/components/application-starter/prompt-parts.tsx +++ b/src/components/application-starter/prompt-parts.tsx @@ -519,7 +519,7 @@ export function StarterPartnerRows({ case 6: return 'grid-cols-2 min-[480px]:grid-cols-3 min-[900px]:grid-cols-6' case 7: - return 'grid-cols-4 min-[900px]:grid-cols-7' + return 'grid-cols-2 min-[480px]:grid-cols-4 min-[900px]:grid-cols-12' default: return 'grid-cols-2 min-[480px]:grid-cols-3' } @@ -541,7 +541,7 @@ export function StarterPartnerRows({ ), )} > - {row.partners.map((partner) => { + {row.partners.map((partner, partnerIndex) => { const selected = selectedPartners.includes(partner.id) const muted = mutedPartnerIds.has(partner.id) const selectionIndex = selected ? selectedOrdinal++ : -1 @@ -566,6 +566,11 @@ export function StarterPartnerRows({ visuallySelected={visuallySelected} className={twMerge( size === 'large' && 'min-w-0 w-full justify-center', + size === 'large' && + row.partners.length === 7 && + (partnerIndex < 4 + ? 'min-[900px]:col-span-3' + : 'min-[900px]:col-span-4'), )} /> From 6da1d78aab4dd8524f5bccd9a8308269e14d7c07 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 31 Aug 2026 13:23:31 -0600 Subject: [PATCH 08/17] Update Vercel partner positioning --- src/utils/partners.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/utils/partners.tsx b/src/utils/partners.tsx index 6816f9822..aea6b1d72 100644 --- a/src/utils/partners.tsx +++ b/src/utils/partners.tsx @@ -1169,7 +1169,7 @@ const vercel = ((): Partner => { tier: 'gold' as const, uniqueConstraints: ['hosting'] satisfies Array, brandColor: '#000000', - tagline: 'Frontend Cloud', + tagline: 'Agentic Infrastructure', applicationStarterIcon: { mode: 'contain', src: vercelLightSvg, @@ -1179,16 +1179,16 @@ const vercel = ((): Partner => { dark: vercelDarkSvg, }, llmDescription: - 'Frontend cloud platform for deploying and scaling web applications with Git-based workflows, preview environments, global delivery, v0 AI app generation, and Vercel Functions.', + 'Agentic infrastructure for building, deploying, and running apps and agents, with Git-based workflows, preview deployments, global delivery, serverless compute, and AI tooling.', category: 'deployment', content: ( <>
- Vercel provides Git-based deployments, preview - environments, global delivery, v0 for AI-generated apps, and - server-side compute through Vercel Functions. That makes it a familiar - deployment option for TanStack Start and Router teams building - full-stack apps. + Vercel provides agentic infrastructure for building, + deploying, and running apps and agents, with Git-based workflows, + preview deployments, global delivery, serverless compute, and AI + tooling. It is a familiar deployment option for TanStack Start and + Router teams shipping full-stack applications.
From 33ef9b8abff0651709dfc18d127b616d0ad6eb2c Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 31 Aug 2026 13:27:05 -0600 Subject: [PATCH 09/17] Keep Gold partner cells equal width --- .../application-starter/prompt-parts.tsx | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/components/application-starter/prompt-parts.tsx b/src/components/application-starter/prompt-parts.tsx index b0d5e2e25..18495f481 100644 --- a/src/components/application-starter/prompt-parts.tsx +++ b/src/components/application-starter/prompt-parts.tsx @@ -519,7 +519,7 @@ export function StarterPartnerRows({ case 6: return 'grid-cols-2 min-[480px]:grid-cols-3 min-[900px]:grid-cols-6' case 7: - return 'grid-cols-2 min-[480px]:grid-cols-4 min-[900px]:grid-cols-12' + return 'grid-cols-2 min-[480px]:grid-cols-4 min-[900px]:grid-cols-8' default: return 'grid-cols-2 min-[480px]:grid-cols-3' } @@ -538,6 +538,8 @@ export function StarterPartnerRows({ twMerge( 'grid w-full gap-[2px] overflow-hidden rounded-xl bg-gray-950/[0.10] dark:bg-white/[0.12]', getLargeGridColumns(row.partners.length), + row.partners.length === 7 && + 'min-[900px]:overflow-visible min-[900px]:rounded-none min-[900px]:bg-transparent min-[900px]:dark:bg-transparent', ), )} > @@ -568,9 +570,27 @@ export function StarterPartnerRows({ size === 'large' && 'min-w-0 w-full justify-center', size === 'large' && row.partners.length === 7 && - (partnerIndex < 4 - ? 'min-[900px]:col-span-3' - : 'min-[900px]:col-span-4'), + 'min-[900px]:col-span-2', + size === 'large' && + row.partners.length === 7 && + partnerIndex === 4 && + 'min-[900px]:col-start-2', + size === 'large' && + row.partners.length === 7 && + partnerIndex === 0 && + 'min-[900px]:rounded-tl-xl', + size === 'large' && + row.partners.length === 7 && + partnerIndex === 3 && + 'min-[900px]:rounded-tr-xl', + size === 'large' && + row.partners.length === 7 && + partnerIndex === 4 && + 'min-[900px]:rounded-bl-xl', + size === 'large' && + row.partners.length === 7 && + partnerIndex === 6 && + 'min-[900px]:rounded-br-xl', )} /> From 156f3f3bc1588413d790e397f602c9910aa51aa8 Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Mon, 31 Aug 2026 14:27:19 -0500 Subject: [PATCH 10/17] fix: prevent Markdown and Highlight landing link crashes (#1202) fix: disambiguate library companion links --- src/components/landing/HighlightLanding.tsx | 4 ++-- src/components/landing/MarkdownLanding.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/landing/HighlightLanding.tsx b/src/components/landing/HighlightLanding.tsx index aa8e072ef..fb5f8030b 100644 --- a/src/components/landing/HighlightLanding.tsx +++ b/src/components/landing/HighlightLanding.tsx @@ -179,8 +179,8 @@ export default function HighlightLanding() { body="Every renderer and adapter receives the highlighter you assembled; none imports every language behind your back." />

diff --git a/src/components/landing/MarkdownLanding.tsx b/src/components/landing/MarkdownLanding.tsx index 9b8cd0f6f..3fece9b9a 100644 --- a/src/components/landing/MarkdownLanding.tsx +++ b/src/components/landing/MarkdownLanding.tsx @@ -244,8 +244,8 @@ export default function MarkdownLanding() { body="Code fences carry language and metadata. An explicit highlighter renders them later, so the core never silently imports a grammar engine." />

From aaff1be8eca375a3d83ee58c594824db41abfe15 Mon Sep 17 00:00:00 2001 From: Andy Date: Mon, 31 Aug 2026 13:46:52 -0600 Subject: [PATCH 11/17] =?UTF-8?q?feat(ds):=20Dialog,=20Drawer=20and=20Take?= =?UTF-8?q?over=20primitives=20=E2=80=94=20overlay=20audit=20(#1205)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ds): add Dialog and Drawer primitives, migrate 9 overlays Audited every overlay on the site and found 11 independent implementations across 6 positional postures. All 4 hand-rolled ones (no Radix) were missing focus trapping, focus restoration, or Escape-to-dismiss — the admin roles confirm was a destructive action a keyboard user could not dismiss. Adds two primitives, both Radix-backed with no opt-out: - Dialog — centered. Header/Body/Footer, sizes xs–xl, scrolling body capped at the viewport, tinted header (media + tint) for third-party brands, and DialogStatus for outcome panels. - Drawer — edge-anchored. side="right|left|bottom", sizes sm–2xl, and `fit` to size the panel to its content instead of filling the edge. Migrated onto them: LoginModal, AvatarCropModal, the npm-stats combine dialog, the admin roles confirm, BaselineSection, BuilderAssistant's model connections, both deploy dialogs, and BuilderGuideDialog (Drawer). The two deploy dialogs were byte-identical 1,065-line twins; they now share one header and one status panel and are fully off raw Tailwind colours. Tokens the primitives needed and the system did not have: - --color-scrim, heavier in dark (0.65 vs 0.5) — equal alpha reads as weaker separation over an already-dark page. Replaces 7 hand-picked black/NN values. - --z-scrim / --z-overlay, set to the 999/1000 pair already used by the majority, so adopting them moves nothing. Five stacking families existed. - Real dialog/drawer keyframes. The animate-in / fade-in-0 / zoom-in-95 classes used elsewhere come from tailwindcss-animate, which is NOT installed — they match zero CSS rules and animate nothing. Timing reuses the existing --motion-duration-* and --motion-ease-* tokens. Also rebalances the text scale. text-muted was #756c5b on #111111 — 3.64:1, below the 4.5:1 AA floor, so all muted copy in dark mode was failing. muted now takes the old secondary value and secondary lightens, adding ds-neutral-150 and -350 as ramp midpoints (not yet in Figma). Both roles now read at matched weight across themes: secondary 12.0/11.3, muted 7.9/7.8. Fixes DsKit's Swatch reading its hex once on mount, which left the palette and semantic pages showing #FFFFFF next to a black chip after a theme toggle. Documented at /ds/overlays (the audit itself), /ds/dialog and /ds/drawer. Not migrated, each blocked on a posture not yet built: SearchModal (command palette), CartDrawer (anchored panel), ProductDrawer (bottom sheet plus cross-panel chrome), LibrariesOverlay (full-bleed). Co-Authored-By: Claude Opus 5 * feat(ds): add Drawer anchor prop, migrate CartDrawer Completes the audit's sixth posture — the anchored panel. It turned out not to need a new component. CartDrawer already matched `Drawer side="right" fit` in every respect except one: its top edge cleared the site header. That offset had nowhere to live, which is the only reason it was a separate implementation. So this adds `anchor="viewport" | "navbar"` rather than a near-duplicate of Drawer. `navbar` reads --navbar-height with the same 56px fallback the navbar itself uses, and shortens the `fit` height cap by the same amount so a content-sized panel still cannot run off the bottom. Verified against a live 58px navbar: viewport anchors at 12px, navbar at 66px. CartDrawer now uses it, and settles the open question about the shop's parallel token namespace: the primitive supplies posture and behaviour while the caller passes `shop-scope` and its surface colours through className. Panel geometry is unchanged at 384px wide; the right gutter normalises from 16px to the DS 12px. That leaves ProductDrawer as the last hand-rolled overlay on the site. Co-Authored-By: Claude Opus 5 * feat(ds): migrate ProductDrawer to the DS Drawer The last hand-rolled overlay on the site. It was a bare

- - {allHandles.length > 1 ? ( - <> - - - - - ) : null} - + + ) } /* ─── Full product content ────────────────────────────────────────────── */ -function DrawerContent({ +function ProductPanel({ product, animateIn, }: { diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index cc5a8a49b..6825cf783 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -67,6 +67,7 @@ import { Route as OauthRegisterRouteImport } from './routes/oauth/register' import { Route as OauthAuthorizeRouteImport } from './routes/oauth/authorize' import { Route as LibrariesFrameworkRouteImport } from './routes/libraries_.$framework' import { Route as DsTypographyRouteImport } from './routes/ds.typography' +import { Route as DsTakeoverRouteImport } from './routes/ds.takeover' import { Route as DsTabsRouteImport } from './routes/ds.tabs' import { Route as DsStatsRouteImport } from './routes/ds.stats' import { Route as DsSpinnerRouteImport } from './routes/ds.spinner' @@ -76,6 +77,7 @@ import { Route as DsPartnerRailRouteImport } from './routes/ds.partner-rail' import { Route as DsPanelRouteImport } from './routes/ds.panel' import { Route as DsPaletteRouteImport } from './routes/ds.palette' import { Route as DsPageHeaderRouteImport } from './routes/ds.page-header' +import { Route as DsOverlaysRouteImport } from './routes/ds.overlays' import { Route as DsNavbarRouteImport } from './routes/ds.navbar' import { Route as DsMaintainersRouteImport } from './routes/ds.maintainers' import { Route as DsLogosRouteImport } from './routes/ds.logos' @@ -84,6 +86,8 @@ import { Route as DsIconographyRouteImport } from './routes/ds.iconography' import { Route as DsEyebrowRouteImport } from './routes/ds.eyebrow' import { Route as DsEffectsRouteImport } from './routes/ds.effects' import { Route as DsDropdownRouteImport } from './routes/ds.dropdown' +import { Route as DsDrawerRouteImport } from './routes/ds.drawer' +import { Route as DsDialogRouteImport } from './routes/ds.dialog' import { Route as DsColorsRouteImport } from './routes/ds.colors' import { Route as DsCardsRouteImport } from './routes/ds.cards' import { Route as DsButtonsRouteImport } from './routes/ds.buttons' @@ -504,6 +508,11 @@ const DsTypographyRoute = DsTypographyRouteImport.update({ path: '/typography', getParentRoute: () => DsRoute, } as any) +const DsTakeoverRoute = DsTakeoverRouteImport.update({ + id: '/takeover', + path: '/takeover', + getParentRoute: () => DsRoute, +} as any) const DsTabsRoute = DsTabsRouteImport.update({ id: '/tabs', path: '/tabs', @@ -549,6 +558,11 @@ const DsPageHeaderRoute = DsPageHeaderRouteImport.update({ path: '/page-header', getParentRoute: () => DsRoute, } as any) +const DsOverlaysRoute = DsOverlaysRouteImport.update({ + id: '/overlays', + path: '/overlays', + getParentRoute: () => DsRoute, +} as any) const DsNavbarRoute = DsNavbarRouteImport.update({ id: '/navbar', path: '/navbar', @@ -591,6 +605,16 @@ const DsDropdownRoute = DsDropdownRouteImport.update({ path: '/dropdown', getParentRoute: () => DsRoute, } as any) +const DsDrawerRoute = DsDrawerRouteImport.update({ + id: '/drawer', + path: '/drawer', + getParentRoute: () => DsRoute, +} as any) +const DsDialogRoute = DsDialogRouteImport.update({ + id: '/dialog', + path: '/dialog', + getParentRoute: () => DsRoute, +} as any) const DsColorsRoute = DsColorsRouteImport.update({ id: '/colors', path: '/colors', @@ -1371,6 +1395,8 @@ export interface FileRoutesByFullPath { '/ds/buttons': typeof DsButtonsRoute '/ds/cards': typeof DsCardsRoute '/ds/colors': typeof DsColorsRoute + '/ds/dialog': typeof DsDialogRoute + '/ds/drawer': typeof DsDrawerRoute '/ds/dropdown': typeof DsDropdownRoute '/ds/effects': typeof DsEffectsRoute '/ds/eyebrow': typeof DsEyebrowRoute @@ -1379,6 +1405,7 @@ export interface FileRoutesByFullPath { '/ds/logos': typeof DsLogosRoute '/ds/maintainers': typeof DsMaintainersRoute '/ds/navbar': typeof DsNavbarRoute + '/ds/overlays': typeof DsOverlaysRoute '/ds/page-header': typeof DsPageHeaderRoute '/ds/palette': typeof DsPaletteRoute '/ds/panel': typeof DsPanelRoute @@ -1388,6 +1415,7 @@ export interface FileRoutesByFullPath { '/ds/spinner': typeof DsSpinnerRoute '/ds/stats': typeof DsStatsRoute '/ds/tabs': typeof DsTabsRoute + '/ds/takeover': typeof DsTakeoverRoute '/ds/typography': typeof DsTypographyRoute '/libraries/$framework': typeof LibrariesFrameworkRoute '/oauth/authorize': typeof OauthAuthorizeRoute @@ -1569,6 +1597,8 @@ export interface FileRoutesByTo { '/ds/buttons': typeof DsButtonsRoute '/ds/cards': typeof DsCardsRoute '/ds/colors': typeof DsColorsRoute + '/ds/dialog': typeof DsDialogRoute + '/ds/drawer': typeof DsDrawerRoute '/ds/dropdown': typeof DsDropdownRoute '/ds/effects': typeof DsEffectsRoute '/ds/eyebrow': typeof DsEyebrowRoute @@ -1577,6 +1607,7 @@ export interface FileRoutesByTo { '/ds/logos': typeof DsLogosRoute '/ds/maintainers': typeof DsMaintainersRoute '/ds/navbar': typeof DsNavbarRoute + '/ds/overlays': typeof DsOverlaysRoute '/ds/page-header': typeof DsPageHeaderRoute '/ds/palette': typeof DsPaletteRoute '/ds/panel': typeof DsPanelRoute @@ -1586,6 +1617,7 @@ export interface FileRoutesByTo { '/ds/spinner': typeof DsSpinnerRoute '/ds/stats': typeof DsStatsRoute '/ds/tabs': typeof DsTabsRoute + '/ds/takeover': typeof DsTakeoverRoute '/ds/typography': typeof DsTypographyRoute '/libraries/$framework': typeof LibrariesFrameworkRoute '/oauth/authorize': typeof OauthAuthorizeRoute @@ -1773,6 +1805,8 @@ export interface FileRoutesById { '/ds/buttons': typeof DsButtonsRoute '/ds/cards': typeof DsCardsRoute '/ds/colors': typeof DsColorsRoute + '/ds/dialog': typeof DsDialogRoute + '/ds/drawer': typeof DsDrawerRoute '/ds/dropdown': typeof DsDropdownRoute '/ds/effects': typeof DsEffectsRoute '/ds/eyebrow': typeof DsEyebrowRoute @@ -1781,6 +1815,7 @@ export interface FileRoutesById { '/ds/logos': typeof DsLogosRoute '/ds/maintainers': typeof DsMaintainersRoute '/ds/navbar': typeof DsNavbarRoute + '/ds/overlays': typeof DsOverlaysRoute '/ds/page-header': typeof DsPageHeaderRoute '/ds/palette': typeof DsPaletteRoute '/ds/panel': typeof DsPanelRoute @@ -1790,6 +1825,7 @@ export interface FileRoutesById { '/ds/spinner': typeof DsSpinnerRoute '/ds/stats': typeof DsStatsRoute '/ds/tabs': typeof DsTabsRoute + '/ds/takeover': typeof DsTakeoverRoute '/ds/typography': typeof DsTypographyRoute '/libraries_/$framework': typeof LibrariesFrameworkRoute '/oauth/authorize': typeof OauthAuthorizeRoute @@ -1981,6 +2017,8 @@ export interface FileRouteTypes { | '/ds/buttons' | '/ds/cards' | '/ds/colors' + | '/ds/dialog' + | '/ds/drawer' | '/ds/dropdown' | '/ds/effects' | '/ds/eyebrow' @@ -1989,6 +2027,7 @@ export interface FileRouteTypes { | '/ds/logos' | '/ds/maintainers' | '/ds/navbar' + | '/ds/overlays' | '/ds/page-header' | '/ds/palette' | '/ds/panel' @@ -1998,6 +2037,7 @@ export interface FileRouteTypes { | '/ds/spinner' | '/ds/stats' | '/ds/tabs' + | '/ds/takeover' | '/ds/typography' | '/libraries/$framework' | '/oauth/authorize' @@ -2179,6 +2219,8 @@ export interface FileRouteTypes { | '/ds/buttons' | '/ds/cards' | '/ds/colors' + | '/ds/dialog' + | '/ds/drawer' | '/ds/dropdown' | '/ds/effects' | '/ds/eyebrow' @@ -2187,6 +2229,7 @@ export interface FileRouteTypes { | '/ds/logos' | '/ds/maintainers' | '/ds/navbar' + | '/ds/overlays' | '/ds/page-header' | '/ds/palette' | '/ds/panel' @@ -2196,6 +2239,7 @@ export interface FileRouteTypes { | '/ds/spinner' | '/ds/stats' | '/ds/tabs' + | '/ds/takeover' | '/ds/typography' | '/libraries/$framework' | '/oauth/authorize' @@ -2382,6 +2426,8 @@ export interface FileRouteTypes { | '/ds/buttons' | '/ds/cards' | '/ds/colors' + | '/ds/dialog' + | '/ds/drawer' | '/ds/dropdown' | '/ds/effects' | '/ds/eyebrow' @@ -2390,6 +2436,7 @@ export interface FileRouteTypes { | '/ds/logos' | '/ds/maintainers' | '/ds/navbar' + | '/ds/overlays' | '/ds/page-header' | '/ds/palette' | '/ds/panel' @@ -2399,6 +2446,7 @@ export interface FileRouteTypes { | '/ds/spinner' | '/ds/stats' | '/ds/tabs' + | '/ds/takeover' | '/ds/typography' | '/libraries_/$framework' | '/oauth/authorize' @@ -3027,6 +3075,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DsTypographyRouteImport parentRoute: typeof DsRoute } + '/ds/takeover': { + id: '/ds/takeover' + path: '/takeover' + fullPath: '/ds/takeover' + preLoaderRoute: typeof DsTakeoverRouteImport + parentRoute: typeof DsRoute + } '/ds/tabs': { id: '/ds/tabs' path: '/tabs' @@ -3090,6 +3145,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DsPageHeaderRouteImport parentRoute: typeof DsRoute } + '/ds/overlays': { + id: '/ds/overlays' + path: '/overlays' + fullPath: '/ds/overlays' + preLoaderRoute: typeof DsOverlaysRouteImport + parentRoute: typeof DsRoute + } '/ds/navbar': { id: '/ds/navbar' path: '/navbar' @@ -3146,6 +3208,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DsDropdownRouteImport parentRoute: typeof DsRoute } + '/ds/drawer': { + id: '/ds/drawer' + path: '/drawer' + fullPath: '/ds/drawer' + preLoaderRoute: typeof DsDrawerRouteImport + parentRoute: typeof DsRoute + } + '/ds/dialog': { + id: '/ds/dialog' + path: '/dialog' + fullPath: '/ds/dialog' + preLoaderRoute: typeof DsDialogRouteImport + parentRoute: typeof DsRoute + } '/ds/colors': { id: '/ds/colors' path: '/colors' @@ -4318,6 +4394,8 @@ interface DsRouteChildren { DsButtonsRoute: typeof DsButtonsRoute DsCardsRoute: typeof DsCardsRoute DsColorsRoute: typeof DsColorsRoute + DsDialogRoute: typeof DsDialogRoute + DsDrawerRoute: typeof DsDrawerRoute DsDropdownRoute: typeof DsDropdownRoute DsEffectsRoute: typeof DsEffectsRoute DsEyebrowRoute: typeof DsEyebrowRoute @@ -4326,6 +4404,7 @@ interface DsRouteChildren { DsLogosRoute: typeof DsLogosRoute DsMaintainersRoute: typeof DsMaintainersRoute DsNavbarRoute: typeof DsNavbarRoute + DsOverlaysRoute: typeof DsOverlaysRoute DsPageHeaderRoute: typeof DsPageHeaderRoute DsPaletteRoute: typeof DsPaletteRoute DsPanelRoute: typeof DsPanelRoute @@ -4335,6 +4414,7 @@ interface DsRouteChildren { DsSpinnerRoute: typeof DsSpinnerRoute DsStatsRoute: typeof DsStatsRoute DsTabsRoute: typeof DsTabsRoute + DsTakeoverRoute: typeof DsTakeoverRoute DsTypographyRoute: typeof DsTypographyRoute DsIndexRoute: typeof DsIndexRoute } @@ -4346,6 +4426,8 @@ const DsRouteChildren: DsRouteChildren = { DsButtonsRoute: DsButtonsRoute, DsCardsRoute: DsCardsRoute, DsColorsRoute: DsColorsRoute, + DsDialogRoute: DsDialogRoute, + DsDrawerRoute: DsDrawerRoute, DsDropdownRoute: DsDropdownRoute, DsEffectsRoute: DsEffectsRoute, DsEyebrowRoute: DsEyebrowRoute, @@ -4354,6 +4436,7 @@ const DsRouteChildren: DsRouteChildren = { DsLogosRoute: DsLogosRoute, DsMaintainersRoute: DsMaintainersRoute, DsNavbarRoute: DsNavbarRoute, + DsOverlaysRoute: DsOverlaysRoute, DsPageHeaderRoute: DsPageHeaderRoute, DsPaletteRoute: DsPaletteRoute, DsPanelRoute: DsPanelRoute, @@ -4363,6 +4446,7 @@ const DsRouteChildren: DsRouteChildren = { DsSpinnerRoute: DsSpinnerRoute, DsStatsRoute: DsStatsRoute, DsTabsRoute: DsTabsRoute, + DsTakeoverRoute: DsTakeoverRoute, DsTypographyRoute: DsTypographyRoute, DsIndexRoute: DsIndexRoute, } diff --git a/src/routes/admin/roles.$roleId.tsx b/src/routes/admin/roles.$roleId.tsx index 85cd06bad..c8a663e83 100644 --- a/src/routes/admin/roles.$roleId.tsx +++ b/src/routes/admin/roles.$roleId.tsx @@ -20,6 +20,12 @@ import { import { requireCapability } from '~/utils/auth.functions' import { hasCapability } from '~/db/types' import { Badge, Button } from '~/ui' +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, +} from '~/components/ds/ui' export const Route = createFileRoute('/admin/roles/$roleId')({ beforeLoad: async () => { @@ -322,52 +328,56 @@ function RoleDetailPage() { )} - {confirmRemove && ( -
-
-

- Confirm Removal -

-

- Remove {confirmRemove.name} from role "{role?.name}"? -

-
- - -
-
-
- )} + { + if (!open) setConfirmRemove(null) + }} + > + + + + + + + +
diff --git a/src/routes/ds.dialog.tsx b/src/routes/ds.dialog.tsx new file mode 100644 index 000000000..4a1172972 --- /dev/null +++ b/src/routes/ds.dialog.tsx @@ -0,0 +1,464 @@ +import * as React from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { + CheckIcon, + GithubLogoIcon, + RocketIcon, + WarningCircleIcon, +} from '@phosphor-icons/react' +import { seo } from '~/utils/seo' +import { + Button, + Dialog, + DialogBody, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogStatus, + type DialogSize, + type DialogStatusTone, +} from '~/components/ds/ui' +import { ComponentPreview, DsPage, DsSection } from '~/components/ds/DsKit' + +export const Route = createFileRoute('/ds/dialog')({ + component: DialogPage, + head: () => ({ + meta: seo({ + title: 'Dialog | TanStack Design System', + description: + 'The centered modal dialog — Radix-backed, on the semantic token layer.', + }), + }), +}) + +const SIZES: Array = ['xs', 'sm', 'md', 'lg', 'xl'] + +/** Which DS token supplies each attribute of the panel. */ +const TOKEN_COVERAGE: Array<{ + attribute: string + token: string + status: 'existing' | 'added' + note: string +}> = [ + { + attribute: 'Panel surface', + token: 'bg-background-elevated', + status: 'existing', + note: 'Highest of the three background tiers. Identical to `surface` in light mode; #2b2b2b in dark, where the shipping dialogs use the warm gray-900 (#201b15) off a different ramp.', + }, + { + attribute: 'Panel border', + token: 'border-border-default', + status: 'existing', + note: 'Carries the edge in light mode, where all three background tiers are #ffffff and only shadow separates layers.', + }, + { + attribute: 'Elevation', + token: 'shadow-2xl', + status: 'existing', + note: 'Already the de facto modal elevation; 4 of 6 centered dialogs use it (one outlier at shadow-xl).', + }, + { + attribute: 'Corner radius', + token: 'rounded-xl corner-squircle', + status: 'existing', + note: 'rounded-xl is the audited majority. corner-squircle matches Card and Button.', + }, + { + attribute: 'Title / body text', + token: 'text-text-primary, text-text-muted', + status: 'existing', + note: 'Replaces text-gray-900 dark:text-gray-100 at every call site.', + }, + { + attribute: 'Close affordance', + token: 'text-icon-muted, hover:bg-surface-state-hover', + status: 'existing', + note: 'The interaction-state overlay tokens already exist and were unused by every dialog.', + }, + { + attribute: 'Focus ring', + token: 'ring-border-focus', + status: 'existing', + note: 'focus-visible only, so the ring does not appear on mouse click.', + }, + { + attribute: 'Scrim', + token: 'bg-scrim', + status: 'added', + note: 'Did not exist. Seven hand-picked black/NN values across the audit. Deliberately heavier in dark (0.65 vs 0.5) — equal alpha reads as weaker separation over an already-dark page.', + }, + { + attribute: 'Stacking tier', + token: 'z-[var(--z-scrim)] / z-[var(--z-overlay)]', + status: 'added', + note: 'Did not exist. Five unrelated z-index families were in use. Values set to 999/1000 — the existing majority — so adopting them moves nothing. A third tier, --z-above-overlay (1200), covers chrome that must float over an open overlay, such as a tooltip on a control inside a modal.', + }, + { + attribute: 'Motion', + token: 'animate-dialog-panel-in / -out', + status: 'added', + note: 'Keyframes are real. The animate-in / fade-in-0 / zoom-in-95 classes used elsewhere in the codebase come from tailwindcss-animate, which is not installed — they match zero CSS rules. Timing reuses --motion-duration-fast and --motion-ease-standard.', + }, +] + +function DialogPage() { + const [basic, setBasic] = React.useState(false) + const [scrolling, setScrolling] = React.useState(false) + const [destructive, setDestructive] = React.useState(false) + const [size, setSize] = React.useState(null) + const [tinted, setTinted] = React.useState(false) + const [statusTone, setStatusTone] = React.useState( + null, + ) + + return ( + + + + + + + + + + + + + +`} + > + + + + + +

+ Body content sits in its own scroll region, so a long dialog + scrolls internally instead of pushing the footer off-screen. +

+
+ + + + + + +
+
+
+
+ + + …`}> +
+ {SIZES.map((s) => ( + + ))} +
+ !open && setSize(null)} + > + + + +

+ max-w-{size} with a calc(100vw - 2rem) floor. +

+
+ + + + + +
+
+
+
+ + + + + {/* long content */} + +`} + > + + + + + + {Array.from({ length: 12 }).map((_, i) => ( +

+ Section {i + 1}. Long-form content demonstrating that the + body scrolls independently while the header and footer + remain pinned. +

+ ))} +
+ + + + + + +
+
+
+
+ + + + + + + + + + +`} + > + + + + + + + + + + + + + + + + + } + tint="#F38020" +/>`} + > + + + + } + tint="#F38020" + /> + +

+ A tinted header takes a rule and even padding, so it reads as + a banded region rather than bleeding into the body. +

+
+
+
+
+
+ + + } + title="Repository Created!" + description="Your repo is ready." + actions={} +/>`} + > +
+ {(['loading', 'neutral', 'success', 'error'] as const).map((t) => ( + + ))} +
+ !open && setStatusTone(null)} + > + + + + {statusTone === 'loading' ? ( + + ) : statusTone === 'neutral' ? ( + } + title="GitHub Authorization Required" + description="We need permission to create a repository on your account." + actions={} + /> + ) : statusTone === 'success' ? ( + } + title="Repository Created!" + description="tanstack/start-basic" + actions={} + /> + ) : statusTone === 'error' ? ( + } + title="Deployment Failed" + description="The repository name is already taken." + actions={ + <> + + + + } + /> + ) : null} + + + +
+
+ + +
+ + + + + + + + + + + {TOKEN_COVERAGE.map((row) => ( + + + + + + + ))} + +
AttributeTokenStatusNote
+ {row.attribute} + + + {row.token} + + + + {row.status} + + {row.note}
+
+
+ + +
    + {[ + 'Focus moves into the panel on open and returns to the trigger on close.', + 'Tab and Shift+Tab are trapped inside the panel.', + 'Escape dismisses. Clicking the scrim dismisses.', + 'Body scroll is locked while open.', + 'aria-modal, role="dialog", and the title/description associations are wired automatically.', + 'prefers-reduced-motion removes the animation but keeps the state change.', + ].map((line) => ( +
  • + + {line} +
  • + ))} +
+
+
+ ) +} diff --git a/src/routes/ds.drawer.tsx b/src/routes/ds.drawer.tsx new file mode 100644 index 000000000..0b6f3c80e --- /dev/null +++ b/src/routes/ds.drawer.tsx @@ -0,0 +1,350 @@ +import * as React from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { ArrowSquareOutIcon } from '@phosphor-icons/react' +import { seo } from '~/utils/seo' +import { + Button, + Drawer, + DrawerBody, + DrawerClose, + DrawerContent, + DrawerFooter, + DrawerHeader, + type DrawerAnchor, + type DrawerSide, + type DrawerSize, +} from '~/components/ds/ui' +import { ComponentPreview, DsPage, DsSection } from '~/components/ds/DsKit' + +export const Route = createFileRoute('/ds/drawer')({ + component: DrawerPage, + head: () => ({ + meta: seo({ + title: 'Drawer | TanStack Design System', + description: + 'The edge-anchored panel — right, left and bottom, on one Radix foundation.', + }), + }), +}) + +const SIDES: Array = ['right', 'left', 'bottom'] +const SIZES: Array = ['sm', 'md', 'lg', 'xl', '2xl'] + +function DrawerPage() { + const [side, setSide] = React.useState(null) + const [size, setSize] = React.useState(null) + const [guide, setGuide] = React.useState(false) + const [footer, setFooter] = React.useState(false) + const [fitDemo, setFitDemo] = React.useState<'full' | 'fit' | null>(null) + const [anchorDemo, setAnchorDemo] = React.useState(null) + + return ( + + + + + + + +`} + > +
+ {SIDES.map((s) => ( + + ))} +
+ !open && setSide(null)} + > + + + +

+ The panel slides without fading. A sheet reads as a physical + surface arriving from off-screen; fading it at the same time + makes it read as a dissolve instead. Only the scrim fades. +

+
+
+
+
+
+ + + …`} + > +
+ {SIZES.map((s) => ( + + ))} +
+ !open && setSize(null)} + > + + + +

+ sm through 2xl. 2xl matches the width the charts builder guide + uses today. +

+
+
+
+
+
+ + + … + +{/* hugs its content — footer sits under the last item */} +`} + > +
+ + +
+ !open && setFitDemo(null)} + > + + + +

+ Short content. Compare where the footer lands. +

+
+ + + + + +
+
+
+
+ + + … + +{/* clears the site header */} +`} + > +
+ + +
+ !open && setAnchorDemo(null)} + > + + + +

+ Scroll up to the header and compare where the panel's top edge + lands. `navbar` also shortens the height cap by the same + amount, so a `fit` panel still never runs off the bottom. +

+
+
+
+
+
+ + + + Plain text + + } +/>`} + > + + + + + Plain text + + + + + + + + + + + + +`} + > + + + + + + {['Classic Tee', 'Sticker Pack'].map((name) => ( +
+
+
+

+ {name} +

+

Qty 1

+
+ + $28.00 + +
+ ))} + + +
+ Subtotal + $56.00 +
+ + + +
+ + + + + + +
    + {[ + 'The panel slides only — no fade. The scrim fades.', + 'Exit animations play: Radix Presence waits for animationend before unmounting.', + 'prefers-reduced-motion removes the movement but keeps the state change.', + 'Bottom is centred with auto margins rather than a translate, so transform stays free for the slide — Tailwind v4 compiles -translate-x-1/2 to the independent translate property, which composes with transform rather than being replaced by it.', + ].map((line) => ( +
  • + + {line} +
  • + ))} +
+
+ + ) +} diff --git a/src/routes/ds.overlays.tsx b/src/routes/ds.overlays.tsx new file mode 100644 index 000000000..c27a91afd --- /dev/null +++ b/src/routes/ds.overlays.tsx @@ -0,0 +1,283 @@ +import * as React from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { CheckIcon, XIcon } from '@phosphor-icons/react' +import shopCss from '~/styles/shop.css?url' +import { seo } from '~/utils/seo' +import { Button } from '~/components/ds/ui' +import { DsPage, DsSection } from '~/components/ds/DsKit' +import { + AvatarCropModalSpecimen, + BuilderGuideDialogSpecimen, + CartDrawerSpecimen, + ExampleDeployDialogSpecimen, + LibrariesOverlaySpecimen, + LoginModalSpecimen, + NpmStatsDialogSpecimen, + ProductDrawerSpecimen, + RolesConfirmDialogSpecimen, + SearchModalSpecimen, + StarterDeployDialogSpecimen, +} from '~/components/ds/overlay-audit' +import { + DIVERGENCE, + SPECIMENS, + type SpecimenMeta, +} from '~/components/ds/overlay-audit/specimen-meta' + +export const Route = createFileRoute('/ds/overlays')({ + component: OverlayAuditPage, + // The two shop specimens depend on tokens defined in shop.css, which is + // otherwise only loaded under /shop. Linking it here is itself an audit + // finding: two of eleven overlays cannot render outside their own route. + head: () => ({ + links: [{ rel: 'stylesheet', href: shopCss }], + meta: seo({ + title: 'Overlay Audit | TanStack Design System', + description: + 'Every dialog, drawer and overlay shipping on the site, pulled into one page for side-by-side review.', + }), + }), +}) + +const SPECIMEN_COMPONENTS: Record< + string, + React.ComponentType<{ open: boolean; onOpenChange: (o: boolean) => void }> +> = { + login: LoginModalSpecimen, + 'avatar-crop': AvatarCropModalSpecimen, + 'npm-stats': NpmStatsDialogSpecimen, + 'roles-confirm': RolesConfirmDialogSpecimen, + 'example-deploy': ExampleDeployDialogSpecimen, + 'starter-deploy': StarterDeployDialogSpecimen, + 'builder-guide': BuilderGuideDialogSpecimen, + 'cart-drawer': CartDrawerSpecimen, + 'product-drawer': ProductDrawerSpecimen, + 'libraries-overlay': LibrariesOverlaySpecimen, + 'search-modal': SearchModalSpecimen, +} + +const POSTURE_ORDER: Array = [ + 'centered', + 'edge-sheet', + 'anchored-panel', + 'bottom-sheet', + 'top-anchored', + 'full-bleed', +] + +const POSTURE_LABEL: Record = { + centered: 'Centered', + 'edge-sheet': 'Edge sheet', + 'anchored-panel': 'Anchored panel', + 'bottom-sheet': 'Bottom sheet', + 'top-anchored': 'Top anchored', + 'full-bleed': 'Full bleed', +} + +function OverlayAuditPage() { + const [openId, setOpenId] = React.useState(null) + + return ( + + +
+ {POSTURE_ORDER.map((posture) => { + const group = SPECIMENS.filter((s) => s.posture === posture) + if (!group.length) return null + return ( +
+

+ {POSTURE_LABEL[posture]} + + {group.length} {group.length === 1 ? 'variant' : 'variants'} + +

+
+ {group.map((s) => ( + + ))} +
+
+ ) + })} +
+
+ + +
+ + + + + + + + + + + + + + + + + + {SPECIMENS.map((s) => ( + + + + + + + + + + + + + + ))} + +
SpecimenPostureBaseTokensz-indexScrimTrapRestoreEscLockAnim
+ +

+ {s.source.replace('src/', '')} · {s.sourceLines} lines +

+
+ {POSTURE_LABEL[s.posture]} + + + {s.base} + + + + {s.tokens} + + + {s.zIndex} + + {s.overlay} +
+
+
+ + +
+ {DIVERGENCE.map((d) => ( +
+
+

{d.property}

+ + {d.values.length} distinct values + +
+
+ {d.values.map((v) => ( + + {v} + + ))} +
+

{d.verdict}

+
+ ))} +
+
+ + +
+ {SPECIMENS.map((s) => ( +
+
{s.name}
+
{s.notes}
+
+ ))} +
+
+ + {SPECIMENS.map((s) => { + const Component = SPECIMEN_COMPONENTS[s.id] + if (!Component) return null + return ( + setOpenId(next ? s.id : null)} + /> + ) + })} +
+ ) +} + +function BoolCell({ value }: { value: boolean }) { + return ( + + {value ? ( + + ) : ( + + )} + + ) +} diff --git a/src/routes/ds.palette.tsx b/src/routes/ds.palette.tsx index e966eb8f9..75b67fc00 100644 --- a/src/routes/ds.palette.tsx +++ b/src/routes/ds.palette.tsx @@ -15,6 +15,15 @@ export const Route = createFileRoute('/ds/palette')({ const RAMPS = ['green', 'terracotta', 'blue', 'purple', 'amber', 'neutral'] const STEPS = [100, 200, 300, 400, 500] +/** + * Neutral carries two half-steps the chromatic ramps do not need. Each is the + * exact midpoint of its neighbours, added so the text scale has a legible + * `secondary` in dark and a legible `muted` in light. + */ +const RAMP_STEPS: Record> = { + neutral: [100, 150, 200, 300, 350, 400, 500], +} + const CATEGORY_COLORS = ['framework', 'data', 'ui', 'performance', 'tooling'] const LIBRARY_COLORS = [ @@ -42,9 +51,17 @@ function PalettePage() { description="The primitive color ramps sourced from Figma. These feed the semantic tokens — change a primitive here (in app.css) and every semantic token referencing it updates across the system. Click a swatch to copy its var() reference." > {RAMPS.map((ramp) => ( - +
- {STEPS.map((step) => ( + {(RAMP_STEPS[ramp] ?? STEPS).map((step) => ( ))}
diff --git a/src/routes/ds.takeover.tsx b/src/routes/ds.takeover.tsx new file mode 100644 index 000000000..2de71a5e9 --- /dev/null +++ b/src/routes/ds.takeover.tsx @@ -0,0 +1,158 @@ +import * as React from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { ArrowLeftIcon } from '@phosphor-icons/react' +import { seo } from '~/utils/seo' +import { + Button, + Takeover, + TakeoverContent, + TakeoverDescription, + TakeoverTitle, + type TakeoverScrim, +} from '~/components/ds/ui' +import { ComponentPreview, DsPage, DsSection } from '~/components/ds/DsKit' + +export const Route = createFileRoute('/ds/takeover')({ + component: TakeoverPage, + head: () => ({ + meta: seo({ + title: 'Takeover | TanStack Design System', + description: 'The full-bleed, immersive overlay posture.', + }), + }), +}) + +function TakeoverPage() { + const [scrim, setScrim] = React.useState(null) + const [withLeading, setWithLeading] = React.useState(false) + + return ( + + + + + All Libraries + … + +`} + > +
+ {(['standard', 'glass'] as const).map((s) => ( + + ))} +
+ !open && setScrim(null)} + > + +
+ + scrim="{scrim}" + + + The content is the scroll container. Clicking its empty space + dismisses, which is why a takeover does not need a separate + backdrop element. + +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+

+ Card {i + 1} +

+

+ Content scrolls edge to edge. +

+
+ ))} +
+
+
+
+
+
+ + + + Back to menu + + } +/>`} + > + + + + + Back to menu + + } + > +
+ + Leading action + + + Both floating controls sit above the scrolling content and + stay put as it moves. + +
+
+
+
+
+ + +
    + {[ + 'No panel and no header bar. TakeoverTitle / TakeoverDescription go wherever the content wants them — visible or sr-only.', + 'The content element is the scroll container, so a takeover never nests a second scrollbar.', + 'Clicking the content’s own empty space dismisses. It routes through the same close button rather than a second dismissal path that could drift from it.', + 'onInteractOutside is forwarded, for content that portals menus of its own and must not close when they are clicked.', + 'The surface fades rather than slides: nothing is arriving from an edge, the whole surface is being replaced.', + ].map((line) => ( +
  • + + {line} +
  • + ))} +
+
+
+ ) +} diff --git a/src/routes/stats/npm/index.tsx b/src/routes/stats/npm/index.tsx index ee83fa58b..4f9fb7ffa 100644 --- a/src/routes/stats/npm/index.tsx +++ b/src/routes/stats/npm/index.tsx @@ -2,10 +2,15 @@ import * as React from 'react' import { createFileRoute } from '@tanstack/react-router' import * as v from 'valibot' import { useThrottledCallback, useThrottler } from '@tanstack/react-pacer' -import * as DialogPrimitive from '@radix-ui/react-dialog' -import { QuestionIcon, XIcon } from '@phosphor-icons/react' +import { QuestionIcon } from '@phosphor-icons/react' import { useQuery } from '@tanstack/react-query' -import { Card } from '~/components/ds/ui' +import { + Card, + Dialog, + DialogBody, + DialogContent, + DialogHeader, +} from '~/components/ds/ui' import { Tooltip } from '~/components/Tooltip' import { seo } from '~/utils/seo' import { chartHeightSchema, chartWidthSchema } from '~/utils/schemas' @@ -1074,27 +1079,17 @@ function RouteComponent() {
{/* Combine Package Dialog */} - { if (!open) setCombiningPackage(null) }} > - - - -
- - Add packages to {combiningPackage} - - - - -
- - Search for additional npm packages to combine with{' '} - {combiningPackage}. - + + + {combiningPackage && ( )} -
-
-
+ + + {/* Color Picker Popover */} {colorPickerPackage && colorPickerPosition && ( diff --git a/src/styles/app.css b/src/styles/app.css index b22f9bacb..90b86ca4c 100644 --- a/src/styles/app.css +++ b/src/styles/app.css @@ -154,6 +154,361 @@ html.theme-switching *::after { --motion-duration-sheet: 280ms; --motion-ease-standard: cubic-bezier(0.2, 0.8, 0.2, 1); --motion-ease-sheet: cubic-bezier(0.32, 0.72, 0, 1); + + /* Stacking tiers, shared by every modal posture (dialog, drawer, …). + Tailwind has no `--z-*` theme namespace, so these are applied as + `z-[var(--z-overlay)]`. Values match the 999/1000 pair already used by the + majority of overlays, so adopting them moves nothing. */ + --z-scrim: 999; + --z-overlay: 1000; + /* Floats above an open overlay — a tooltip on a control inside a modal has + nowhere else to go. Kept well clear of --z-overlay so overlay-internal + chrome can still stack between them. */ + --z-above-overlay: 1200; +} + +/* ----------------------------------------------------------------- Dialog -- + Enter/exit motion for the DS Dialog, driven off Radix's data-state rather + than utility classes. Written as real keyframes on purpose: the + `animate-in` / `fade-in-0` / `zoom-in-95` classes used elsewhere in this + codebase come from tailwindcss-animate, which is NOT installed — they match + no CSS rule and animate nothing. Timing reuses the existing motion tokens. + + Radix's Presence waits for animationend before unmounting, so the closed + state genuinely plays instead of being cut off. + + The panel keyframes animate `transform` only. Tailwind v4's + `-translate-x-1/2 -translate-y-1/2` compile to the independent `translate` + property, which composes with `transform` rather than being overwritten by + it — repeating the centring translate here would shift the panel a full + height instead of half and hang it off the top of the viewport. + -------------------------------------------------------------------------- */ +@keyframes dialog-scrim-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes dialog-scrim-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +@keyframes dialog-panel-in { + from { + opacity: 0; + transform: scale(0.96); + } + to { + opacity: 1; + transform: scale(1); + } +} +@keyframes dialog-panel-out { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.96); + } +} + +[data-ds-dialog-scrim][data-state='open'] { + animation: dialog-scrim-in var(--motion-duration-fast) + var(--motion-ease-standard) both; +} +[data-ds-dialog-scrim][data-state='closed'] { + animation: dialog-scrim-out var(--motion-duration-fast) + var(--motion-ease-standard) both; +} +[data-ds-dialog-panel][data-state='open'] { + animation: dialog-panel-in var(--motion-duration-fast) + var(--motion-ease-standard) both; +} +[data-ds-dialog-panel][data-state='closed'] { + animation: dialog-panel-out var(--motion-duration-fast) + var(--motion-ease-standard) both; +} + +/* The dialog is the first DS component with real motion, so it states the + reduced-motion contract the rest of the system should follow: the state + change still happens, it just stops moving. */ +/* ------------------------------------------------- Transient surfaces & -- + ------------------------------------------------- one-shot entrances -- + Same reason as the dialog keyframes: the `animate-in` / `fade-in-0` / + `zoom-in-95` / `slide-in-from-*` classes these replaced come from + tailwindcss-animate, which is not installed. They matched no CSS rule and + animated nothing. The `duration-*` utilities alongside them were inert too — + Tailwind's duration sets transition-duration, not animation-duration. + + `transform` carries scale/offset only, never a centring translate: Tailwind + v4 compiles `-translate-x-1/2` to the independent `translate` property, + which composes with `transform` rather than being replaced by it. That also + leaves Radix's --radix-*-content-transform-origin free to govern where a + popover grows from. + -------------------------------------------------------------------------- */ +@keyframes ds-pop-in { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} +@keyframes ds-pop-out { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.95); + } +} + +/* Radix-driven transient surfaces: tooltips, dropdown menus. + Tooltip does not use `open` — it reports `delayed-open` when it appears + after the hover delay and `instant-open` inside the skip-delay window, so + all three have to be matched or tooltips silently never animate. */ +[data-ds-pop][data-state='open'], +[data-ds-pop][data-state='delayed-open'], +[data-ds-pop][data-state='instant-open'] { + animation: ds-pop-in var(--motion-duration-fast) var(--motion-ease-standard) + both; +} +[data-ds-pop][data-state='closed'] { + animation: ds-pop-out var(--motion-duration-fast) var(--motion-ease-standard) + both; +} + +/* One-shot entrances for content that mounts already open. */ +@keyframes ds-enter-rise { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.ds-enter-fade { + animation: takeover-in var(--motion-duration-sheet) + var(--motion-ease-standard) both; +} +.ds-enter-rise { + animation: ds-enter-rise var(--motion-duration-sheet) + var(--motion-ease-standard) both; +} +.ds-enter-pop { + animation: ds-pop-in var(--motion-duration-sheet) var(--motion-ease-standard) + both; +} + +@media (prefers-reduced-motion: reduce) { + [data-ds-pop], + .ds-enter-fade, + .ds-enter-rise, + .ds-enter-pop { + animation: none !important; + } +} + +/* --------------------------------------------------------------- Takeover -- + The full-bleed posture. Two scrims, which is what the overlay audit asked + for: `standard` is the same --color-scrim every other overlay uses, and + `glass` is the immersive treatment that dissolves the page behind a heavy + blur instead of dimming it. Seven hand-picked scrims existed before this. + + The panel fades rather than slides — nothing is arriving from an edge, the + whole surface is being replaced. + -------------------------------------------------------------------------- */ +[data-ds-takeover-scrim='standard'] { + background: var(--color-scrim); +} + +[data-ds-takeover-scrim='glass'] { + background: rgb(255 255 255 / 0.95); + -webkit-backdrop-filter: blur(40px) saturate(1.5); + backdrop-filter: blur(40px) saturate(1.5); +} + +html.dark [data-ds-takeover-scrim='glass'] { + background: rgb(0 0 0 / 0.95); +} + +@keyframes takeover-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes takeover-out { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +[data-ds-takeover-scrim][data-state='open'], +[data-ds-takeover-panel][data-state='open'] { + animation: takeover-in var(--motion-duration-fast) var(--motion-ease-standard) + both; +} +[data-ds-takeover-scrim][data-state='closed'], +[data-ds-takeover-panel][data-state='closed'] { + animation: takeover-out var(--motion-duration-fast) + var(--motion-ease-standard) both; +} + +@media (prefers-reduced-motion: reduce) { + [data-ds-takeover-scrim], + [data-ds-takeover-panel] { + animation: none !important; + } +} + +/* ----------------------------------------------------------------- Drawer -- + Edge-anchored panels. Slides only — the panel does not fade, because a + sheet reads as a physical surface arriving from off-screen and a + simultaneous fade makes it read as a dissolve instead. The scrim fades. + + Timing uses --motion-duration-sheet / --motion-ease-sheet, which already + existed for exactly this and were previously used only by /shop. + + `bottom` is centred with `left-4 right-4 mx-auto` rather than a translate, + which deliberately leaves `transform` free for the slide. Do not swap that + for `left-1/2 -translate-x-1/2`: Tailwind v4 compiles those to the + independent `translate` property, which composes with `transform`. + -------------------------------------------------------------------------- */ +@keyframes drawer-right-in { + from { + transform: translateX(100%); + } + to { + transform: translateX(0); + } +} +@keyframes drawer-right-out { + from { + transform: translateX(0); + } + to { + transform: translateX(100%); + } +} +@keyframes drawer-left-in { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} +@keyframes drawer-left-out { + from { + transform: translateX(0); + } + to { + transform: translateX(-100%); + } +} +@keyframes drawer-bottom-in { + from { + transform: translateY(100%); + } + to { + transform: translateY(0); + } +} +@keyframes drawer-bottom-out { + from { + transform: translateY(0); + } + to { + transform: translateY(100%); + } +} + +[data-ds-drawer-panel][data-side='right'][data-state='open'] { + animation: drawer-right-in var(--motion-duration-sheet) + var(--motion-ease-sheet) both; +} +[data-ds-drawer-panel][data-side='right'][data-state='closed'] { + animation: drawer-right-out var(--motion-duration-sheet) + var(--motion-ease-sheet) both; +} +[data-ds-drawer-panel][data-side='left'][data-state='open'] { + animation: drawer-left-in var(--motion-duration-sheet) + var(--motion-ease-sheet) both; +} +[data-ds-drawer-panel][data-side='left'][data-state='closed'] { + animation: drawer-left-out var(--motion-duration-sheet) + var(--motion-ease-sheet) both; +} +[data-ds-drawer-panel][data-side='bottom'][data-state='open'] { + animation: drawer-bottom-in var(--motion-duration-sheet) + var(--motion-ease-sheet) both; +} +[data-ds-drawer-panel][data-side='bottom'][data-state='closed'] { + animation: drawer-bottom-out var(--motion-duration-sheet) + var(--motion-ease-sheet) both; +} + +/* Wizard step cross-fade. Steps swap instantly today, so an outcome can + replace a spinner with no visual connection between them. Short and + opacity-only: the panel must not resize or shift, or the footer buttons jump + under the cursor mid-click. */ +@keyframes dialog-step-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +[data-ds-dialog-step] { + animation: dialog-step-in var(--motion-duration-fast) + var(--motion-ease-standard) both; +} + +@media (prefers-reduced-motion: reduce) { + [data-ds-dialog-step] { + animation: none !important; + } +} + +[data-ds-drawer-scrim][data-state='open'] { + animation: dialog-scrim-in var(--motion-duration-sheet) + var(--motion-ease-sheet) both; +} +[data-ds-drawer-scrim][data-state='closed'] { + animation: dialog-scrim-out var(--motion-duration-sheet) + var(--motion-ease-sheet) both; +} + +@media (prefers-reduced-motion: reduce) { + [data-ds-dialog-scrim], + [data-ds-dialog-panel], + [data-ds-drawer-scrim], + [data-ds-drawer-panel] { + animation: none !important; + } } /* ============================================================================ @@ -199,8 +554,15 @@ html.theme-switching *::after { --color-ds-neutral-0: #ffffff; --color-ds-neutral-100: #eeebd4; + /* Half-steps. Both are the exact midpoint of their neighbours, so they sit + on the ramp's hue rather than beside it. They exist because the text scale + needed a legible `secondary` in dark (200 was only 7.8:1 against #111111, + against 12.0:1 for its light counterpart) and a legible `muted` in light + (300 was 5.2:1). NOTE: not yet in Figma — add them there when syncing. */ + --color-ds-neutral-150: #cec8b2; --color-ds-neutral-200: #aea691; --color-ds-neutral-300: #756c5b; + --color-ds-neutral-350: #5a5042; --color-ds-neutral-400: #3e3529; --color-ds-neutral-500: #111111; @@ -299,7 +661,7 @@ html.theme-switching *::after { using neutral-500 for the light context. Confirm intended value. */ --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-muted: var(--color-ds-neutral-350); /* Mega-menu item title rest color; brightens to text-primary on hover. Light menu → muted dark; dark menu → neutral tint (see html.dark). */ --color-text-menu-title: var(--color-ds-neutral-400); @@ -317,6 +679,11 @@ html.theme-switching *::after { --color-background-subtle: #fafafa; --color-background-inverse: var(--color-ds-neutral-500); + /* Scrim behind modal surfaces. Deliberately heavier in dark mode: an + identical alpha reads as weaker separation once the page beneath it is + already dark. Replaces seven hand-picked black/NN values. */ + --color-scrim: rgb(0 0 0 / 0.5); + --color-border-default: var(--color-ds-neutral-200); --color-border-strong: var(--color-ds-neutral-400); --color-border-subtle: var(--color-ds-neutral-100); @@ -538,8 +905,11 @@ html.light body { Primitives don't change between modes — only these semantic mappings do. */ html.dark { --color-text-primary: #ffffff; - --color-text-secondary: #aea691; - --color-text-muted: #756c5b; + /* = ds-neutral-150 and ds-neutral-200. Kept as literals to match the rest of + this block; secondary and muted each moved one step lighter so both clear + AA against #111111 (11.3:1 and 7.8:1, from 7.8:1 and a failing 3.6:1). */ + --color-text-secondary: #cec8b2; + --color-text-muted: #aea691; --color-text-menu-title: var(--color-ds-neutral-tint-200); --color-text-accent: #61adbf; --color-text-disabled: #3e3529; @@ -555,6 +925,8 @@ html.dark { --color-background-subtle: #1b1b1b; --color-background-inverse: #ffffff; + --color-scrim: rgb(0 0 0 / 0.65); + --color-border-default: #2d2d2d; --color-border-strong: #aea691; --color-border-subtle: #232323; @@ -619,7 +991,7 @@ html.dark { 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-muted: var(--color-ds-neutral-350); --color-text-inverse: #ffffff; --color-text-error: var(--color-ds-terracotta-500); --color-background-default: #ffffff; @@ -627,6 +999,7 @@ html.dark { --color-background-elevated: #ffffff; --color-background-subtle: #fafafa; --color-background-inverse: var(--color-ds-neutral-500); + --color-scrim: rgb(0 0 0 / 0.5); --color-border-default: var(--color-ds-neutral-200); --color-border-subtle: var(--color-ds-neutral-100); --color-border-strong: var(--color-ds-neutral-400); @@ -661,8 +1034,11 @@ html.dark { .ds-mode-dark { color-scheme: dark; --color-text-primary: #ffffff; - --color-text-secondary: #aea691; - --color-text-muted: #756c5b; + /* = ds-neutral-150 and ds-neutral-200. Kept as literals to match the rest of + this block; secondary and muted each moved one step lighter so both clear + AA against #111111 (11.3:1 and 7.8:1, from 7.8:1 and a failing 3.6:1). */ + --color-text-secondary: #cec8b2; + --color-text-muted: #aea691; --color-text-inverse: #111111; --color-text-error: var(--color-ds-terracotta-200); --color-background-default: #111111; @@ -670,6 +1046,7 @@ html.dark { --color-background-elevated: #2b2b2b; --color-background-subtle: #1b1b1b; --color-background-inverse: #ffffff; + --color-scrim: rgb(0 0 0 / 0.65); --color-border-default: #2d2d2d; --color-border-subtle: #232323; --color-border-strong: #aea691; diff --git a/src/styles/shop.css b/src/styles/shop.css index 0fa3ecfc3..29ef79756 100644 --- a/src/styles/shop.css +++ b/src/styles/shop.css @@ -95,16 +95,11 @@ html.dark .shop-scope { min-height: 100%; } -.shop-product-scrim { - transition-duration: var(--motion-duration-fast); -} - +/* Height and enter/exit motion now come from the DS Drawer. This keeps only + the custom property, which descendant rules below still read, so the cap + has exactly one source of truth (applied as a max-h utility on the panel). */ .shop-product-sheet { --shop-product-sheet-top: calc(var(--navbar-height, 56px) + 48px); - - max-height: calc(100svh - var(--shop-product-sheet-top)); - transition-duration: var(--motion-duration-sheet); - transition-timing-function: var(--motion-ease-sheet); } .shop-product-animate .shop-product-reveal { diff --git a/src/ui/Tooltip.tsx b/src/ui/Tooltip.tsx index 6cb52ded7..5d759704e 100644 --- a/src/ui/Tooltip.tsx +++ b/src/ui/Tooltip.tsx @@ -32,13 +32,12 @@ export function Tooltip({ side={side} align={align} sideOffset={5} + data-ds-pop="" className={twMerge( - 'z-1300 rounded-lg px-3 py-2 text-xs', + 'z-[var(--z-above-overlay)] rounded-lg px-3 py-2 text-xs', 'bg-background-inverse text-text-inverse', 'shadow-lg', - 'animate-in fade-in-0 zoom-in-95', - 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95', - '[transform-origin:var(--radix-tooltip-content-transform-origin)] motion-reduce:animate-none', + '[transform-origin:var(--radix-tooltip-content-transform-origin)]', className, )} > From 049b9d98e1e098da5fb6338cc825252820836a7c Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 31 Aug 2026 15:57:32 -0600 Subject: [PATCH 12/17] Update Vercel partner logos --- src/images/vercel-dark.svg | 4 +++- src/images/vercel-light.svg | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/images/vercel-dark.svg b/src/images/vercel-dark.svg index f8b16b6a9..9f67d8fec 100644 --- a/src/images/vercel-dark.svg +++ b/src/images/vercel-dark.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/src/images/vercel-light.svg b/src/images/vercel-light.svg index bf8d7259f..84c419ff6 100644 --- a/src/images/vercel-light.svg +++ b/src/images/vercel-light.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + From f3d96dc6b553110b9d220f46d8ebc1193942734e Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 31 Aug 2026 16:34:37 -0600 Subject: [PATCH 13/17] feat: add Vercel and Render builder actions --- .agents/analytics.md | 1 + src/application-starter/api/create-worker.ts | 12 ++++ src/components/ApplicationStarter.tsx | 36 ++++++++++-- .../application-starter/prompt-deploy.ts | 47 ++++++++++++++++ .../application-starter/prompt-shared.ts | 32 ++--------- .../useApplicationStarter.tsx | 4 +- src/components/deploy/shared.ts | 16 +++++- src/utils/analytics/events.ts | 1 + src/utils/application-starter.ts | 18 +++++- src/utils/partners.tsx | 6 +- tests/application-starter-partners.test.ts | 30 ++++++++++ .../application-starter-prompt-deploy.test.ts | 44 +++++++++++++++ tests/create-worker.test.ts | 56 ++++++++++++++++++- tests/deploy-provider.test.ts | 17 ++++++ 14 files changed, 280 insertions(+), 40 deletions(-) create mode 100644 src/components/application-starter/prompt-deploy.ts create mode 100644 tests/application-starter-prompt-deploy.test.ts create mode 100644 tests/deploy-provider.test.ts diff --git a/.agents/analytics.md b/.agents/analytics.md index c1ba501ac..dbcf3ab58 100644 --- a/.agents/analytics.md +++ b/.agents/analytics.md @@ -203,6 +203,7 @@ User took an action on the generated result. Single event with `action` prop cov | `open_codex` | Opened the result in Codex | | `open_claude` | Opened the result in Claude | | `open_cursor` | Opened the result in Cursor | +| `open_prompt_builder` | Opened the result in a partner's prompt-based builder | | `download` | Downloaded the project as a zip | | `open_advanced` | Opened the advanced builder editor | | `netlify_start` | Started a Netlify deploy from the result | diff --git a/src/application-starter/api/create-worker.ts b/src/application-starter/api/create-worker.ts index 8bdd95275..921f99a9d 100644 --- a/src/application-starter/api/create-worker.ts +++ b/src/application-starter/api/create-worker.ts @@ -512,6 +512,9 @@ const reactAddOnLoaders = defineAddOnLoaders({ railway: loadAddOn(() => import('@tanstack/create/worker-manifest/frameworks/react/add-ons/railway'), ), + render: loadAddOn(() => + import('@tanstack/create/worker-manifest/frameworks/react/add-ons/render'), + ), resume: loadAddOn(() => import('@tanstack/create/worker-manifest/frameworks/react/add-ons/resume'), ), @@ -554,6 +557,9 @@ const reactAddOnLoaders = defineAddOnLoaders({ tRPC: loadAddOn(() => import('@tanstack/create/worker-manifest/frameworks/react/add-ons/trpc'), ), + vercel: loadAddOn(() => + import('@tanstack/create/worker-manifest/frameworks/react/add-ons/vercel'), + ), workos: loadAddOn(() => import('@tanstack/create/worker-manifest/frameworks/react/add-ons/workos'), ), @@ -591,6 +597,9 @@ const solidAddOnLoaders = defineAddOnLoaders({ railway: loadAddOn(() => import('@tanstack/create/worker-manifest/frameworks/solid/add-ons/railway'), ), + render: loadAddOn(() => + import('@tanstack/create/worker-manifest/frameworks/solid/add-ons/render'), + ), sentry: loadAddOn(() => import('@tanstack/create/worker-manifest/frameworks/solid/add-ons/sentry'), ), @@ -616,6 +625,9 @@ const solidAddOnLoaders = defineAddOnLoaders({ '@tanstack/create/worker-manifest/frameworks/solid/add-ons/tanstack-query' ), ), + vercel: loadAddOn(() => + import('@tanstack/create/worker-manifest/frameworks/solid/add-ons/vercel'), + ), }) const addOnLoaders: Record< diff --git a/src/components/ApplicationStarter.tsx b/src/components/ApplicationStarter.tsx index edf42e9ef..01fcae607 100644 --- a/src/components/ApplicationStarter.tsx +++ b/src/components/ApplicationStarter.tsx @@ -29,9 +29,12 @@ import { } from '~/components/application-starter/prompt-parts' import { buildStarterPromptDeployUrl, + getStarterPromptBuildLabel, + type StarterPromptDeployProvider, +} from '~/components/application-starter/prompt-deploy' +import { toneClasses, type ApplicationStarterIntegration, - type StarterPromptDeployProvider, type StarterTone, } from '~/components/application-starter/prompt-shared' import { useApplicationStarter } from '~/components/application-starter/useApplicationStarter' @@ -79,7 +82,13 @@ const starterToolchains = ['biome', 'eslint'] as const const starterEyebrowClassName = 'font-ds-mono text-ds-mono-xs uppercase tracking-wider text-text-muted' -type HostingDeployPartnerId = 'cloudflare' | 'lovable' | 'netlify' | 'railway' +type HostingDeployPartnerId = + | 'cloudflare' + | 'lovable' + | 'netlify' + | 'render' + | 'railway' + | 'vercel' type StarterTransientAction = | 'claude' | 'clone' @@ -93,7 +102,9 @@ const hostingDeployPartnerLabels: Record = { cloudflare: 'Cloudflare', lovable: 'Lovable', netlify: 'Netlify', + render: 'Render', railway: 'Railway', + vercel: 'Vercel', } function getHostingDeployPartnerId( @@ -103,7 +114,9 @@ function getHostingDeployPartnerId( case 'cloudflare': case 'lovable': case 'netlify': + case 'render': case 'railway': + case 'vercel': return partnerId default: return undefined @@ -117,7 +130,10 @@ function getPromptDeployProvider( case 'lovable': case 'netlify': return partnerId + case 'vercel': + return 'v0' case 'cloudflare': + case 'render': case 'railway': return undefined } @@ -405,7 +421,9 @@ export function ApplicationStarter({ trackActivation({ action: - selectedHostingDeployPartner === 'netlify' ? 'netlify_start' : 'deploy', + selectedHostingDeployPartner === 'netlify' + ? 'netlify_start' + : 'open_prompt_builder', surface: 'result_panel', provider: selectedHostingDeployPartner, }) @@ -426,9 +444,14 @@ export function ApplicationStarter({ break case 'netlify': break + case 'render': + await openDeployDialog('render') + break case 'railway': await openDeployDialog('railway') break + case 'vercel': + break } } finally { setPendingHostingDeployPartner(null) @@ -574,6 +597,9 @@ export function ApplicationStarter({ } if (selectedPromptDeployProvider) { + const buildLabel = getStarterPromptBuildLabel( + selectedPromptDeployProvider, + ) const disabled = !canUseFinalActions || !selectedHostingDeployHref || @@ -601,7 +627,7 @@ export function ApplicationStarter({ showTransientActionFeedback('deploy') }} className={disabled ? 'pointer-events-none opacity-50' : undefined} - aria-label={`Deploy to ${hostingDeployPartnerLabels[selectedHostingDeployPartner]}`} + aria-label={buildLabel} > {isDeployFeedbackActive || waitingForHref ? ( @@ -612,7 +638,7 @@ export function ApplicationStarter({ ? 'Opening...' : waitingForHref ? 'Preparing...' - : 'Deploy'} + : buildLabel} ) } diff --git a/src/components/application-starter/prompt-deploy.ts b/src/components/application-starter/prompt-deploy.ts new file mode 100644 index 000000000..0cb6fa131 --- /dev/null +++ b/src/components/application-starter/prompt-deploy.ts @@ -0,0 +1,47 @@ +const starterPromptBuildProductLabels = { + lovable: 'Lovable', + netlify: 'Netlify', + v0: 'v0', +} + +export type StarterPromptDeployProvider = + keyof typeof starterPromptBuildProductLabels + +export function buildStarterPromptDeployUrl( + provider: StarterPromptDeployProvider, + prompt: string, +) { + switch (provider) { + case 'lovable': { + const url = new URL('https://lovable.dev/') + + url.searchParams.set('autosubmit', 'true') + url.searchParams.set('utm_source', 'tanstack') + url.hash = `prompt=${encodeURIComponent(prompt)}` + + return url.toString() + } + case 'netlify': { + const url = new URL('https://app.netlify.com/start') + + url.searchParams.set('prompt', prompt) + url.searchParams.set('utm_source', 'tanstack') + + return url.toString() + } + case 'v0': { + const url = new URL('https://v0.app/') + + url.searchParams.set('q', prompt) + url.searchParams.set('utm_source', 'tanstack') + + return url.toString() + } + } +} + +export function getStarterPromptBuildLabel( + provider: StarterPromptDeployProvider, +) { + return `Build with ${starterPromptBuildProductLabels[provider]}` +} diff --git a/src/components/application-starter/prompt-shared.ts b/src/components/application-starter/prompt-shared.ts index d75ddd80a..fdd641616 100644 --- a/src/components/application-starter/prompt-shared.ts +++ b/src/components/application-starter/prompt-shared.ts @@ -9,8 +9,11 @@ import { } from '~/utils/application-starter' export type StarterTone = 'cyan' | 'emerald' | 'violet' -export type StarterDeployProvider = 'cloudflare' | 'netlify' | 'railway' -export type StarterPromptDeployProvider = 'lovable' | 'netlify' +export type StarterDeployProvider = + | 'cloudflare' + | 'netlify' + | 'railway' + | 'render' export type StarterPackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn' export type StarterToolchain = 'biome' | 'eslint' @@ -144,31 +147,6 @@ export const starterLoadingPhrases = [ 'Finding calmer waters...', ] -export function buildStarterPromptDeployUrl( - provider: StarterPromptDeployProvider, - prompt: string, -) { - switch (provider) { - case 'lovable': { - const url = new URL('https://lovable.dev/') - - url.searchParams.set('autosubmit', 'true') - url.searchParams.set('utm_source', 'tanstack') - url.hash = `prompt=${encodeURIComponent(prompt)}` - - return url.toString() - } - case 'netlify': { - const url = new URL('https://app.netlify.com/start') - - url.searchParams.set('prompt', prompt) - url.searchParams.set('utm_source', 'tanstack') - - return url.toString() - } - } -} - export function isPinnedStarterLibrary(libraryId: LibraryId) { return starterPinnedLibraryIds.some( (pinnedLibraryId) => pinnedLibraryId === libraryId, diff --git a/src/components/application-starter/useApplicationStarter.tsx b/src/components/application-starter/useApplicationStarter.tsx index 4975c614f..d626368c2 100644 --- a/src/components/application-starter/useApplicationStarter.tsx +++ b/src/components/application-starter/useApplicationStarter.tsx @@ -25,8 +25,8 @@ import { } from '~/utils/partners' import { usePartnerPlacementContext } from '~/utils/usePartnerPlacementContext' import type { LibraryId } from '~/libraries' +import { buildStarterPromptDeployUrl } from './prompt-deploy' import { - buildStarterPromptDeployUrl, composeStarterInput, isNextJsMigrationInput, isPinnedStarterLibrary, @@ -839,7 +839,7 @@ export function useApplicationStarter({ try { await withResolvedPrompt((nextResult) => { trackActivation({ - action: 'deploy', + action: 'open_prompt_builder', surface: 'result_panel', provider: 'lovable', }) diff --git a/src/components/deploy/shared.ts b/src/components/deploy/shared.ts index 6e210d68a..70e44db8f 100644 --- a/src/components/deploy/shared.ts +++ b/src/components/deploy/shared.ts @@ -4,7 +4,7 @@ * Common types, constants, and validation for deploy dialogs. */ -export type DeployProvider = 'cloudflare' | 'netlify' | 'railway' +export type DeployProvider = 'cloudflare' | 'netlify' | 'railway' | 'render' export type DeployState = | { step: 'auth-check' } @@ -60,6 +60,20 @@ export const PROVIDER_INFO: Record = { url.searchParams.set('utm_source', 'oss') url.searchParams.set('utm_campaign', 'tanstack') + return url.toString() + }, + }, + render: { + name: 'Render', + color: '#46E3B7', + deployUrl: (owner, repo) => { + const url = new URL('https://render.com/deploy') + + url.searchParams.set('repo', `https://github.com/${owner}/${repo}`) + url.searchParams.set('utm_source', 'tanstack') + url.searchParams.set('utm_medium', 'referral') + url.searchParams.set('utm_campaign', 'gold-launch') + return url.toString() }, }, diff --git a/src/utils/analytics/events.ts b/src/utils/analytics/events.ts index d69670437..0b4af009f 100644 --- a/src/utils/analytics/events.ts +++ b/src/utils/analytics/events.ts @@ -46,6 +46,7 @@ export type ApplicationStarterAction = | 'open_codex' | 'open_claude' | 'open_cursor' + | 'open_prompt_builder' | 'download' | 'open_advanced' | 'netlify_start' diff --git a/src/utils/application-starter.ts b/src/utils/application-starter.ts index f2b696edc..145cbfe86 100644 --- a/src/utils/application-starter.ts +++ b/src/utils/application-starter.ts @@ -21,7 +21,13 @@ export type ApplicationStarterResultType = | 'scaffoldable' export interface ApplicationStarterRecipe { - deployment?: 'cloudflare' | 'netlify' | 'nitro' | 'railway' + deployment?: + | 'cloudflare' + | 'netlify' + | 'nitro' + | 'railway' + | 'render' + | 'vercel' featureOptions: Record> features: Array framework: FrameworkId @@ -759,6 +765,10 @@ function applyPartnerOverrides( recipe.deployment = 'netlify' } else if (partnerIds.has('railway')) { recipe.deployment = 'railway' + } else if (partnerIds.has('render')) { + recipe.deployment = 'render' + } else if (partnerIds.has('vercel')) { + recipe.deployment = 'vercel' } if (partnerIds.has('workos')) { @@ -1069,6 +1079,12 @@ function detectDeployment(input: string) { if (/\brailway\b/i.test(input)) { return 'railway' as const } + if (/\brender\b/i.test(input)) { + return 'render' + } + if (/\b(vercel|v0)\b/i.test(input)) { + return 'vercel' + } if (/\bnitro\b/i.test(input)) { return 'nitro' as const } diff --git a/src/utils/partners.tsx b/src/utils/partners.tsx index aea6b1d72..77c8e7136 100644 --- a/src/utils/partners.tsx +++ b/src/utils/partners.tsx @@ -799,7 +799,7 @@ const lovable = ((): Partner => { applicationStarterPromptInstructions: [ 'Treat Lovable as the AI app-building and hosting path, not as a TanStack CLI deployment flag or npm package.', 'Keep the generated app portable: start with the TanStack CLI output, preserve GitHub/project ownership notes, and call out any Lovable Cloud setup that cannot be automated from code.', - 'When Lovable is selected, do not add a separate Cloudflare, Netlify, or Railway deployment target unless the user explicitly asks for a handoff path.', + 'When Lovable is selected, do not add a separate Cloudflare, Netlify, Railway, Render, or Vercel deployment target unless the user explicitly asks for a handoff path.', ], image: { light: lovableBlackSvg, @@ -1767,6 +1767,10 @@ const applicationStarterInferenceRules: Array<{ partnerId: 'railway', patterns: [/\brailway\b/i], }, + { + partnerId: 'vercel', + patterns: [/\b(vercel|v0)\b/i], + }, { partnerId: 'sentry', patterns: [/\bsentry\b/i], diff --git a/tests/application-starter-partners.test.ts b/tests/application-starter-partners.test.ts index 2f6790c90..02fd6844c 100644 --- a/tests/application-starter-partners.test.ts +++ b/tests/application-starter-partners.test.ts @@ -359,6 +359,36 @@ test('OpenRouter guidance prefers the TanStack AI adapter', async () => { assert.match(result.prompt, /@tanstack\/ai-openrouter/) }) +test('selected Vercel partner uses the Vercel deployment target', async () => { + const input = composeApplicationStarterInput( + 'Build a full-stack app.', + ['vercel'], + [], + ) + const result = await resolveApplicationStarterDeterministically({ + context: 'home', + input, + }) + + assert.equal(result.recipe.deployment, 'vercel') + assert.match(result.cliCommand, /--deployment vercel/) +}) + +test('selected Render partner uses the Render deployment target', async () => { + const input = composeApplicationStarterInput( + 'Build a full-stack app.', + ['render'], + [], + ) + const result = await resolveApplicationStarterDeterministically({ + context: 'home', + input, + }) + + assert.equal(result.recipe.deployment, 'render') + assert.match(result.cliCommand, /--deployment render/) +}) + test('Render uses per-placement UTM content for approved surfaces', () => { const renderPartner = partners.find((p) => p.id === 'render') assert.ok(renderPartner, 'Render partner should exist') diff --git a/tests/application-starter-prompt-deploy.test.ts b/tests/application-starter-prompt-deploy.test.ts new file mode 100644 index 000000000..f53a2381b --- /dev/null +++ b/tests/application-starter-prompt-deploy.test.ts @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { + buildStarterPromptDeployUrl, + getStarterPromptBuildLabel, +} from '../src/components/application-starter/prompt-deploy' + +test('prompt handoffs use build labels', () => { + assert.equal(getStarterPromptBuildLabel('lovable'), 'Build with Lovable') + assert.equal(getStarterPromptBuildLabel('netlify'), 'Build with Netlify') + assert.equal(getStarterPromptBuildLabel('v0'), 'Build with v0') +}) + +test('v0 handoff preserves the generated prompt', () => { + const prompt = 'Build a café dashboard\nwith revenue in €' + const url = new URL(buildStarterPromptDeployUrl('v0', prompt)) + + assert.equal(url.origin, 'https://v0.app') + assert.equal(url.pathname, '/') + assert.equal(url.searchParams.get('q'), prompt) + assert.equal(url.searchParams.get('utm_source'), 'tanstack') +}) + +test('Netlify handoff preserves the generated prompt', () => { + const prompt = 'Build a café dashboard\nwith revenue in €' + const url = new URL(buildStarterPromptDeployUrl('netlify', prompt)) + + assert.equal(url.origin, 'https://app.netlify.com') + assert.equal(url.pathname, '/start') + assert.equal(url.searchParams.get('prompt'), prompt) + assert.equal(url.searchParams.get('utm_source'), 'tanstack') +}) + +test('Lovable handoff preserves the generated prompt', () => { + const prompt = 'Build a café dashboard\nwith revenue in €' + const url = new URL(buildStarterPromptDeployUrl('lovable', prompt)) + const hash = new URLSearchParams(url.hash.slice(1)) + + assert.equal(url.origin, 'https://lovable.dev') + assert.equal(url.pathname, '/') + assert.equal(url.searchParams.get('autosubmit'), 'true') + assert.equal(url.searchParams.get('utm_source'), 'tanstack') + assert.equal(hash.get('prompt'), prompt) +}) diff --git a/tests/create-worker.test.ts b/tests/create-worker.test.ts index 60f565501..f455a065d 100644 --- a/tests/create-worker.test.ts +++ b/tests/create-worker.test.ts @@ -17,9 +17,6 @@ assert.deepEqual(workos.partner, { assert.deepEqual(workos.packageAdditions?.engines, { node: '>=22.11.0', }) -assert.deepEqual(sentry.packageAdditions?.pnpm, { - onlyBuiltDependencies: ['@sentry/cli'], -}) const [materializedWorkos] = await create.finalizeAddOns(react, 'file-router', [ 'workos', @@ -29,4 +26,57 @@ assert.deepEqual(materializedWorkos?.partner, { tier: 'silver', }) +const [materializedSentry] = await create.finalizeAddOns(react, 'file-router', [ + 'sentry', +]) +assert.match(materializedSentry?.packageTemplate ?? '', /addOnEnabled\.vercel/) +assert.match( + materializedSentry?.packageTemplate ?? '', + /"onlyBuiltDependencies"/, +) + +for (const frameworkId of ['react', 'solid']) { + const framework = await create.getFrameworkById(frameworkId) + if (!framework) throw new Error(`${frameworkId} framework not found`) + + const frameworkAddOns = create.getAllAddOns(framework, 'file-router') + const codeRouterAddOnIds = create + .getAllAddOns(framework, 'code-router') + .map((addOn) => addOn.id) + + for (const deployment of ['render', 'vercel']) { + const addOn = frameworkAddOns.find( + (candidate) => candidate.id === deployment, + ) + + if (!addOn) { + throw new Error(`${frameworkId} ${deployment} add-on not found`) + } + assert.deepEqual(addOn.partner, { + id: deployment, + tier: 'gold', + }) + assert.deepEqual(addOn.modes, ['file-router']) + assert.equal(codeRouterAddOnIds.includes(deployment), false) + + const [materializedDeployment] = await create.finalizeAddOns( + framework, + 'file-router', + [deployment], + ) + + if (deployment === 'render') { + assert.match( + materializedDeployment?.files['render.yaml.ejs'] ?? '', + /BUN_VERSION/, + ) + } else { + assert.match( + materializedDeployment?.files['vercel.json'] ?? '', + /"framework": "tanstack-start"/, + ) + } + } +} + console.log('create worker tests passed') diff --git a/tests/deploy-provider.test.ts b/tests/deploy-provider.test.ts new file mode 100644 index 000000000..ad25cf2b4 --- /dev/null +++ b/tests/deploy-provider.test.ts @@ -0,0 +1,17 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import { PROVIDER_INFO } from '../src/components/deploy/shared' + +test('Render deployment opens the repository as a Blueprint', () => { + const url = new URL(PROVIDER_INFO.render.deployUrl('tanstack', 'books')) + + assert.equal(url.origin, 'https://render.com') + assert.equal(url.pathname, '/deploy') + assert.equal( + url.searchParams.get('repo'), + 'https://github.com/tanstack/books', + ) + assert.equal(url.searchParams.get('utm_source'), 'tanstack') + assert.equal(url.searchParams.get('utm_medium'), 'referral') + assert.equal(url.searchParams.get('utm_campaign'), 'gold-launch') +}) From 0d0f2912503b1ff256636c33e3bbc3658874511a Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 31 Aug 2026 15:57:32 -0600 Subject: [PATCH 14/17] Update Vercel partner logos --- src/images/vercel-dark.svg | 4 +++- src/images/vercel-light.svg | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/images/vercel-dark.svg b/src/images/vercel-dark.svg index f8b16b6a9..9f67d8fec 100644 --- a/src/images/vercel-dark.svg +++ b/src/images/vercel-dark.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/src/images/vercel-light.svg b/src/images/vercel-light.svg index bf8d7259f..84c419ff6 100644 --- a/src/images/vercel-light.svg +++ b/src/images/vercel-light.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + From f874711d112b976fc72cd6c79bc990018c0f528d Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 31 Aug 2026 16:45:48 -0600 Subject: [PATCH 15/17] fix: infer Render deployment partner --- .agents/analytics.md | 2 +- src/utils/partners.tsx | 18 ++++++++------ tests/application-starter-partners.test.ts | 28 ++++++++++++++++++---- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/.agents/analytics.md b/.agents/analytics.md index dbcf3ab58..d5757b286 100644 --- a/.agents/analytics.md +++ b/.agents/analytics.md @@ -166,7 +166,7 @@ User took an action on the generated result. Single event with `action` prop cov | `idea_used` | string | Session context | | `action` | enum | See `BuilderAction` below | | `surface` | enum | `result_panel` (main builder UI) or `deploy_dialog` | -| `provider` | string? | Deploy provider when applicable: `vercel`, `netlify`, `cloudflare` | +| `provider` | string? | Build or deploy provider when applicable: `cloudflare`, `lovable`, `netlify`, `railway`, `render`, `vercel` | | `automatic` | boolean | `true` for system-driven actions (e.g., deploy_dialog auto-redirect countdown). Filter to `false` for true user click rates. | **Important:** automatic prompt-copies that fire as a side-effect of generation do NOT emit `builder_activated`. Only user-driven actions count as activation. diff --git a/src/utils/partners.tsx b/src/utils/partners.tsx index 77c8e7136..9dd542ce5 100644 --- a/src/utils/partners.tsx +++ b/src/utils/partners.tsx @@ -1163,11 +1163,11 @@ const vercel = ((): Partner => { href: '/start/latest/docs/framework/react/guide/hosting', }, ], - relatedProducts: ['start', 'router'] as const, - status: 'active' as const, + relatedProducts: ['start', 'router'], + status: 'active', lastReviewedAt: currentPartnerReviewDate, - tier: 'gold' as const, - uniqueConstraints: ['hosting'] satisfies Array, + tier: 'gold', + uniqueConstraints: ['hosting'], brandColor: '#000000', tagline: 'Agentic Infrastructure', applicationStarterIcon: { @@ -1431,10 +1431,10 @@ const render = ((): Partner => { name: 'Render', id: 'render', relatedProducts: ['start'], - status: 'active' as const, + status: 'active', lastReviewedAt: currentPartnerReviewDate, - tier: 'gold' as const, - uniqueConstraints: ['hosting'] satisfies Array, + tier: 'gold', + uniqueConstraints: ['hosting'], href, canonicalHref: 'https://render.com/', resources: [ @@ -1767,6 +1767,10 @@ const applicationStarterInferenceRules: Array<{ partnerId: 'railway', patterns: [/\brailway\b/i], }, + { + partnerId: 'render', + patterns: [/\brender\b/i], + }, { partnerId: 'vercel', patterns: [/\b(vercel|v0)\b/i], diff --git a/tests/application-starter-partners.test.ts b/tests/application-starter-partners.test.ts index 02fd6844c..fc719872e 100644 --- a/tests/application-starter-partners.test.ts +++ b/tests/application-starter-partners.test.ts @@ -2,6 +2,7 @@ import assert from 'node:assert/strict' import { existsSync } from 'node:fs' import { createRequire } from 'node:module' import { test } from 'node:test' +import type { PartnerPlacement } from '../src/utils/analytics' const require = createRequire(import.meta.url) const loadAsset: NodeJS.RequireExtensions[string] = (module, filename) => { @@ -389,16 +390,35 @@ test('selected Render partner uses the Render deployment target', async () => { assert.match(result.cliCommand, /--deployment render/) }) +test('hosting names infer their matching partner and deployment target', async () => { + for (const hosting of [ + { id: 'render', name: 'Render' }, + { id: 'vercel', name: 'Vercel' }, + ]) { + const input = `Build a full-stack app and deploy to ${hosting.name}.` + const inferredPartnerIds = + getInferredApplicationStarterPartnerIdsFromUserInput(input, []) + const result = await resolveApplicationStarterDeterministically({ + context: 'home', + input, + }) + + assert.ok(inferredPartnerIds.includes(hosting.id)) + assert.equal(result.recipe.deployment, hosting.id) + assert.match(result.cliCommand, new RegExp(`--deployment ${hosting.id}`)) + } +}) + test('Render uses per-placement UTM content for approved surfaces', () => { const renderPartner = partners.find((p) => p.id === 'render') assert.ok(renderPartner, 'Render partner should exist') - const placements = [ + const placements: PartnerPlacement[] = [ 'home_grid', 'library_grid', 'docs_rail', 'docs_strip', - ] as const + ] for (const placement of placements) { const href = getPartnerHref(renderPartner, placement) assert.match( @@ -427,13 +447,13 @@ test('other partners use their default href regardless of placement', () => { const vercel = partners.find((p) => p.id === 'vercel') assert.ok(vercel, 'Vercel partner should exist') - const placements = [ + const placements: PartnerPlacement[] = [ 'home_grid', 'library_grid', 'docs_rail', 'docs_strip', 'directory', - ] as const + ] for (const placement of placements) { const href = getPartnerHref(vercel, placement) assert.equal( From 36be7012dfad384d19df2405a0c34d33601c9c92 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 31 Aug 2026 16:48:34 -0600 Subject: [PATCH 16/17] fix: disambiguate Render deployment requests --- src/utils/application-starter.ts | 3 ++- src/utils/partners.tsx | 14 +++++++++++++- tests/application-starter-partners.test.ts | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/utils/application-starter.ts b/src/utils/application-starter.ts index 145cbfe86..c048fed77 100644 --- a/src/utils/application-starter.ts +++ b/src/utils/application-starter.ts @@ -8,6 +8,7 @@ import { getApplicationStarterSelectedPartnerIds, getApplicationStarterUserBrief, hasApplicationStarterPartnerConflictWithAny, + isRenderDeploymentRequest, } from '~/utils/partners' export type ApplicationStarterContext = @@ -1079,7 +1080,7 @@ function detectDeployment(input: string) { if (/\brailway\b/i.test(input)) { return 'railway' as const } - if (/\brender\b/i.test(input)) { + if (isRenderDeploymentRequest(input)) { return 'render' } if (/\b(vercel|v0)\b/i.test(input)) { diff --git a/src/utils/partners.tsx b/src/utils/partners.tsx index 9dd542ce5..2676966a5 100644 --- a/src/utils/partners.tsx +++ b/src/utils/partners.tsx @@ -1735,6 +1735,18 @@ export function hasApplicationStarterPartnerUniqueConstraint( return partner?.uniqueConstraints.includes(uniqueConstraint) ?? false } +const renderDeploymentPatterns = [ + /\brender\.com\b/i, + /\brender\s+blueprints?\b/i, + /\b(?:deploy|deploying|deployment|host|hosting)\b[^.!?\n]{0,40}\b(?:to|on|with|via)\s+render\b(?=\s*(?:$|[,.!?;:]|\b(?:hosting|cloud|platform)\b))/i, + /\brender\s+(?:hosting|deployment)\b/i, + /\buse\s+render\s+(?:for\s+)?(?:hosting|deployment)\b/i, +] + +export function isRenderDeploymentRequest(input: string) { + return renderDeploymentPatterns.some((pattern) => pattern.test(input)) +} + const applicationStarterInferenceRules: Array<{ partnerId: string patterns: Array @@ -1769,7 +1781,7 @@ const applicationStarterInferenceRules: Array<{ }, { partnerId: 'render', - patterns: [/\brender\b/i], + patterns: renderDeploymentPatterns, }, { partnerId: 'vercel', diff --git a/tests/application-starter-partners.test.ts b/tests/application-starter-partners.test.ts index fc719872e..e7b95e904 100644 --- a/tests/application-starter-partners.test.ts +++ b/tests/application-starter-partners.test.ts @@ -409,6 +409,25 @@ test('hosting names infer their matching partner and deployment target', async ( } }) +test('ordinary rendering language does not select Render hosting', async () => { + for (const input of [ + 'Render a chart with server data.', + 'Server render this page before hydration.', + 'Deploy a canvas app that uses WebGL to render charts.', + ]) { + const inferredPartnerIds = + getInferredApplicationStarterPartnerIdsFromUserInput(input, []) + const result = await resolveApplicationStarterDeterministically({ + context: 'home', + input, + }) + + assert.equal(inferredPartnerIds.includes('render'), false) + assert.notEqual(result.recipe.deployment, 'render') + assert.doesNotMatch(result.cliCommand, /--deployment render/) + } +}) + test('Render uses per-placement UTM content for approved surfaces', () => { const renderPartner = partners.find((p) => p.id === 'render') assert.ok(renderPartner, 'Render partner should exist') From 5eb86e19aba2de71488ea6d245b4761c0c13fe3f Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Mon, 31 Aug 2026 19:13:05 -0600 Subject: [PATCH 17/17] chore: update TanStack Create --- package.json | 2 +- pnpm-lock.yaml | 335 +++---------------------------------------------- 2 files changed, 18 insertions(+), 319 deletions(-) diff --git a/package.json b/package.json index 0a82141e4..cbbc3c244 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@tanstack/ai-client": "^0.28.0", "@tanstack/ai-openai": "^0.22.0", "@tanstack/charts": "0.16.0", - "@tanstack/create": "^0.69.0", + "@tanstack/create": "^0.70.0", "@tanstack/highlight": "^0.0.9", "@tanstack/markdown": "^0.0.11", "@tanstack/pacer": "^0.21.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd1680b5f..829a90857 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -112,8 +112,8 @@ importers: specifier: 0.16.0 version: 0.16.0(lit@3.3.2)(octane@0.1.13(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite@8.0.16(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0)(yaml@2.9.0)))(preact@10.29.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(solid-js@1.9.12) '@tanstack/create': - specifier: ^0.69.0 - version: 0.69.0(tslib@2.8.1) + specifier: ^0.70.0 + version: 0.70.0 '@tanstack/highlight': specifier: ^0.0.9 version: 0.0.9 @@ -1573,126 +1573,6 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@jsonjoy.com/base64@1.1.2': - resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/base64@17.67.0': - resolution: {integrity: sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/buffers@1.2.1': - resolution: {integrity: sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/buffers@17.67.0': - resolution: {integrity: sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/codegen@1.0.0': - resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/codegen@17.67.0': - resolution: {integrity: sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-core@4.57.1': - resolution: {integrity: sha512-YrEi/ZPmgc+GfdO0esBF04qv8boK9Dg9WpRQw/+vM8Qt3nnVIJWIa8HwZ/LXVZ0DB11XUROM8El/7yYTJX+WtA==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-fsa@4.57.1': - resolution: {integrity: sha512-ooEPvSW/HQDivPDPZMibHGKZf/QS4WRir1czGZmXmp3MsQqLECZEpN0JobrD8iV9BzsuwdIv+PxtWX9WpPLsIA==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-node-builtins@4.57.1': - resolution: {integrity: sha512-XHkFKQ5GSH3uxm8c3ZYXVrexGdscpWKIcMWKFQpMpMJc8gA3AwOMBJXJlgpdJqmrhPyQXxaY9nbkNeYpacC0Og==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-node-to-fsa@4.57.1': - resolution: {integrity: sha512-pqGHyWWzNck4jRfaGV39hkqpY5QjRUQ/nRbNT7FYbBa0xf4bDG+TE1Gt2KWZrSkrkZZDE3qZUjYMbjwSliX6pg==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-node-utils@4.57.1': - resolution: {integrity: sha512-vp+7ZzIB8v43G+GLXTS4oDUSQmhAsRz532QmmWBbdYA20s465JvwhkSFvX9cVTqRRAQg+vZ7zWDaIEh0lFe2gw==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-node@4.57.1': - resolution: {integrity: sha512-3YaKhP8gXEKN+2O49GLNfNb5l2gbnCFHyAaybbA2JkkbQP3dpdef7WcUaHAulg/c5Dg4VncHsA3NWAUSZMR5KQ==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-print@4.57.1': - resolution: {integrity: sha512-Ynct7ZJmfk6qoXDOKfpovNA36ITUx8rChLmRQtW08J73VOiuNsU8PB6d/Xs7fxJC2ohWR3a5AqyjmLojfrw5yw==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/fs-snapshot@4.57.1': - resolution: {integrity: sha512-/oG8xBNFMbDXTq9J7vepSA1kerS5vpgd3p5QZSPd+nX59uwodGJftI51gDYyHRpP57P3WCQf7LHtBYPqwUg2Bg==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/json-pack@1.21.0': - resolution: {integrity: sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/json-pack@17.67.0': - resolution: {integrity: sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/json-pointer@1.0.2': - resolution: {integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/json-pointer@17.67.0': - resolution: {integrity: sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/util@1.9.0': - resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - - '@jsonjoy.com/util@17.67.0': - resolution: {integrity: sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - '@kapaai/react-sdk@0.9.10': resolution: {integrity: sha512-osQyFgBJmhNM207MpB0aZbjs2kjNHjEJbNc5YEReJuyFE72Iydn9CoZGxox2GYRgMIlQ7wljHCzgQqkvR8lXqA==} peerDependencies: @@ -3527,8 +3407,8 @@ packages: vue: optional: true - '@tanstack/create@0.69.0': - resolution: {integrity: sha512-LVbzkTWiCK+JCKa/1Qwax0JddfJt4rxwekrDyHzhDWsSeNodJrTwbUg4eqqbQsxEWcrk0kHEJSfiTVzMnmI+kQ==} + '@tanstack/create@0.70.0': + resolution: {integrity: sha512-mkWcVbimcXJuo4eyqJiIcLO4LSLvcYS4EStTe259KDPLz0pqHVWVbnEbLFJy6MrxG0kBthWB956z7l0oZTshhw==} engines: {node: '>=20'} '@tanstack/db-ivm@0.1.19': @@ -5171,12 +5051,6 @@ packages: gifenc@1.0.3: resolution: {integrity: sha512-xdr6AdrfGBcfzncONUOlXMBuc5wJDtOueE3c5rdG0oNgtINLD+f2iFZltrBRZYzACRbKr+mSVU/x98zv2u3jmw==} - glob-to-regex.js@1.2.0: - resolution: {integrity: sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - glob@13.0.6: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} @@ -5294,10 +5168,6 @@ packages: engines: {node: '>=18'} hasBin: true - hyperdyperid@1.2.0: - resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} - engines: {node: '>=10.18'} - iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -5309,8 +5179,8 @@ packages: ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.7: + resolution: {integrity: sha512-dML0wP6oak21rsNYCJpJB6O1BJIEwNpGrTw0URPfAk4hm0e3pRfCtzkfB6olBcXcVlU2rouCyz7lCyRB0OMVCA==} engines: {node: '>= 4'} immediate@3.0.6: @@ -5797,11 +5667,6 @@ packages: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} - memfs@4.57.1: - resolution: {integrity: sha512-WvzrWPwMQT+PtbX2Et64R4qXKK0fj/8pO85MrUCzymX3twwCiJCdvntW3HdhG1teLJcHDDLIKx5+c3HckWYZtQ==} - peerDependencies: - tslib: '2' - memorystream@0.3.1: resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} engines: {node: '>= 0.10.0'} @@ -6198,6 +6063,11 @@ packages: engines: {node: '>=14'} hasBin: true + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -6702,12 +6572,6 @@ packages: text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} - thingies@2.6.0: - resolution: {integrity: sha512-rMHRjmlFLM1R96UYPvpmnc3LYtdFrT33JIB7L9hetGue1qAPfn1N2LJeEjxUSidu1Iku+haLZXDuEXUHNGO/lg==} - engines: {node: '>=10.18'} - peerDependencies: - tslib: ^2 - three-mesh-bvh@0.8.3: resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} peerDependencies: @@ -6755,12 +6619,6 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tree-dump@1.1.0: - resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} - engines: {node: '>=10.0'} - peerDependencies: - tslib: '2' - troika-three-text@0.52.4: resolution: {integrity: sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==} peerDependencies: @@ -8132,133 +7990,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/base64@17.67.0(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/buffers@1.2.1(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/buffers@17.67.0(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/codegen@1.0.0(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/codegen@17.67.0(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/fs-core@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/fs-node-builtins': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - thingies: 2.6.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/fs-fsa@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/fs-core': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - thingies: 2.6.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/fs-node-builtins@4.57.1(tslib@2.8.1)': - dependencies: - tslib: 2.8.1 - - '@jsonjoy.com/fs-node-to-fsa@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/fs-fsa': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/fs-node-utils@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/fs-node-builtins': 4.57.1(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/fs-node@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/fs-core': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-print': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-snapshot': 4.57.1(tslib@2.8.1) - glob-to-regex.js: 1.2.0(tslib@2.8.1) - thingies: 2.6.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/fs-print@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - tree-dump: 1.1.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/fs-snapshot@4.57.1(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/json-pack': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/json-pack@1.21.0(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) - '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) - '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) - '@jsonjoy.com/json-pointer': 1.0.2(tslib@2.8.1) - '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) - hyperdyperid: 1.2.0 - thingies: 2.6.0(tslib@2.8.1) - tree-dump: 1.1.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/json-pack@17.67.0(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/base64': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/json-pointer': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) - hyperdyperid: 1.2.0 - thingies: 2.6.0(tslib@2.8.1) - tree-dump: 1.1.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/json-pointer@1.0.2(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) - '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/json-pointer@17.67.0(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/util@1.9.0(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) - '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) - tslib: 2.8.1 - - '@jsonjoy.com/util@17.67.0(tslib@2.8.1)': - dependencies: - '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) - '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) - tslib: 2.8.1 - '@kapaai/react-sdk@0.9.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@fingerprintjs/fingerprintjs-pro-react': 2.7.1 @@ -9857,18 +9588,15 @@ snapshots: react-dom: 19.2.3(react@19.2.3) solid-js: 1.9.12 - '@tanstack/create@0.69.0(tslib@2.8.1)': + '@tanstack/create@0.70.0': dependencies: ejs: 3.1.10 execa: 9.6.1 - ignore: 7.0.5 - memfs: 4.57.1(tslib@2.8.1) + ignore: 7.0.7 parse-gitignore: 2.0.0 - prettier: 3.8.1 + prettier: 3.9.6 rimraf: 6.1.3 zod: 3.25.76 - transitivePeerDependencies: - - tslib '@tanstack/db-ivm@0.1.19(typescript@6.0.2)': dependencies: @@ -11782,10 +11510,6 @@ snapshots: gifenc@1.0.3: {} - glob-to-regex.js@1.2.0(tslib@2.8.1): - dependencies: - tslib: 2.8.1 - glob@13.0.6: dependencies: minimatch: 10.2.5 @@ -11890,8 +11614,6 @@ snapshots: husky@9.1.7: {} - hyperdyperid@1.2.0: {} - iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -11902,7 +11624,7 @@ snapshots: ieee754@1.2.1: {} - ignore@7.0.5: {} + ignore@7.0.7: {} immediate@3.0.6: {} @@ -12341,23 +12063,6 @@ snapshots: media-typer@1.1.0: {} - memfs@4.57.1(tslib@2.8.1): - dependencies: - '@jsonjoy.com/fs-core': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-fsa': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-builtins': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-to-fsa': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-node-utils': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-print': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/fs-snapshot': 4.57.1(tslib@2.8.1) - '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) - '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) - glob-to-regex.js: 1.2.0(tslib@2.8.1) - thingies: 2.6.0(tslib@2.8.1) - tree-dump: 1.1.0(tslib@2.8.1) - tslib: 2.8.1 - memorystream@0.3.1: {} merge-descriptors@2.0.0: {} @@ -12789,6 +12494,8 @@ snapshots: prettier@3.8.1: {} + prettier@3.9.6: {} + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -13425,10 +13132,6 @@ snapshots: transitivePeerDependencies: - react-native-b4a - thingies@2.6.0(tslib@2.8.1): - dependencies: - tslib: 2.8.1 - three-mesh-bvh@0.8.3(three@0.183.2): dependencies: three: 0.183.2 @@ -13474,10 +13177,6 @@ snapshots: tr46@0.0.3: {} - tree-dump@1.1.0(tslib@2.8.1): - dependencies: - tslib: 2.8.1 - troika-three-text@0.52.4(three@0.183.2): dependencies: bidi-js: 1.0.3