Skip to content

Commit e62ca0d

Browse files
committed
Refine builder partner prompt flow
1 parent d82a4df commit e62ca0d

19 files changed

Lines changed: 1434 additions & 1948 deletions

src/components/ApplicationStarter.tsx

Lines changed: 508 additions & 636 deletions
Large diffs are not rendered by default.

src/components/ApplicationStarterHotkeys.client.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import * as React from 'react'
22
import { useHeldKeys, useHotkey } from '@tanstack/react-hotkeys'
33

44
interface ApplicationStarterHotkeysProps {
5-
onAnalyze: () => void
5+
onSubmit: () => void
66
onModKeyChange: (isHeld: boolean) => void
77
promptFocused: boolean
88
}
99

1010
export function ApplicationStarterHotkeys({
11-
onAnalyze,
11+
onSubmit,
1212
onModKeyChange,
1313
promptFocused,
1414
}: ApplicationStarterHotkeysProps) {
@@ -20,7 +20,7 @@ export function ApplicationStarterHotkeys({
2020
return
2121
}
2222

23-
onAnalyze()
23+
onSubmit()
2424
})
2525

2626
React.useEffect(() => {

src/components/application-builder/parts.tsx

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as TooltipPrimitive from '@radix-ui/react-tooltip'
33
import { Check, Copy, Sparkles, X } from 'lucide-react'
44
import { twMerge } from 'tailwind-merge'
55
import {
6+
getApplicationStarterConflictingPartnerIds,
67
type ApplicationStarterPartnerSuggestion,
78
PartnerImage,
89
} from '~/utils/partners'
@@ -196,12 +197,14 @@ const StarterPartnerButton = React.forwardRef<
196197
compact?: boolean
197198
palette: StarterPalette
198199
partner: ApplicationStarterPartnerSuggestion
200+
muted: boolean
199201
selected: boolean
200202
size?: 'compact' | 'default'
201203
} & React.ComponentPropsWithoutRef<'button'>
202204
>(function StarterPartnerButton(
203205
{
204206
compact = false,
207+
muted,
205208
palette,
206209
partner,
207210
selected,
@@ -240,20 +243,24 @@ const StarterPartnerButton = React.forwardRef<
240243
const tierOneTone = isTierOne
241244
? selected
242245
? 'translate-y-[-1px] border-transparent'
243-
: '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)]'
246+
: '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)]'
244247
: null
245248
const tierThreeTone = isTierThree
246249
? selected
247250
? 'translate-y-[-1px] border-current bg-white shadow-[0_4px_12px_rgba(15,23,42,0.08)] dark:bg-gray-950'
248-
: '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)]'
251+
: '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)]'
249252
: null
253+
const hoverBorderColor = colorWithAlpha(accent, 0.5) ?? accent
250254
const style: StarterPartnerButtonStyle = {
251-
'--starter-partner-border-hover': usesPaletteSurface ? accent : undefined,
252-
'--starter-partner-hover-border-color': accent,
255+
'--starter-partner-active-border-color': accent,
256+
'--starter-partner-border-hover': usesPaletteSurface
257+
? hoverBorderColor
258+
: undefined,
259+
'--starter-partner-hover-border-color': hoverBorderColor,
253260
backgroundColor: undefined,
254261
borderColor:
255262
isTierOne && selected
256-
? colorWithAlpha(accent, 0.92)
263+
? accent
257264
: selected && usesPaletteSurface
258265
? accent
259266
: undefined,
@@ -273,7 +280,11 @@ const StarterPartnerButton = React.forwardRef<
273280
ref={ref}
274281
type="button"
275282
aria-pressed={selected}
276-
aria-label={accessibleLabel}
283+
aria-label={
284+
muted
285+
? `${accessibleLabel}, inactive while another exclusive partner is selected`
286+
: accessibleLabel
287+
}
277288
className={twMerge(
278289
'inline-flex items-center text-gray-800 transition-all duration-200 dark:text-gray-100',
279290
'border-2',
@@ -283,8 +294,10 @@ const StarterPartnerButton = React.forwardRef<
283294
tierOneTone,
284295
usesPaletteSurface && palette.chip,
285296
usesPaletteSurface &&
286-
'hover:border-[var(--starter-partner-border-hover)]',
297+
'hover:border-[var(--starter-partner-border-hover)] active:border-[var(--starter-partner-active-border-color)]',
287298
tierThreeTone,
299+
muted &&
300+
'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',
288301
buttonProps.className,
289302
)}
290303
style={style}
@@ -353,6 +366,32 @@ export function StarterPartnerRows({
353366
selected: boolean,
354367
) => void
355368
}) {
369+
const mutedPartnerIds = React.useMemo(() => {
370+
const partnerIds = new Set<string>()
371+
372+
for (const selectedPartnerId of selectedPartners) {
373+
const selectedPartner = partnerSuggestions.find(
374+
(partner) => partner.id === selectedPartnerId,
375+
)
376+
377+
if (!selectedPartner) {
378+
continue
379+
}
380+
381+
for (const partnerId of getApplicationStarterConflictingPartnerIds(
382+
selectedPartner,
383+
partnerSuggestions,
384+
)) {
385+
partnerIds.add(partnerId)
386+
}
387+
}
388+
389+
for (const selectedPartnerId of selectedPartners) {
390+
partnerIds.delete(selectedPartnerId)
391+
}
392+
393+
return partnerIds
394+
}, [partnerSuggestions, selectedPartners])
356395
const rows = ([1, 2, 3] as const)
357396
.map((tier) => ({
358397
tier,
@@ -366,6 +405,7 @@ export function StarterPartnerRows({
366405
<div key={row.tier} className="flex flex-wrap gap-1.5">
367406
{row.partners.map((partner) => {
368407
const selected = selectedPartners.includes(partner.id)
408+
const muted = mutedPartnerIds.has(partner.id)
369409

370410
return (
371411
<StarterHoverTooltip
@@ -375,6 +415,7 @@ export function StarterPartnerRows({
375415
<StarterPartnerButton
376416
compact={compact}
377417
onClick={() => togglePartner(partner, selected)}
418+
muted={muted}
378419
palette={palette}
379420
partner={partner}
380421
selected={selected}

src/components/application-builder/shared.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010

1111
export type StarterTone = 'cyan' | 'emerald' | 'violet'
1212
export type StarterDeployProvider = 'cloudflare' | 'netlify' | 'railway'
13+
export type StarterPromptDeployProvider = 'lovable' | 'netlify'
1314
export type StarterPackageManager = 'bun' | 'npm' | 'pnpm' | 'yarn'
1415
export type StarterToolchain = 'biome' | 'eslint'
1516

@@ -21,7 +22,10 @@ export interface StarterPalette {
2122
}
2223

2324
export interface ApplicationStarterBuilderIntegration {
24-
applyResult: (result: ApplicationStarterResult) => Promise<boolean>
25+
applyResult: (
26+
result: ApplicationStarterResult,
27+
options?: { silent?: boolean },
28+
) => Promise<boolean>
2529
}
2630

2731
export interface ApplicationStarterAnonymousQuota {
@@ -45,6 +49,7 @@ export interface StarterTryLibrary {
4549
}
4650

4751
export type StarterPartnerButtonStyle = CSSProperties & {
52+
'--starter-partner-active-border-color'?: string
4853
'--starter-partner-border-hover'?: string
4954
'--starter-partner-hover-border-color'?: string
5055
}
@@ -138,6 +143,31 @@ export const starterLoadingPhrases = [
138143
'Finding calmer waters...',
139144
]
140145

146+
export function buildStarterPromptDeployUrl(
147+
provider: StarterPromptDeployProvider,
148+
prompt: string,
149+
) {
150+
switch (provider) {
151+
case 'lovable': {
152+
const url = new URL('https://lovable.dev/')
153+
154+
url.searchParams.set('autosubmit', 'true')
155+
url.searchParams.set('utm_source', 'tanstack')
156+
url.hash = `prompt=${encodeURIComponent(prompt)}`
157+
158+
return url.toString()
159+
}
160+
case 'netlify': {
161+
const url = new URL('https://app.netlify.com/start')
162+
163+
url.searchParams.set('prompt', prompt)
164+
url.searchParams.set('utm_source', 'tanstack')
165+
166+
return url.toString()
167+
}
168+
}
169+
}
170+
141171
export function isPinnedStarterLibrary(libraryId: LibraryId) {
142172
return starterPinnedLibraryIds.some(
143173
(pinnedLibraryId) => pinnedLibraryId === libraryId,

0 commit comments

Comments
 (0)