Files
zeekay 0129f23dc0 Remove the store from the docs site
Supabase auth, Stripe, purchase modals, Bento and Takeout promotion, the
consulting link and the vendor's talk images were the upstream business,
not documentation. The header, home page, theme page and quick nav lose
the account and promotion surfaces; the content loses the paid gating.
2026-09-03 18:02:51 -07:00

354 lines
8.2 KiB
TypeScript

import { ChevronLeft, ChevronRight, X } from '@hanzogui/lucide-icons-2'
import { useStore } from '@hanzogui/use-store'
import type { GuiElement } from '@hanzogui/web'
import {
memo,
Suspense,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import {
AnimatePresence,
Button,
ScrollView,
Separator,
Spacer,
styled,
Theme,
useMedia,
View,
XStack,
YStack,
} from '@hanzo/gui'
import { ThemeNameEffectNoTheme } from '~/features/site/theme/ThemeNameEffect'
import { Dialogs } from '~/features/studio/components/Dialogs'
import {
StudioPreviewComponents,
StudioPreviewComponentsSkeleton,
} from '~/features/studio/theme/StudioPreviewComponents'
import { StudioPreviewComponentsBar } from '~/features/studio/theme/StudioPreviewComponentsBar'
import { useBaseThemePreview } from '~/features/studio/theme/steps/2-base/useBaseThemePreview'
import { useThemeBuilderStore } from '~/features/studio/theme/store/ThemeBuilderStore'
import { lastInserted } from '~/features/studio/theme/updatePreviewTheme'
import { weakKey } from '~/helpers/weakKey'
import { type ThemePageProps, themePageStore, ThemePageStore } from './themePageStore'
import { router, useRouter } from 'one'
// TO avoid changing the entire React tree we can do this, better perf
export function ThemePageUpdater(props: ThemePageProps) {
useLayoutEffect(() => {
themePageStore.setProps(props)
}, [props])
return null
}
export function ThemePage() {
const { curProps: props } = useStore(ThemePageStore)
return (
<>
<Dialogs />
<YStack shrink={0} flexBasis="auto" mb="$10">
<Suspense fallback={null}>
<ThemeBuilderModal />
</Suspense>
<XStack
width="100%"
height="max-content"
pr={540}
pt={10}
$lg={{ pr: 0 }}
justify="flex-end"
overflow="hidden"
z={100}
>
<YStack
p="$4"
flex={1}
flexBasis="auto"
maxW="calc(min(100vw, 1300px))"
group="content"
$md={{
maxW: `calc(min(100vw, 900px))`,
p: '$4',
}}
>
<PreviewTheme>
<YStack gap="$6">
<StudioPreviewComponentsBar
scrollView={
typeof window !== 'undefined' ? document.documentElement : null
}
/>
{typeof window !== 'undefined' ? (
<StudioPreviewComponents isReady={typeof window !== 'undefined'} />
) : (
<StudioPreviewComponentsSkeleton />
)}
</YStack>
</PreviewTheme>
</YStack>
</XStack>
</YStack>
</>
)
}
const PreviewTheme = (props: { children: any; noKey?: any }) => {
const { name: baseStepThemeName } = useBaseThemePreview()
return (
<>
<Theme name={baseStepThemeName}>
<ThemeNameEffectNoTheme />
<YStack flex={1} flexBasis="auto">
{props.children}
</YStack>
</Theme>
</>
)
}
const Empty = () => null
const ThemeBuilderModal = memo(() => {
const store = useThemeBuilderStore()
const { currentSection } = store
const StepComponent = currentSection?.children ?? Empty
const ref = useRef<GuiElement>(null)
const [hide, setHide] = useState(false)
const { gtLg } = useMedia()
useEffect(() => {
if (gtLg) {
setHide(false)
}
}, [gtLg])
return (
<YStack
position="fixed"
t={70}
r={0}
b={0}
width={530}
maxH="90vh"
maxW="95vw"
z={1000}
x={hide ? 500 : 0}
transition="medium"
>
<YStack
fullscreen
transition="medium"
animateOnly={['transform']}
ref={ref}
x={0}
elevation="$5"
borderTopLeftRadius="$6"
borderBottomLeftRadius="$6"
borderWidth={0.5}
borderColor="$color6"
bg="$background06"
backdropFilter="blur(60px)"
{...(hide && {
borderColor: '$color0',
bg: '$color3',
})}
>
<XStack
position="absolute"
z={999}
t="$2"
l="$2"
$gtLg={{
display: 'none',
}}
>
<Button
size="$2"
circular
icon={hide ? ChevronLeft : ChevronRight}
onPress={() => setHide(!hide)}
/>
</XStack>
<YStack
transition={['medium', { opacity: { overshootClamping: true } }]}
opacity={hide ? 0 : 1}
gap="$4"
flex={1}
>
<AnimatePresence exitBeforeEnter custom={{ going: store.direction }}>
<Section
flex={1}
transition="75ms"
animateOnly={['transform', 'opacity']}
key={weakKey(StepComponent)}
>
{useMemo(() => {
return (
<ScrollView flex={1} contentContainerStyle={{ flex: 1 }}>
<YStack flex={1}>
<StepComponent />
</YStack>
</ScrollView>
)
}, [StepComponent])}
</Section>
</AnimatePresence>
</YStack>
<StudioThemeBuilderTray />
{/* bottom */}
<StudioThemeBuilderBottomBar />
</YStack>
</YStack>
)
})
const StudioThemeBuilderTray = memo(() => {
const store = useThemeBuilderStore()
const Tray = store.currentSection?.tray
if (!Tray) {
return null
}
return (
<>
<Tray />
</>
)
})
const StudioThemeBuilderBottomBar = memo(() => {
return (
<XStack p="$4" py="$3" items="center" z={100} bg="$background02">
<CurrentStepActionBar />
<Spacer flex={1} />
<ThemeStudioStepButtonsBar />
</XStack>
)
})
const CurrentStepActionBar = () => {
const router = useRouter()
const { currentSection } = useThemeBuilderStore()
const ActionComponent = currentSection?.actions as any
if (!ActionComponent) {
return null
}
return <ActionComponent />
}
const ThemeStudioStepButtonsBar = () => {
const store = useThemeBuilderStore()
const {
canGoBackward,
canGoForward,
backward,
forward,
currentSection,
disableForward,
} = store
const forwardOrFinish = () => {
if (!canGoForward) {
console.warn('done')
} else {
forward()
}
}
return (
<XStack gap="$2">
{typeof location !== 'undefined' &&
location.host === 'localhost' &&
lastInserted && (
<>
<a
href={`start-chat-dev://theme?value=${btoa(JSON.stringify(lastInserted))}`}
>
<Button size="$3">Chat</Button>
</a>
<View flex={1} />
</>
)}
<Button
size="$3"
onPress={() => {
if (confirm(`Reset theme builder state?`)) {
store.reset()
router.navigate('/theme')
}
}}
>
Reset
</Button>
{canGoBackward && (
<Button
chromeless
size="$3"
// disabled={disableBackward}
// opacity={disableBackward ? 0.5 : 1}
icon={ChevronLeft}
onPress={backward}
>
{currentSection.prevTitle || 'Back'}
</Button>
)}
{canGoForward && (
<Theme name={!disableForward ? 'accent' : undefined}>
<Button
size="$3"
disabled={disableForward}
opacity={disableForward ? 0.5 : 1}
cursor={disableForward ? 'not-allowed' : undefined}
iconAfter={canGoForward ? ChevronRight : null}
onPress={forwardOrFinish}
>
{currentSection.nextTitle || 'Next'}
</Button>
</Theme>
)}
</XStack>
)
}
const Section = styled(YStack, {
gap: '$2',
x: 0,
opacity: 1,
variants: {
// 1 = right, 0 = nowhere, -1 = left
going: {
':number': (going) => ({
enterStyle: {
x: going > 0 ? 20 : -20,
opacity: 0,
},
exitStyle: {
zIndex: 0,
x: going < 0 ? 20 : -20,
opacity: 0,
},
}),
},
} as const,
})