Skip to content

Commit f1f2791

Browse files
committed
builder alpha
1 parent 5323004 commit f1f2791

29 files changed

+1178
-551
lines changed

.github/workflows/pr.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
fetch-depth: 0
1515
- name: Setup Tools
1616
uses: tanstack/config/.github/setup@main
17-
- name: Run Lint
18-
run: pnpm lint
1917
- name: Run Build
2018
run: pnpm build
19+
- name: Run Tests
20+
run: pnpm test

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
"test:tsc": "tsc",
2424
"test:lint": "pnpm run lint",
2525
"test:smoke": "tsx tests/smoke.ts",
26-
"test:smoke:ci": "tsx tests/smoke.ts --server",
2726
"test:e2e": "playwright test",
2827
"prepare": "husky"
2928
},

playwright.config.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { defineConfig } from '@playwright/test'
22

3+
const port = process.env.PORT || '3000'
4+
const baseURL = `http://localhost:${port}`
5+
36
export default defineConfig({
47
testDir: './tests',
58
testMatch: '**/*.spec.ts',
@@ -10,14 +13,14 @@ export default defineConfig({
1013
reporter: 'list',
1114
timeout: 30000,
1215
use: {
13-
baseURL: 'http://localhost:3000',
16+
baseURL,
1417
trace: 'off',
1518
video: 'off',
1619
screenshot: 'off',
1720
},
1821
webServer: {
19-
command: 'pnpm dev',
20-
url: 'http://localhost:3000',
22+
command: `PORT=${port} pnpm dev`,
23+
url: baseURL,
2124
reuseExistingServer: !process.env.CI,
2225
timeout: 120000,
2326
},

src/components/Navbar.tsx

Lines changed: 44 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
useMatches,
1010
useNavigate,
1111
} from '@tanstack/react-router'
12+
import { NetlifyImage } from './NetlifyImage'
1213
import {
1314
Code,
1415
Users,
@@ -117,9 +118,10 @@ const LogoSection = ({
117118
className={twMerge(`inline-flex items-center gap-1.5 cursor-pointer`)}
118119
>
119120
<div className="w-[30px] inline-grid items-center grid-cols-1 grid-rows-1 [&>*]:transition-opacity [&>*]:duration-1000">
120-
<img
121-
src={'/images/logos/logo-color-100.png'}
121+
<NetlifyImage
122+
src="/images/logos/logo-color-100.png"
122123
alt=""
124+
width={30}
123125
className="row-start-1 col-start-1 w-full group-hover:opacity-0"
124126
/>
125127
<img
@@ -554,48 +556,46 @@ export function Navbar({ children }: { children: React.ReactNode }) {
554556
<div className="bg-gray-500/10 h-px" />
555557
</div>
556558
<div className="contents md:block">
557-
<Authenticated>
558-
{/* Mobile: Builder card */}
559-
{capabilities.some((capability: string) =>
560-
(['builder', 'admin'] as const).includes(
561-
capability as 'builder' | 'admin',
562-
),
563-
) ? (
564-
<>
565-
<MobileCard>
566-
<Link
567-
to="/builder"
568-
className={twMerge(linkClasses, 'font-normal md:hidden')}
569-
activeProps={{
570-
className: twMerge(
571-
'font-bold! bg-gray-500/10 dark:bg-gray-500/30',
572-
),
573-
}}
574-
>
575-
<div className="flex items-center gap-2">
576-
<Hammer className="w-5 h-5" />
577-
<div>Builder</div>
578-
</div>
579-
</Link>
580-
</MobileCard>
581-
{/* Desktop: Builder link */}
582-
<Link
583-
to="/builder"
584-
className={twMerge(linkClasses, 'font-normal hidden md:flex')}
585-
activeProps={{
586-
className: twMerge(
587-
'font-bold! bg-gray-500/10 dark:bg-gray-500/30',
588-
),
589-
}}
590-
>
591-
<div className="flex items-center gap-2">
592-
<Hammer className="w-4 h-4" />
593-
<div>Builder</div>
594-
</div>
595-
</Link>
596-
</>
597-
) : null}
598-
</Authenticated>
559+
{/* Mobile: Builder card */}
560+
<MobileCard>
561+
<Link
562+
to="/builder"
563+
className={twMerge(linkClasses, 'font-normal md:hidden')}
564+
activeProps={{
565+
className: twMerge(
566+
'font-bold! bg-gray-500/10 dark:bg-gray-500/30',
567+
),
568+
}}
569+
>
570+
<div className="flex items-center gap-2 w-full">
571+
<Hammer className="w-5 h-5" />
572+
<div className="flex items-center justify-between flex-1 gap-2">
573+
<span>Builder</span>
574+
<span className="px-1.5 py-0.5 text-[.6rem] font-black border border-amber-500 text-amber-500 rounded-md uppercase">
575+
Alpha
576+
</span>
577+
</div>
578+
</div>
579+
</Link>
580+
</MobileCard>
581+
{/* Desktop: Builder link */}
582+
<Link
583+
to="/builder"
584+
className={twMerge(linkClasses, 'font-normal hidden md:flex')}
585+
activeProps={{
586+
className: twMerge('font-bold! bg-gray-500/10 dark:bg-gray-500/30'),
587+
}}
588+
>
589+
<div className="flex items-center gap-2 w-full">
590+
<Hammer className="w-4 h-4" />
591+
<div className="flex items-center justify-between flex-1 gap-2">
592+
<span>Builder</span>
593+
<span className="px-1.5 py-0.5 text-[.6rem] font-black border border-amber-500 text-amber-500 rounded-md uppercase">
594+
Alpha
595+
</span>
596+
</div>
597+
</div>
598+
</Link>
599599
{[
600600
{
601601
label: (

src/components/builder/BuilderProvider.tsx

Lines changed: 86 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* BuilderProvider wraps CTA providers and handles initialization
2+
* BuilderProvider - Wraps CTA providers and handles initialization
33
*/
44

55
import * as React from 'react'
@@ -14,22 +14,54 @@ import {
1414
useBuilderSearch,
1515
useInitializeAddonsFromUrl,
1616
} from './hooks/useBuilderSearch'
17+
import { useCapabilities } from '~/hooks/useCapabilities'
18+
import { hasCapability } from '~/db/types'
1719

1820
type BuilderProviderProps = {
1921
children: React.ReactNode
2022
}
2123

22-
function BuilderProviderInner({ children }: BuilderProviderProps) {
23-
// Initialize manager to start CTA engine
24-
useManager()
24+
// Context for lazy preview activation
25+
type PreviewContextValue = {
26+
canPreview: boolean
27+
previewActivated: boolean
28+
activatePreview: () => void
29+
}
2530

26-
// Sync URL state with CTA state
31+
const PreviewContext = React.createContext<PreviewContextValue>({
32+
canPreview: false,
33+
previewActivated: false,
34+
activatePreview: () => {},
35+
})
36+
37+
export function usePreviewContext() {
38+
return React.useContext(PreviewContext)
39+
}
40+
41+
// Inner component that only runs after CTA is ready
42+
// This avoids calling useAddOns before data is loaded (which has a bug)
43+
function BuilderReadyInner({ children }: BuilderProviderProps) {
44+
// Sync URL state with CTA state (uses useAddOns internally)
2745
useBuilderSearch()
2846

2947
// Initialize addons from URL after registry loads
3048
useInitializeAddonsFromUrl()
3149

32-
const ready = useReady()
50+
// Check if user can use preview (admin only for now)
51+
const capabilities = useCapabilities()
52+
const canPreview = hasCapability(capabilities, 'admin')
53+
54+
// Lazy preview activation - only start WebContainer when user clicks Preview
55+
const [previewActivated, setPreviewActivated] = React.useState(false)
56+
const activatePreview = React.useCallback(() => {
57+
setPreviewActivated(true)
58+
}, [])
59+
60+
const previewContextValue = React.useMemo(
61+
() => ({ canPreview, previewActivated, activatePreview }),
62+
[canPreview, previewActivated, activatePreview],
63+
)
64+
3365
const dryRun = useDryRun()
3466

3567
// Convert dry run files to WebContainer format
@@ -42,17 +74,60 @@ function BuilderProviderInner({ children }: BuilderProviderProps) {
4274
}))
4375
}, [dryRunFiles])
4476

45-
if (!ready) {
46-
return <BuilderLoading />
77+
// Only load WebContainer if user can preview AND has activated it
78+
if (!canPreview || !previewActivated) {
79+
return (
80+
<PreviewContext.Provider value={previewContextValue}>
81+
{children}
82+
</PreviewContext.Provider>
83+
)
4784
}
4885

4986
return (
50-
<WebContainerProvider projectFiles={projectFiles}>
51-
{children}
52-
</WebContainerProvider>
87+
<PreviewContext.Provider value={previewContextValue}>
88+
<WebContainerProvider projectFiles={projectFiles}>
89+
{children}
90+
</WebContainerProvider>
91+
</PreviewContext.Provider>
5392
)
5493
}
5594

95+
function BuilderProviderInner({ children }: BuilderProviderProps) {
96+
// Initialize manager to start CTA engine
97+
useManager()
98+
99+
const ready = useReady()
100+
101+
// Delay showing loading state to avoid flash during HMR
102+
// HMR typically re-initializes within ~100ms, so we wait a bit before showing loading
103+
const [showLoading, setShowLoading] = React.useState(false)
104+
105+
React.useEffect(() => {
106+
if (ready) {
107+
setShowLoading(false)
108+
return
109+
}
110+
111+
// Only show loading after a short delay to avoid HMR flash
112+
const timeout = setTimeout(() => {
113+
setShowLoading(true)
114+
}, 150)
115+
116+
return () => clearTimeout(timeout)
117+
}, [ready])
118+
119+
if (!ready && showLoading) {
120+
return <BuilderLoading />
121+
}
122+
123+
if (!ready) {
124+
// Brief moment before showing loading - render nothing or a minimal placeholder
125+
return null
126+
}
127+
128+
return <BuilderReadyInner>{children}</BuilderReadyInner>
129+
}
130+
56131
export function BuilderProvider({ children }: BuilderProviderProps) {
57132
return (
58133
<CTAProvider>

0 commit comments

Comments
 (0)