Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,317 changes: 667 additions & 650 deletions src/components/ApplicationStarter.tsx

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/components/ApplicationStarterHotkeys.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import * as React from 'react'
import { useHeldKeys, useHotkey } from '@tanstack/react-hotkeys'

interface ApplicationStarterHotkeysProps {
onAnalyze: () => void
onSubmit: () => void
onModKeyChange: (isHeld: boolean) => void
promptFocused: boolean
}

export function ApplicationStarterHotkeys({
onAnalyze,
onSubmit,
onModKeyChange,
promptFocused,
}: ApplicationStarterHotkeysProps) {
Expand All @@ -20,7 +20,7 @@ export function ApplicationStarterHotkeys({
return
}

onAnalyze()
onSubmit()
})

React.useEffect(() => {
Expand Down
55 changes: 48 additions & 7 deletions src/components/application-builder/parts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import { Check, Copy, Sparkles, X } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
import {
getApplicationStarterConflictingPartnerIds,
type ApplicationStarterPartnerSuggestion,
PartnerImage,
} from '~/utils/partners'
Expand Down Expand Up @@ -196,12 +197,14 @@ const StarterPartnerButton = React.forwardRef<
compact?: boolean
palette: StarterPalette
partner: ApplicationStarterPartnerSuggestion
muted: boolean
selected: boolean
size?: 'compact' | 'default'
} & React.ComponentPropsWithoutRef<'button'>
>(function StarterPartnerButton(
{
compact = false,
muted,
palette,
partner,
selected,
Expand Down Expand Up @@ -240,20 +243,24 @@ const StarterPartnerButton = React.forwardRef<
const tierOneTone = isTierOne
? selected
? 'translate-y-[-1px] border-transparent'
: 'border-gray-200 bg-white hover:border-[var(--starter-partner-hover-border-color)] dark:border-gray-800 dark:bg-gray-950 dark:hover:border-[var(--starter-partner-hover-border-color)]'
: 'border-gray-200 bg-white hover:border-[var(--starter-partner-hover-border-color)] active:border-[var(--starter-partner-active-border-color)] dark:border-gray-800 dark:bg-gray-950 dark:hover:border-[var(--starter-partner-hover-border-color)] dark:active:border-[var(--starter-partner-active-border-color)]'
: null
const tierThreeTone = isTierThree
? selected
? 'translate-y-[-1px] border-current bg-white shadow-[0_4px_12px_rgba(15,23,42,0.08)] dark:bg-gray-950'
: 'border-gray-200 bg-white text-gray-700 hover:border-[var(--starter-partner-hover-border-color)] hover:text-current dark:border-gray-800 dark:bg-gray-950 dark:text-gray-200 dark:hover:border-[var(--starter-partner-hover-border-color)]'
: 'border-gray-200 bg-white text-gray-700 hover:border-[var(--starter-partner-hover-border-color)] hover:text-current active:border-[var(--starter-partner-active-border-color)] dark:border-gray-800 dark:bg-gray-950 dark:text-gray-200 dark:hover:border-[var(--starter-partner-hover-border-color)] dark:active:border-[var(--starter-partner-active-border-color)]'
: null
const hoverBorderColor = colorWithAlpha(accent, 0.5) ?? accent
const style: StarterPartnerButtonStyle = {
'--starter-partner-border-hover': usesPaletteSurface ? accent : undefined,
'--starter-partner-hover-border-color': accent,
'--starter-partner-active-border-color': accent,
'--starter-partner-border-hover': usesPaletteSurface
? hoverBorderColor
: undefined,
'--starter-partner-hover-border-color': hoverBorderColor,
backgroundColor: undefined,
borderColor:
isTierOne && selected
? colorWithAlpha(accent, 0.92)
? accent
: selected && usesPaletteSurface
? accent
: undefined,
Expand All @@ -273,7 +280,11 @@ const StarterPartnerButton = React.forwardRef<
ref={ref}
type="button"
aria-pressed={selected}
aria-label={accessibleLabel}
aria-label={
muted
? `${accessibleLabel}, inactive while another exclusive partner is selected`
: accessibleLabel
}
className={twMerge(
'inline-flex items-center text-gray-800 transition-all duration-200 dark:text-gray-100',
'border-2',
Expand All @@ -283,8 +294,10 @@ const StarterPartnerButton = React.forwardRef<
tierOneTone,
usesPaletteSurface && palette.chip,
usesPaletteSurface &&
'hover:border-[var(--starter-partner-border-hover)]',
'hover:border-[var(--starter-partner-border-hover)] active:border-[var(--starter-partner-active-border-color)]',
tierThreeTone,
muted &&
'border-transparent opacity-60 saturate-50 grayscale hover:border-transparent hover:text-gray-700 active:border-transparent dark:border-transparent dark:hover:border-transparent dark:hover:text-gray-200 dark:active:border-transparent',
buttonProps.className,
)}
style={style}
Expand Down Expand Up @@ -353,6 +366,32 @@ export function StarterPartnerRows({
selected: boolean,
) => void
}) {
const mutedPartnerIds = React.useMemo(() => {
const partnerIds = new Set<string>()

for (const selectedPartnerId of selectedPartners) {
const selectedPartner = partnerSuggestions.find(
(partner) => partner.id === selectedPartnerId,
)

if (!selectedPartner) {
continue
}

for (const partnerId of getApplicationStarterConflictingPartnerIds(
selectedPartner,
partnerSuggestions,
)) {
partnerIds.add(partnerId)
}
}

for (const selectedPartnerId of selectedPartners) {
partnerIds.delete(selectedPartnerId)
}

return partnerIds
}, [partnerSuggestions, selectedPartners])
const rows = ([1, 2, 3] as const)
.map((tier) => ({
tier,
Expand All @@ -366,6 +405,7 @@ export function StarterPartnerRows({
<div key={row.tier} className="flex flex-wrap gap-1.5">
{row.partners.map((partner) => {
const selected = selectedPartners.includes(partner.id)
const muted = mutedPartnerIds.has(partner.id)

return (
<StarterHoverTooltip
Expand All @@ -375,6 +415,7 @@ export function StarterPartnerRows({
<StarterPartnerButton
compact={compact}
onClick={() => togglePartner(partner, selected)}
muted={muted}
palette={palette}
partner={partner}
selected={selected}
Expand Down
32 changes: 31 additions & 1 deletion src/components/application-builder/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {

export type StarterTone = 'cyan' | 'emerald' | 'violet'
export type StarterDeployProvider = 'cloudflare' | 'netlify' | 'railway'
export type StarterPromptDeployProvider = 'lovable' | 'netlify'
export type StarterPackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
export type StarterToolchain = 'biome' | 'eslint'

Expand All @@ -21,7 +22,10 @@ export interface StarterPalette {
}

export interface ApplicationStarterBuilderIntegration {
applyResult: (result: ApplicationStarterResult) => Promise<boolean>
applyResult: (
result: ApplicationStarterResult,
options?: { silent?: boolean },
) => Promise<boolean>
}

export interface ApplicationStarterAnonymousQuota {
Expand All @@ -45,6 +49,7 @@ export interface StarterTryLibrary {
}

export type StarterPartnerButtonStyle = CSSProperties & {
'--starter-partner-active-border-color'?: string
'--starter-partner-border-hover'?: string
'--starter-partner-hover-border-color'?: string
}
Expand Down Expand Up @@ -138,6 +143,31 @@ 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,
Expand Down
Loading
Loading