Skip to content

Commit 8861a31

Browse files
committed
builder enhancements
1 parent 37d96f5 commit 8861a31

File tree

3 files changed

+115
-51
lines changed

3 files changed

+115
-51
lines changed

opencode.json

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,6 @@
1010
"type": "remote",
1111
"url": "https://mcp.sentry.dev/mcp",
1212
"enabled": false
13-
},
14-
"tanstack": {
15-
"type": "remote",
16-
"url": "https://tanstack.com/api/mcp",
17-
"enabled": true
18-
},
19-
"tanstack-dev": {
20-
"type": "remote",
21-
"url": "http://localhost:3000/api/mcp",
22-
"enabled": false
2313
}
2414
}
2515
}

src/components/builder/ConfigPanel.tsx

Lines changed: 88 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export function ConfigPanel() {
7474
const features = useFeatures()
7575
const [dropdownOpen, setDropdownOpen] = useState(false)
7676
const [deployDialogOpen, setDeployDialogOpen] = useState(false)
77+
const [cliCopied, setCliCopied] = useState(false)
78+
const cliCommand = useCliCommand()
7779
const [deployDialogProvider, setDeployDialogProvider] = useState<
7880
'cloudflare' | 'netlify' | 'railway' | null
7981
>(null)
@@ -94,31 +96,56 @@ export function ConfigPanel() {
9496
<div className="h-full flex flex-col bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800">
9597
{/* Header - Project Name + Build Button */}
9698
<div className="shrink-0 p-4 border-b border-gray-200 dark:border-gray-800">
97-
<div className="flex items-center gap-2">
99+
<div className="flex flex-wrap items-center gap-2">
98100
<input
99101
type="text"
100102
value={projectName}
101103
onChange={(e) => setProjectName(e.target.value)}
102-
className="min-w-0 flex-1 h-8 bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md px-2 text-gray-900 dark:text-white font-mono text-xs focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-cyan-500 focus:border-transparent"
104+
className="min-w-[140px] flex-1 h-8 bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md px-2 text-gray-900 dark:text-white font-mono text-xs focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-cyan-500 focus:border-transparent"
103105
placeholder="my-tanstack-app"
104106
/>
105-
<BuildProjectDropdown
106-
onOpenChange={setDropdownOpen}
107-
onCreateRepo={() => openDeployDialog(null)}
108-
/>
109-
{deployProvider && (
110-
<button
111-
onClick={() => openDeployDialog(deployProvider.provider)}
112-
className="shrink-0 h-8 px-2.5 text-xs text-white rounded-md transition-opacity hover:opacity-90 whitespace-nowrap flex items-center gap-1"
113-
style={{ backgroundColor: deployProvider.color }}
114-
>
115-
<Rocket className="w-3.5 h-3.5" />
116-
<span className="font-medium">Deploy</span>
117-
<span className="opacity-80 text-[10px]">
118-
to {deployProvider.name}
119-
</span>
120-
</button>
121-
)}
107+
<div className="flex flex-wrap items-center gap-2">
108+
<div className="relative">
109+
<button
110+
onClick={async () => {
111+
await navigator.clipboard.writeText(cliCommand)
112+
setCliCopied(true)
113+
setTimeout(() => setCliCopied(false), 2000)
114+
}}
115+
className="shrink-0 h-8 px-2 flex items-center justify-center gap-1.5 rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-400 hover:border-gray-400 dark:hover:border-gray-600 hover:text-gray-900 dark:hover:text-white transition-colors text-xs font-medium"
116+
title="Copy CLI command"
117+
>
118+
{cliCopied ? (
119+
<Check className="w-3.5 h-3.5 text-green-500" />
120+
) : (
121+
<Copy className="w-3.5 h-3.5" />
122+
)}
123+
CLI
124+
</button>
125+
{cliCopied && (
126+
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-1 px-2 py-1 text-xs font-medium text-white bg-gray-900 dark:bg-white dark:text-gray-900 rounded shadow-lg whitespace-nowrap z-10">
127+
Copied!
128+
</div>
129+
)}
130+
</div>
131+
<BuildProjectDropdown
132+
onOpenChange={setDropdownOpen}
133+
onCreateRepo={() => openDeployDialog(null)}
134+
/>
135+
{deployProvider && (
136+
<button
137+
onClick={() => openDeployDialog(deployProvider.provider)}
138+
className="shrink-0 h-8 px-2.5 text-xs text-white rounded-md transition-opacity hover:opacity-90 whitespace-nowrap flex items-center gap-1"
139+
style={{ backgroundColor: deployProvider.color }}
140+
>
141+
<Rocket className="w-3.5 h-3.5" />
142+
<span className="font-medium">Deploy</span>
143+
<span className="opacity-80 text-[10px]">
144+
to {deployProvider.name}
145+
</span>
146+
</button>
147+
)}
148+
</div>
122149
</div>
123150
</div>
124151

@@ -150,6 +177,11 @@ export function ConfigPanel() {
150177
<StylesPicker />
151178
</div>
152179

180+
{/* Package Manager */}
181+
<div className="p-4 pb-0">
182+
<PackageManagerPicker />
183+
</div>
184+
153185
{/* Feature Picker */}
154186
<FeaturePicker />
155187

@@ -184,32 +216,14 @@ function IntegrationSearch() {
184216
)
185217
}
186218

187-
const PACKAGE_MANAGERS = ['pnpm', 'npm', 'yarn', 'bun'] as const
188-
189219
function CliOptionsInline() {
190-
const packageManager = useBuilderStore((s) => s.packageManager)
191-
const setPackageManager = useBuilderStore((s) => s.setPackageManager)
192220
const skipInstall = useBuilderStore((s) => s.skipInstall)
193221
const setSkipInstall = useBuilderStore((s) => s.setSkipInstall)
194222
const skipGit = useBuilderStore((s) => s.skipGit)
195223
const setSkipGit = useBuilderStore((s) => s.setSkipGit)
196224

197225
return (
198226
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-gray-600 dark:text-gray-400">
199-
<select
200-
value={packageManager}
201-
onChange={(e) =>
202-
setPackageManager(e.target.value as (typeof PACKAGE_MANAGERS)[number])
203-
}
204-
className="bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:ring-cyan-500"
205-
>
206-
{PACKAGE_MANAGERS.map((pm) => (
207-
<option key={pm} value={pm}>
208-
{pm}
209-
</option>
210-
))}
211-
</select>
212-
213227
<label className="flex items-center gap-1.5 cursor-pointer">
214228
<input
215229
type="checkbox"
@@ -368,6 +382,43 @@ function StylesPicker() {
368382
)
369383
}
370384

385+
const PACKAGE_MANAGERS = ['pnpm', 'npm', 'yarn', 'bun'] as const
386+
387+
function PackageManagerPicker() {
388+
const packageManager = useBuilderStore((s) => s.packageManager)
389+
const setPackageManager = useBuilderStore((s) => s.setPackageManager)
390+
391+
return (
392+
<div>
393+
<h3 className="text-base font-semibold text-gray-700 dark:text-gray-300 mb-1">
394+
Package Manager
395+
</h3>
396+
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
397+
CLI tool for dependencies
398+
</p>
399+
<div className="flex gap-1">
400+
{PACKAGE_MANAGERS.map((pm) => {
401+
const isSelected = packageManager === pm
402+
return (
403+
<button
404+
key={pm}
405+
onClick={() => setPackageManager(pm)}
406+
className={twMerge(
407+
'flex-1 px-2 py-1.5 text-xs font-medium rounded-md border-2 transition-all',
408+
isSelected
409+
? 'bg-blue-50 dark:bg-cyan-950 border-blue-500 dark:border-cyan-500 text-blue-700 dark:text-cyan-300'
410+
: 'bg-white dark:bg-gray-800/50 border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:border-gray-300 dark:hover:border-gray-600',
411+
)}
412+
>
413+
{pm}
414+
</button>
415+
)
416+
})}
417+
</div>
418+
</div>
419+
)
420+
}
421+
371422
function SelectedFeatureOptions({ features }: { features: Array<string> }) {
372423
const availableFeatures = useBuilderStore((s) => s.availableFeatures)
373424

src/components/builder/useBuilderUrl.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface BuilderSearchParams {
1616
framework?: string // react-cra, solid-cra, etc.
1717
template?: string // template preset ID
1818
features?: string // comma-separated feature IDs
19+
pm?: string // package manager: pnpm, npm, yarn, bun
1920
// Feature options serialized as: featureId.optionKey=value
2021
[key: string]: string | undefined
2122
}
@@ -30,11 +31,13 @@ export function useBuilderUrl() {
3031
const features = useBuilderStore((s) => s.features)
3132
const featureOptions = useBuilderStore((s) => s.featureOptions)
3233
const selectedTemplate = useBuilderStore((s) => s.selectedTemplate)
34+
const packageManager = useBuilderStore((s) => s.packageManager)
3335
const featuresLoaded = useBuilderStore((s) => s.featuresLoaded)
3436
const setProjectName = useBuilderStore((s) => s.setProjectName)
3537
const setFeatures = useBuilderStore((s) => s.setFeatures)
3638
const setFeatureOption = useBuilderStore((s) => s.setFeatureOption)
3739
const setTemplate = useBuilderStore((s) => s.setTemplate)
40+
const setPackageManager = useBuilderStore((s) => s.setPackageManager)
3841

3942
// Initialize from URL on mount (only once when features load)
4043
const initializedRef = useRef(false)
@@ -49,6 +52,11 @@ export function useBuilderUrl() {
4952
setProjectName(search.name)
5053
}
5154

55+
// Set package manager
56+
if (search.pm && ['pnpm', 'npm', 'yarn', 'bun'].includes(search.pm)) {
57+
setPackageManager(search.pm as 'pnpm' | 'npm' | 'yarn' | 'bun')
58+
}
59+
5260
// Note: framework is set in BuilderProvider before features load
5361

5462
// Apply template if specified (this sets features)
@@ -86,6 +94,7 @@ export function useBuilderUrl() {
8694
feats: Array<FeatureId>,
8795
opts: Record<string, Record<string, unknown>>,
8896
template: string | null,
97+
pm: string,
8998
) => {
9099
navigate({
91100
to: '/builder',
@@ -113,6 +122,13 @@ export function useBuilderUrl() {
113122
delete params.template
114123
}
115124

125+
// Update package manager (skip if default pnpm)
126+
if (pm && pm !== 'pnpm') {
127+
params.pm = pm
128+
} else {
129+
delete params.pm
130+
}
131+
116132
// Update features
117133
if (feats.length > 0) {
118134
params.features = feats.join(',')
@@ -153,13 +169,15 @@ export function useBuilderUrl() {
153169
features,
154170
featureOptions,
155171
selectedTemplate,
172+
packageManager,
156173
)
157174
}, [
158175
projectName,
159176
framework,
160177
features,
161178
featureOptions,
162179
selectedTemplate,
180+
packageManager,
163181
featuresLoaded,
164182
syncToUrl,
165183
])
@@ -170,6 +188,7 @@ export function useBuilderUrl() {
170188
*/
171189
export function useCliCommand(): string {
172190
const projectName = useBuilderStore((s) => s.projectName)
191+
const framework = useBuilderStore((s) => s.framework)
173192
const features = useBuilderStore((s) => s.features)
174193
const _featureOptions = useBuilderStore((s) => s.featureOptions) // TODO: Add to CLI command
175194
const tailwind = useBuilderStore((s) => s.tailwind)
@@ -179,19 +198,23 @@ export function useCliCommand(): string {
179198

180199
let cmd = `npx @tanstack/cli@latest create ${projectName}`
181200

182-
// Always add -y to skip prompts
183-
cmd += ' -y'
201+
// Map internal framework ID to CLI framework name
202+
if (framework === 'solid') {
203+
cmd += ' --framework Solid'
204+
}
184205

185206
if (packageManager !== 'pnpm') {
186207
cmd += ` --package-manager ${packageManager}`
187208
}
188209

189-
if (!tailwind) {
210+
if (tailwind) {
211+
cmd += ' --tailwind'
212+
} else {
190213
cmd += ' --no-tailwind'
191214
}
192215

193216
if (features.length > 0) {
194-
cmd += ` --integrations ${features.join(',')}`
217+
cmd += ` --add-ons ${features.join(',')}`
195218
}
196219

197220
if (skipInstall) {

0 commit comments

Comments
 (0)