mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
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.
354 lines
8.2 KiB
TypeScript
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,
|
|
})
|