2
0

(theme) Add container theme options: border, shadow, filter (#1436)

Closes #1332
This commit is contained in:
Baptiste Arnaud
2024-04-10 10:19:54 +02:00
committed by GitHub
parent 75dd554ac2
commit 5c3c7c2b64
46 changed files with 2126 additions and 549 deletions

View File

@ -1,4 +1,3 @@
# Make sure to change this to your own random string of 32 characters (https://docs.typebot.io/self-hosting/docker#2-add-the-required-configuration)
ENCRYPTION_SECRET=H+KbL/OFrqbEuDy/1zX8bsPG+spXri3S ENCRYPTION_SECRET=H+KbL/OFrqbEuDy/1zX8bsPG+spXri3S
DATABASE_URL=postgresql://postgres:typebot@localhost:5432/typebot DATABASE_URL=postgresql://postgres:typebot@localhost:5432/typebot

View File

@ -12,9 +12,10 @@
"editor.tabSize": 2, "editor.tabSize": 2,
"typescript.updateImportsOnFileMove.enabled": "always", "typescript.updateImportsOnFileMove.enabled": "always",
"playwright.env": { "playwright.env": {
"DATABASE_URL": "postgresql://postgres:typebot@localhost:5432/typebot", "DATABASE_URL": "postgresql://postgres:typebot@127.0.0.1:5432/typebot",
"NEXT_PUBLIC_VIEWER_URL": "http://localhost:3001", "NEXT_PUBLIC_VIEWER_URL": "http://localhost:3001",
"NEXTAUTH_URL": "http://localhost:3000" "NEXTAUTH_URL": "http://localhost:3000",
"ENCRYPTION_SECRET": "H+KbL/OFrqbEuDy/1zX8bsPG+spXri3S"
}, },
"[prisma]": { "[prisma]": {
"editor.defaultFormatter": "Prisma.prisma" "editor.defaultFormatter": "Prisma.prisma"

View File

@ -13,6 +13,7 @@
"format:check": "prettier --check ./src --ignore-path ../../.prettierignore" "format:check": "prettier --check ./src --ignore-path ../../.prettierignore"
}, },
"dependencies": { "dependencies": {
"@typebot.io/theme": "workspace:*",
"@braintree/sanitize-url": "7.0.1", "@braintree/sanitize-url": "7.0.1",
"@chakra-ui/anatomy": "2.1.1", "@chakra-ui/anatomy": "2.1.1",
"@chakra-ui/react": "2.7.1", "@chakra-ui/react": "2.7.1",

View File

@ -35,10 +35,16 @@ const colorsSelection: `#${string}`[] = [
type Props = { type Props = {
value?: string value?: string
defaultValue?: string defaultValue?: string
isDisabled?: boolean
onColorChange: (color: string) => void onColorChange: (color: string) => void
} }
export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => { export const ColorPicker = ({
value,
defaultValue,
isDisabled,
onColorChange,
}: Props) => {
const { t } = useTranslate() const { t } = useTranslate()
const [color, setColor] = useState(defaultValue ?? '') const [color, setColor] = useState(defaultValue ?? '')
const displayedValue = value ?? color const displayedValue = value ?? color
@ -63,6 +69,7 @@ export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
padding={0} padding={0}
borderRadius={3} borderRadius={3}
borderWidth={1} borderWidth={1}
isDisabled={isDisabled}
> >
<Box rounded="full" boxSize="14px" bgColor={displayedValue} /> <Box rounded="full" boxSize="14px" bgColor={displayedValue} />
</Button> </Button>

View File

@ -141,6 +141,8 @@ export const UnsplashPicker = ({ imageSize, onImageSelect }: Props) => {
fetchNewImages(query, 0) fetchNewImages(query, 0)
}} }}
withVariableButton={false} withVariableButton={false}
debounceTimeout={500}
forceDebounce
/> />
<Link <Link
isExternal isExternal

View File

@ -24,6 +24,7 @@ import { env } from '@typebot.io/env'
import { MoreInfoTooltip } from '../MoreInfoTooltip' import { MoreInfoTooltip } from '../MoreInfoTooltip'
export type TextInputProps = { export type TextInputProps = {
forceDebounce?: boolean
defaultValue?: string defaultValue?: string
onChange?: (value: string) => void onChange?: (value: string) => void
debounceTimeout?: number debounceTimeout?: number
@ -62,6 +63,7 @@ export const TextInput = forwardRef(function TextInput(
autoComplete, autoComplete,
isDisabled, isDisabled,
autoFocus, autoFocus,
forceDebounce,
onChange: _onChange, onChange: _onChange,
onFocus, onFocus,
onKeyUp, onKeyUp,
@ -83,7 +85,7 @@ export const TextInput = forwardRef(function TextInput(
const onChange = useDebouncedCallback( const onChange = useDebouncedCallback(
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
_onChange ?? (() => {}), _onChange ?? (() => {}),
env.NEXT_PUBLIC_E2E_TEST ? 0 : debounceTimeout env.NEXT_PUBLIC_E2E_TEST && !forceDebounce ? 0 : debounceTimeout
) )
useEffect(() => { useEffect(() => {

View File

@ -73,7 +73,6 @@ export const PreviewDrawer = () => {
right="0" right="0"
top={`0`} top={`0`}
h={`100%`} h={`100%`}
w={`${width}px`}
bgColor={useColorModeValue('white', 'gray.900')} bgColor={useColorModeValue('white', 'gray.900')}
borderLeftWidth={'1px'} borderLeftWidth={'1px'}
shadow="lg" shadow="lg"
@ -82,6 +81,7 @@ export const PreviewDrawer = () => {
onMouseLeave={() => setIsResizeHandleVisible(false)} onMouseLeave={() => setIsResizeHandleVisible(false)}
p="6" p="6"
zIndex={10} zIndex={10}
style={{ width: `${width}px` }}
> >
<Fade in={isResizeHandleVisible}> <Fade in={isResizeHandleVisible}>
<ResizeHandle <ResizeHandle

View File

@ -5,13 +5,13 @@ import { Typebot } from '@typebot.io/schemas'
import { useState } from 'react' import { useState } from 'react'
import { BubbleSettings } from '../../../settings/BubbleSettings/BubbleSettings' import { BubbleSettings } from '../../../settings/BubbleSettings/BubbleSettings'
import { JavascriptBubbleSnippet } from '../JavascriptBubbleSnippet' import { JavascriptBubbleSnippet } from '../JavascriptBubbleSnippet'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants' import { defaultButtonsBackgroundColor } from '@typebot.io/schemas/features/typebot/theme/constants'
export const parseDefaultBubbleTheme = (typebot?: Typebot) => ({ export const parseDefaultBubbleTheme = (typebot?: Typebot) => ({
button: { button: {
backgroundColor: backgroundColor:
typebot?.theme.chat?.buttons?.backgroundColor ?? typebot?.theme.chat?.buttons?.backgroundColor ??
defaultTheme.chat.buttons.backgroundColor, defaultButtonsBackgroundColor,
}, },
}) })

View File

@ -8,6 +8,7 @@ export const DefaultAvatar = (props: IconProps) => {
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
boxSize="40px" boxSize="40px"
borderRadius="full"
data-testid="default-avatar" data-testid="default-avatar"
{...props} {...props}
> >

View File

@ -35,7 +35,7 @@ export const ThemeSideMenu = () => {
typebot && typebot &&
updateTypebot({ updates: { theme: { ...typebot.theme, customCss } } }) updateTypebot({ updates: { theme: { ...typebot.theme, customCss } } })
const selectedTemplate = ( const selectTemplate = (
selectedTemplate: Partial<Pick<ThemeTemplate, 'id' | 'theme'>> selectedTemplate: Partial<Pick<ThemeTemplate, 'id' | 'theme'>>
) => { ) => {
if (!typebot) return if (!typebot) return
@ -56,6 +56,8 @@ export const ThemeSideMenu = () => {
}, },
}) })
const templateId = typebot?.selectedThemeTemplateId ?? undefined
return ( return (
<Stack <Stack
flex="1" flex="1"
@ -84,12 +86,10 @@ export const ThemeSideMenu = () => {
<AccordionPanel pb={12}> <AccordionPanel pb={12}>
{typebot && ( {typebot && (
<ThemeTemplates <ThemeTemplates
selectedTemplateId={ selectedTemplateId={templateId}
typebot.selectedThemeTemplateId ?? undefined
}
currentTheme={typebot.theme} currentTheme={typebot.theme}
workspaceId={typebot.workspaceId} workspaceId={typebot.workspaceId}
onTemplateSelect={selectedTemplate} onTemplateSelect={selectTemplate}
/> />
)} )}
</AccordionPanel> </AccordionPanel>
@ -106,6 +106,7 @@ export const ThemeSideMenu = () => {
<AccordionPanel pb={4}> <AccordionPanel pb={4}>
{typebot && ( {typebot && (
<GeneralSettings <GeneralSettings
key={templateId}
isBrandingEnabled={ isBrandingEnabled={
typebot.settings.general?.isBrandingEnabled ?? typebot.settings.general?.isBrandingEnabled ??
defaultSettings.general.isBrandingEnabled defaultSettings.general.isBrandingEnabled
@ -128,9 +129,11 @@ export const ThemeSideMenu = () => {
<AccordionPanel pb={4}> <AccordionPanel pb={4}>
{typebot && ( {typebot && (
<ChatThemeSettings <ChatThemeSettings
key={templateId}
workspaceId={typebot.workspaceId} workspaceId={typebot.workspaceId}
typebotId={typebot.id} typebotId={typebot.id}
chatTheme={typebot.theme.chat} chatTheme={typebot.theme.chat}
generalBackground={typebot.theme.general?.background}
onChatThemeChange={updateChatTheme} onChatThemeChange={updateChatTheme}
/> />
)} )}
@ -147,6 +150,7 @@ export const ThemeSideMenu = () => {
<AccordionPanel pb={4}> <AccordionPanel pb={4}>
{typebot && ( {typebot && (
<CustomCssSettings <CustomCssSettings
key={templateId}
customCss={typebot.theme.customCss} customCss={typebot.theme.customCss}
onCustomCssChange={updateCustomCss} onCustomCssChange={updateCustomCss}
/> />

View File

@ -19,8 +19,13 @@ import { Theme, ThemeTemplate } from '@typebot.io/schemas'
import { useState } from 'react' import { useState } from 'react'
import { DefaultAvatar } from './DefaultAvatar' import { DefaultAvatar } from './DefaultAvatar'
import { import {
defaultButtonsBackgroundColor,
BackgroundType, BackgroundType,
defaultTheme, defaultGuestAvatarIsEnabled,
defaultGuestBubblesBackgroundColor,
defaultHostAvatarIsEnabled,
defaultBackgroundColor,
defaultHostBubblesBackgroundColor,
} from '@typebot.io/schemas/features/typebot/theme/constants' } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react' import { useTranslate } from '@tolgee/react'
@ -71,28 +76,28 @@ export const ThemeTemplateCard = ({
const hostAvatar = { const hostAvatar = {
isEnabled: isEnabled:
themeTemplate.theme.chat?.hostAvatar?.isEnabled ?? themeTemplate.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled, defaultHostAvatarIsEnabled,
url: themeTemplate.theme.chat?.hostAvatar?.url, url: themeTemplate.theme.chat?.hostAvatar?.url,
} }
const hostBubbleBgColor = const hostBubbleBgColor =
themeTemplate.theme.chat?.hostBubbles?.backgroundColor ?? themeTemplate.theme.chat?.hostBubbles?.backgroundColor ??
defaultTheme.chat.hostBubbles.backgroundColor defaultHostBubblesBackgroundColor
const guestAvatar = { const guestAvatar = {
isEnabled: isEnabled:
themeTemplate.theme.chat?.guestAvatar?.isEnabled ?? themeTemplate.theme.chat?.guestAvatar?.isEnabled ??
defaultTheme.chat.guestAvatar.isEnabled, defaultGuestAvatarIsEnabled,
url: themeTemplate.theme.chat?.guestAvatar?.url, url: themeTemplate.theme.chat?.guestAvatar?.url,
} }
const guestBubbleBgColor = const guestBubbleBgColor =
themeTemplate.theme.chat?.guestBubbles?.backgroundColor ?? themeTemplate.theme.chat?.guestBubbles?.backgroundColor ??
defaultTheme.chat.guestBubbles.backgroundColor defaultGuestBubblesBackgroundColor
const buttonBgColor = const buttonBgColor =
themeTemplate.theme.chat?.buttons?.backgroundColor ?? themeTemplate.theme.chat?.buttons?.backgroundColor ??
defaultTheme.chat.buttons.backgroundColor defaultButtonsBackgroundColor
return ( return (
<Stack <Stack
@ -197,8 +202,7 @@ const parseBackground = (
case undefined: case undefined:
case BackgroundType.COLOR: case BackgroundType.COLOR:
return { return {
backgroundColor: backgroundColor: background?.content ?? defaultBackgroundColor,
background?.content ?? defaultTheme.general.background.content,
} }
case BackgroundType.IMAGE: case BackgroundType.IMAGE:
return { backgroundImage: `url(${background.content})` } return { backgroundImage: `url(${background.content})` }

View File

@ -0,0 +1,134 @@
import { FormLabel, HStack, Stack } from '@chakra-ui/react'
import { ChatTheme, GeneralTheme } from '@typebot.io/schemas'
import React from 'react'
import {
defaultBlur,
defaultContainerBackgroundColor,
defaultContainerMaxHeight,
defaultContainerMaxWidth,
defaultDarkTextColor,
defaultLightTextColor,
defaultOpacity,
defaultRoundness,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { ContainerThemeForm } from './ContainerThemeForm'
import { NumberInput } from '@/components/inputs'
import { DropdownList } from '@/components/DropdownList'
import { isChatContainerLight } from '@typebot.io/theme/isChatContainerLight'
type Props = {
generalBackground: GeneralTheme['background']
container: ChatTheme['container']
onContainerChange: (container: ChatTheme['container'] | undefined) => void
}
export const ChatContainerForm = ({
generalBackground,
container,
onContainerChange,
}: Props) => {
const updateMaxWidth = (maxWidth?: number) =>
updateDimension('maxWidth', maxWidth, maxWidthUnit)
const updateMaxWidthUnit = (unit: string) =>
updateDimension('maxWidth', maxWidth, unit)
const updateMaxHeight = (maxHeight?: number) =>
updateDimension('maxHeight', maxHeight, maxHeightUnit)
const updateMaxHeightUnit = (unit: string) =>
updateDimension('maxHeight', maxHeight, unit)
const updateDimension = (
dimension: 'maxWidth' | 'maxHeight',
value: number | undefined,
unit: string
) =>
onContainerChange({
...container,
[dimension]: `${value}${unit}`,
})
const { value: maxWidth, unit: maxWidthUnit } = parseValueAndUnit(
container?.maxWidth ?? defaultContainerMaxWidth
)
const { value: maxHeight, unit: maxHeightUnit } = parseValueAndUnit(
container?.maxHeight ?? defaultContainerMaxHeight
)
return (
<Stack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Max width:
</FormLabel>
<HStack>
<NumberInput
size="sm"
width="100px"
defaultValue={maxWidth}
min={0}
step={10}
withVariableButton={false}
onValueChange={updateMaxWidth}
/>
<DropdownList
size="sm"
items={['px', '%', 'vh', 'vw']}
currentItem={maxWidthUnit}
onItemSelect={updateMaxWidthUnit}
/>
</HStack>
</HStack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Max height:
</FormLabel>
<HStack>
<NumberInput
size="sm"
width="100px"
defaultValue={maxHeight}
min={0}
step={10}
onValueChange={updateMaxHeight}
withVariableButton={false}
/>
<DropdownList
size="sm"
items={['px', '%', 'vh', 'vw']}
currentItem={maxHeightUnit}
onItemSelect={updateMaxHeightUnit}
/>
</HStack>
</HStack>
<ContainerThemeForm
theme={container}
defaultTheme={{
backgroundColor: defaultContainerBackgroundColor,
border: {
roundeness: defaultRoundness,
},
blur: defaultBlur,
opacity: defaultOpacity,
color: isChatContainerLight({
chatContainer: container,
generalBackground,
})
? defaultLightTextColor
: defaultDarkTextColor,
}}
onThemeChange={onContainerChange}
/>
</Stack>
)
}
const parseValueAndUnit = (valueWithUnit: string) => {
const value = parseFloat(valueWithUnit)
const unit = valueWithUnit.replace(value.toString(), '')
return { value, unit }
}

View File

@ -1,23 +1,36 @@
import {
LargeRadiusIcon,
MediumRadiusIcon,
NoRadiusIcon,
} from '@/components/icons'
import { RadioButtons } from '@/components/inputs/RadioButtons'
import { Heading, Stack } from '@chakra-ui/react' import { Heading, Stack } from '@chakra-ui/react'
import { AvatarProps, ChatTheme, Theme } from '@typebot.io/schemas' import {
AvatarProps,
ChatTheme,
GeneralTheme,
Theme,
} from '@typebot.io/schemas'
import React from 'react' import React from 'react'
import { AvatarForm } from './AvatarForm' import { AvatarForm } from './AvatarForm'
import { ButtonsTheme } from './ButtonsTheme'
import { GuestBubbles } from './GuestBubbles'
import { HostBubbles } from './HostBubbles'
import { InputsTheme } from './InputsTheme'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react' import { useTranslate } from '@tolgee/react'
import { ChatContainerForm } from './ChatContainerForm'
import { ContainerThemeForm } from './ContainerThemeForm'
import {
defaultButtonsBackgroundColor,
defaultButtonsColor,
defaultButtonsBorderThickness,
defaultGuestBubblesBackgroundColor,
defaultGuestBubblesColor,
defaultHostBubblesBackgroundColor,
defaultHostBubblesColor,
defaultInputsBackgroundColor,
defaultInputsColor,
defaultInputsPlaceholderColor,
defaultInputsShadow,
defaultOpacity,
defaultBlur,
defaultRoundness,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = { type Props = {
workspaceId: string workspaceId: string
typebotId: string typebotId: string
generalBackground: GeneralTheme['background']
chatTheme: Theme['chat'] chatTheme: Theme['chat']
onChatThemeChange: (chatTheme: ChatTheme) => void onChatThemeChange: (chatTheme: ChatTheme) => void
} }
@ -26,6 +39,7 @@ export const ChatThemeSettings = ({
workspaceId, workspaceId,
typebotId, typebotId,
chatTheme, chatTheme,
generalBackground,
onChatThemeChange, onChatThemeChange,
}: Props) => { }: Props) => {
const { t } = useTranslate() const { t } = useTranslate()
@ -44,6 +58,16 @@ export const ChatThemeSettings = ({
const updateInputs = (inputs: NonNullable<Theme['chat']>['inputs']) => const updateInputs = (inputs: NonNullable<Theme['chat']>['inputs']) =>
onChatThemeChange({ ...chatTheme, inputs }) onChatThemeChange({ ...chatTheme, inputs })
const updateChatContainer = (
container: NonNullable<Theme['chat']>['container']
) => onChatThemeChange({ ...chatTheme, container })
const updateInputsPlaceholderColor = (placeholderColor: string) =>
onChatThemeChange({
...chatTheme,
inputs: { ...chatTheme?.inputs, placeholderColor },
})
const updateHostAvatar = (hostAvatar: AvatarProps) => const updateHostAvatar = (hostAvatar: AvatarProps) =>
onChatThemeChange({ ...chatTheme, hostAvatar }) onChatThemeChange({ ...chatTheme, hostAvatar })
@ -52,6 +76,14 @@ export const ChatThemeSettings = ({
return ( return (
<Stack spacing={6}> <Stack spacing={6}>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">Container</Heading>
<ChatContainerForm
generalBackground={generalBackground}
container={chatTheme?.container}
onContainerChange={updateChatContainer}
/>
</Stack>
<AvatarForm <AvatarForm
uploadFileProps={{ uploadFileProps={{
workspaceId, workspaceId,
@ -59,7 +91,7 @@ export const ChatThemeSettings = ({
fileName: 'hostAvatar', fileName: 'hostAvatar',
}} }}
title={t('theme.sideMenu.chat.botAvatar')} title={t('theme.sideMenu.chat.botAvatar')}
avatarProps={chatTheme?.hostAvatar ?? defaultTheme.chat.hostAvatar} avatarProps={chatTheme?.hostAvatar}
isDefaultCheck isDefaultCheck
onAvatarChange={updateHostAvatar} onAvatarChange={updateHostAvatar}
/> />
@ -70,63 +102,83 @@ export const ChatThemeSettings = ({
fileName: 'guestAvatar', fileName: 'guestAvatar',
}} }}
title={t('theme.sideMenu.chat.userAvatar')} title={t('theme.sideMenu.chat.userAvatar')}
avatarProps={chatTheme?.guestAvatar ?? defaultTheme.chat.guestAvatar} avatarProps={chatTheme?.guestAvatar}
onAvatarChange={updateGuestAvatar} onAvatarChange={updateGuestAvatar}
/> />
<Stack borderWidth={1} rounded="md" p="4" spacing={4}> <Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.botBubbles')}</Heading> <Heading fontSize="lg">{t('theme.sideMenu.chat.botBubbles')}</Heading>
<HostBubbles <ContainerThemeForm
hostBubbles={chatTheme?.hostBubbles ?? defaultTheme.chat.hostBubbles} testId="hostBubblesTheme"
onHostBubblesChange={updateHostBubbles} theme={chatTheme?.hostBubbles}
onThemeChange={updateHostBubbles}
defaultTheme={{
backgroundColor: defaultHostBubblesBackgroundColor,
color: defaultHostBubblesColor,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
},
}}
/> />
</Stack> </Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}> <Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.userBubbles')}</Heading> <Heading fontSize="lg">{t('theme.sideMenu.chat.userBubbles')}</Heading>
<GuestBubbles <ContainerThemeForm
guestBubbles={ testId="guestBubblesTheme"
chatTheme?.guestBubbles ?? defaultTheme.chat.guestBubbles theme={chatTheme?.guestBubbles}
} onThemeChange={updateGuestBubbles}
onGuestBubblesChange={updateGuestBubbles} defaultTheme={{
backgroundColor: defaultGuestBubblesBackgroundColor,
color: defaultGuestBubblesColor,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
},
}}
/> />
</Stack> </Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}> <Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.buttons')}</Heading> <Heading fontSize="lg">{t('theme.sideMenu.chat.buttons')}</Heading>
<ButtonsTheme <ContainerThemeForm
buttons={chatTheme?.buttons ?? defaultTheme.chat.buttons} testId="buttonsTheme"
onButtonsChange={updateButtons} theme={chatTheme?.buttons}
onThemeChange={updateButtons}
defaultTheme={{
backgroundColor: defaultButtonsBackgroundColor,
color: defaultButtonsColor,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
thickness: defaultButtonsBorderThickness,
color:
chatTheme?.buttons?.backgroundColor ??
defaultButtonsBackgroundColor,
},
}}
/> />
</Stack> </Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}> <Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.inputs')}</Heading> <Heading fontSize="lg">{t('theme.sideMenu.chat.inputs')}</Heading>
<InputsTheme <ContainerThemeForm
inputs={chatTheme?.inputs ?? defaultTheme.chat.inputs} testId="inputsTheme"
onInputsChange={updateInputs} theme={chatTheme?.inputs}
/> onThemeChange={updateInputs}
</Stack> onPlaceholderColorChange={updateInputsPlaceholderColor}
<Stack borderWidth={1} rounded="md" p="4" spacing={4}> defaultTheme={{
<Heading fontSize="lg"> backgroundColor: defaultInputsBackgroundColor,
{t('theme.sideMenu.chat.cornersRoundness')} color: defaultInputsColor,
</Heading> placeholderColor: defaultInputsPlaceholderColor,
<RadioButtons shadow: defaultInputsShadow,
options={[ opacity: defaultOpacity,
{ blur: defaultBlur,
label: <NoRadiusIcon />, border: {
value: 'none', roundeness: defaultRoundness,
}, },
{ }}
label: <MediumRadiusIcon />,
value: 'medium',
},
{
label: <LargeRadiusIcon />,
value: 'large',
},
]}
value={chatTheme?.roundness ?? defaultTheme.chat.roundness}
onSelect={(roundness) =>
onChatThemeChange({ ...chatTheme, roundness })
}
/> />
</Stack> </Stack>
</Stack> </Stack>

View File

@ -0,0 +1,297 @@
import {
Stack,
FormLabel,
HStack,
Switch,
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
} from '@chakra-ui/react'
import {
ContainerTheme,
ContainerBorderTheme,
InputTheme,
} from '@typebot.io/schemas'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
import { useTranslate } from '@tolgee/react'
import { NumberInput } from '@/components/inputs'
import { DropdownList } from '@/components/DropdownList'
import {
borderRoundness,
defaultOpacity,
shadows,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props<T extends ((placeholder: string) => void) | undefined> = {
theme: (T extends undefined ? ContainerTheme : InputTheme) | undefined
defaultTheme: T extends undefined ? ContainerTheme : InputTheme
placeholderColor?: T extends undefined ? never : string
testId?: string
onThemeChange: (
theme: T extends undefined ? ContainerTheme : InputTheme
) => void
onPlaceholderColorChange?: T
}
export const ContainerThemeForm = <
T extends ((placeholder: string) => void) | undefined
>({
theme,
testId,
defaultTheme,
onPlaceholderColorChange,
onThemeChange,
}: Props<T>) => {
const { t } = useTranslate()
const updateBackgroundColor = (backgroundColor: string) =>
onThemeChange({ ...theme, backgroundColor })
const toggleBackgroundColor = () =>
onThemeChange({
...theme,
backgroundColor:
backgroundColor === 'transparent' ? '#ffffff' : 'transparent',
})
const updateTextColor = (color: string) => onThemeChange({ ...theme, color })
const updateShadow = (shadow?: ContainerTheme['shadow']) =>
onThemeChange({ ...theme, shadow })
const updateBlur = (blur?: number) => onThemeChange({ ...theme, blur })
const updateOpacity = (opacity?: number) =>
onThemeChange({ ...theme, opacity })
const updateBorder = (border: ContainerBorderTheme) =>
onThemeChange({ ...theme, border })
const updatePlaceholderColor = (color: string) =>
onThemeChange({ ...theme, placeholderColor: color } as InputTheme)
const backgroundColor =
theme?.backgroundColor ?? defaultTheme?.backgroundColor
const shadow = theme?.shadow ?? defaultTheme?.shadow ?? 'none'
return (
<Stack spacing={4} data-testid={testId}>
<HStack justify="space-between">
<FormLabel mb="0" mr="0">
{t('theme.sideMenu.chat.theme.background')}
</FormLabel>
<HStack>
<Switch
defaultChecked={backgroundColor !== 'transparent'}
onChange={toggleBackgroundColor}
/>
<ColorPicker
isDisabled={backgroundColor === 'transparent'}
value={backgroundColor}
onColorChange={updateBackgroundColor}
/>
</HStack>
</HStack>
<HStack justify="space-between">
<FormLabel mb="0" mr="0">
{t('theme.sideMenu.chat.theme.text')}
</FormLabel>
<ColorPicker
value={theme?.color ?? defaultTheme?.color}
onColorChange={updateTextColor}
/>
</HStack>
{onPlaceholderColorChange && (
<HStack justify="space-between">
<FormLabel mb="0" mr="0">
{t('theme.sideMenu.chat.theme.placeholder')}
</FormLabel>
<ColorPicker
value={
theme && 'placeholderColor' in theme
? theme.placeholderColor
: defaultTheme && 'placeholderColor' in defaultTheme
? defaultTheme.placeholderColor
: undefined
}
onColorChange={updatePlaceholderColor}
/>
</HStack>
)}
<Accordion allowToggle>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Border
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
<BorderThemeForm
border={theme?.border}
defaultBorder={defaultTheme.border}
onBorderChange={updateBorder}
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
Advanced
<AccordionIcon />
</AccordionButton>
<AccordionPanel as={Stack}>
{backgroundColor !== 'transparent' && (
<>
<NumberInput
size="sm"
direction="row"
label="Opacity:"
width="100px"
min={0}
max={1}
step={0.1}
defaultValue={theme?.opacity ?? defaultTheme?.opacity}
onValueChange={updateOpacity}
withVariableButton={false}
/>
{(theme?.opacity ?? defaultTheme?.opacity) !== 1 && (
<NumberInput
size="sm"
direction="row"
label="Blur:"
suffix="px"
width="100px"
min={0}
defaultValue={theme?.blur ?? defaultTheme?.blur}
onValueChange={updateBlur}
withVariableButton={false}
/>
)}
</>
)}
<HStack justify="space-between">
<FormLabel mb="0" mr="0">
Shadow:
</FormLabel>
<HStack>
<DropdownList
currentItem={shadow}
onItemSelect={updateShadow}
items={shadows}
size="sm"
/>
</HStack>
</HStack>
</AccordionPanel>
</AccordionItem>
</Accordion>
</Stack>
)
}
const BorderThemeForm = ({
border,
defaultBorder,
onBorderChange,
}: {
border: ContainerBorderTheme | undefined
defaultBorder: ContainerBorderTheme | undefined
onBorderChange: (border: ContainerBorderTheme) => void
}) => {
const updateRoundness = (roundeness: (typeof borderRoundness)[number]) => {
onBorderChange({ ...border, roundeness })
}
const updateCustomRoundeness = (customRoundeness: number | undefined) => {
onBorderChange({ ...border, customRoundeness })
}
const updateThickness = (thickness: number | undefined) => {
onBorderChange({ ...border, thickness })
}
const updateColor = (color: string | undefined) => {
onBorderChange({ ...border, color })
}
const updateOpacity = (opacity: number | undefined) => {
onBorderChange({ ...border, opacity })
}
const thickness = border?.thickness ?? defaultBorder?.thickness ?? 0
return (
<Stack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Roundness:
</FormLabel>
<HStack>
<DropdownList
currentItem={border?.roundeness ?? defaultBorder?.roundeness}
onItemSelect={updateRoundness}
items={borderRoundness}
placeholder="md"
size="sm"
/>
{(border?.roundeness ?? defaultBorder?.roundeness) === 'custom' && (
<NumberInput
size="sm"
suffix="px"
width="60px"
min={0}
defaultValue={border?.customRoundeness}
onValueChange={updateCustomRoundeness}
withVariableButton={false}
/>
)}
</HStack>
</HStack>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Thickness:
</FormLabel>
<NumberInput
size="sm"
suffix="px"
width="60px"
min={0}
defaultValue={thickness}
onValueChange={updateThickness}
withVariableButton={false}
/>
</HStack>
{thickness > 0 && (
<>
<HStack justifyContent="space-between">
<FormLabel mb="0" mr="0">
Color:
</FormLabel>
<ColorPicker
value={border?.color ?? defaultBorder?.color}
onColorChange={updateColor}
/>
</HStack>
<NumberInput
size="sm"
direction="row"
label="Opacity:"
width="100px"
min={0}
max={1}
step={0.1}
defaultValue={border?.opacity ?? defaultOpacity}
onValueChange={updateOpacity}
withVariableButton={false}
/>
</>
)}
</Stack>
)
}

View File

@ -1,13 +1,16 @@
import { Stack, Flex, Text } from '@chakra-ui/react' import { Stack, Flex, Text } from '@chakra-ui/react'
import { ContainerColors } from '@typebot.io/schemas' import { ContainerTheme } from '@typebot.io/schemas'
import React from 'react' import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker' import { ColorPicker } from '../../../../components/ColorPicker'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react' import { useTranslate } from '@tolgee/react'
import {
defaultGuestBubblesBackgroundColor,
defaultGuestBubblesColor,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = { type Props = {
guestBubbles: ContainerColors | undefined guestBubbles: ContainerTheme | undefined
onGuestBubblesChange: (hostBubbles: ContainerColors | undefined) => void onGuestBubblesChange: (hostBubbles: ContainerTheme | undefined) => void
} }
export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => { export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
@ -25,8 +28,7 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
<Text>{t('theme.sideMenu.chat.theme.background')}</Text> <Text>{t('theme.sideMenu.chat.theme.background')}</Text>
<ColorPicker <ColorPicker
value={ value={
guestBubbles?.backgroundColor ?? guestBubbles?.backgroundColor ?? defaultGuestBubblesBackgroundColor
defaultTheme.chat.guestBubbles.backgroundColor
} }
onColorChange={updateBackground} onColorChange={updateBackground}
/> />
@ -34,7 +36,7 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.text')}</Text> <Text>{t('theme.sideMenu.chat.theme.text')}</Text>
<ColorPicker <ColorPicker
value={guestBubbles?.color ?? defaultTheme.chat.guestBubbles.color} value={guestBubbles?.color ?? defaultGuestBubblesColor}
onColorChange={updateText} onColorChange={updateText}
/> />
</Flex> </Flex>

View File

@ -1,13 +1,16 @@
import { Stack, Flex, Text } from '@chakra-ui/react' import { Stack, Flex, Text } from '@chakra-ui/react'
import { ContainerColors } from '@typebot.io/schemas' import { ContainerTheme } from '@typebot.io/schemas'
import React from 'react' import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker' import { ColorPicker } from '../../../../components/ColorPicker'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react' import { useTranslate } from '@tolgee/react'
import {
defaultHostBubblesBackgroundColor,
defaultHostBubblesColor,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = { type Props = {
hostBubbles: ContainerColors | undefined hostBubbles: ContainerTheme | undefined
onHostBubblesChange: (hostBubbles: ContainerColors | undefined) => void onHostBubblesChange: (hostBubbles: ContainerTheme | undefined) => void
} }
export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => { export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
@ -24,8 +27,7 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
<Text>{t('theme.sideMenu.chat.theme.background')}</Text> <Text>{t('theme.sideMenu.chat.theme.background')}</Text>
<ColorPicker <ColorPicker
value={ value={
hostBubbles?.backgroundColor ?? hostBubbles?.backgroundColor ?? defaultHostBubblesBackgroundColor
defaultTheme.chat.hostBubbles.backgroundColor
} }
onColorChange={handleBackgroundChange} onColorChange={handleBackgroundChange}
/> />
@ -33,7 +35,7 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.text')}</Text> <Text>{t('theme.sideMenu.chat.theme.text')}</Text>
<ColorPicker <ColorPicker
value={hostBubbles?.color ?? defaultTheme.chat.hostBubbles.color} value={hostBubbles?.color ?? defaultHostBubblesColor}
onColorChange={handleTextChange} onColorChange={handleTextChange}
/> />
</Flex> </Flex>

View File

@ -1,44 +0,0 @@
import { Stack, Flex, Text } from '@chakra-ui/react'
import { InputColors, Theme } from '@typebot.io/schemas'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
import { useTranslate } from '@tolgee/react'
type Props = {
inputs: NonNullable<Theme['chat']>['inputs']
onInputsChange: (buttons: InputColors) => void
}
export const InputsTheme = ({ inputs, onInputsChange }: Props) => {
const { t } = useTranslate()
const handleBackgroundChange = (backgroundColor: string) =>
onInputsChange({ ...inputs, backgroundColor })
const handleTextChange = (color: string) =>
onInputsChange({ ...inputs, color })
const handlePlaceholderChange = (placeholderColor: string) =>
onInputsChange({ ...inputs, placeholderColor })
return (
<Stack data-testid="inputs-theme">
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.background')}</Text>
<ColorPicker
value={inputs?.backgroundColor}
onColorChange={handleBackgroundChange}
/>
</Flex>
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.text')}</Text>
<ColorPicker value={inputs?.color} onColorChange={handleTextChange} />
</Flex>
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.placeholder')}</Text>
<ColorPicker
value={inputs?.placeholderColor}
onColorChange={handlePlaceholderChange}
/>
</Flex>
</Stack>
)
}

View File

@ -16,7 +16,8 @@ import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker' import { ColorPicker } from '../../../../components/ColorPicker'
import { import {
BackgroundType, BackgroundType,
defaultTheme, defaultBackgroundColor,
defaultBackgroundType,
} from '@typebot.io/schemas/features/typebot/theme/constants' } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react' import { useTranslate } from '@tolgee/react'
@ -34,10 +35,7 @@ export const BackgroundContent = ({
const handleContentChange = (content: string) => const handleContentChange = (content: string) =>
onBackgroundContentChange(content) onBackgroundContentChange(content)
if ( if ((background?.type ?? defaultBackgroundType) === BackgroundType.IMAGE) {
(background?.type ?? defaultTheme.general.background.type) ===
BackgroundType.IMAGE
) {
if (!typebot) return null if (!typebot) return null
return ( return (
<Popover isLazy placement="top"> <Popover isLazy placement="top">
@ -76,15 +74,12 @@ export const BackgroundContent = ({
</Popover> </Popover>
) )
} }
if ( if ((background?.type ?? defaultBackgroundType) === BackgroundType.COLOR) {
(background?.type ?? defaultTheme.general.background.type) ===
BackgroundType.COLOR
) {
return ( return (
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.global.background.color')}</Text> <Text>{t('theme.sideMenu.global.background.color')}</Text>
<ColorPicker <ColorPicker
value={background?.content ?? defaultTheme.general.background.content} value={background?.content ?? defaultBackgroundColor}
onColorChange={handleContentChange} onColorChange={handleContentChange}
/> />
</Flex> </Flex>

View File

@ -1,11 +1,11 @@
import { RadioButtons } from '@/components/inputs/RadioButtons' import { RadioButtons } from '@/components/inputs/RadioButtons'
import { Stack, Text } from '@chakra-ui/react' import { Stack } from '@chakra-ui/react'
import { Background } from '@typebot.io/schemas' import { Background } from '@typebot.io/schemas'
import React from 'react' import React from 'react'
import { BackgroundContent } from './BackgroundContent' import { BackgroundContent } from './BackgroundContent'
import { import {
BackgroundType, BackgroundType,
defaultTheme, defaultBackgroundType,
} from '@typebot.io/schemas/features/typebot/theme/constants' } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react' import { useTranslate } from '@tolgee/react'
@ -28,7 +28,6 @@ export const BackgroundSelector = ({
return ( return (
<Stack spacing={4}> <Stack spacing={4}>
<Text>{t('theme.sideMenu.global.background')}</Text>
<RadioButtons <RadioButtons
options={[ options={[
{ {
@ -44,7 +43,7 @@ export const BackgroundSelector = ({
value: BackgroundType.NONE, value: BackgroundType.NONE,
}, },
]} ]}
value={background?.type ?? defaultTheme.general.background.type} value={background?.type ?? defaultBackgroundType}
onSelect={handleBackgroundTypeChange} onSelect={handleBackgroundTypeChange}
/> />
<BackgroundContent <BackgroundContent

View File

@ -4,7 +4,11 @@ import {
Stack, Stack,
Switch, Switch,
useDisclosure, useDisclosure,
Text, Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import { Background, Font, ProgressBar, Theme } from '@typebot.io/schemas' import { Background, Font, ProgressBar, Theme } from '@typebot.io/schemas'
import React from 'react' import React from 'react'
@ -16,7 +20,7 @@ import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal' import { ChangePlanModal } from '@/features/billing/components/ChangePlanModal'
import { useTranslate } from '@tolgee/react' import { useTranslate } from '@tolgee/react'
import { import {
defaultTheme, defaultFontType,
fontTypes, fontTypes,
} from '@typebot.io/schemas/features/typebot/theme/constants' } from '@typebot.io/schemas/features/typebot/theme/constants'
import { trpc } from '@/lib/trpc' import { trpc } from '@/lib/trpc'
@ -91,7 +95,7 @@ export const GeneralSettings = ({
const fontType = const fontType =
(typeof generalTheme?.font === 'string' (typeof generalTheme?.font === 'string'
? 'Google' ? 'Google'
: generalTheme?.font?.type) ?? defaultTheme.general.font.type : generalTheme?.font?.type) ?? defaultFontType
return ( return (
<Stack spacing={6}> <Stack spacing={6}>
@ -115,23 +119,46 @@ export const GeneralSettings = ({
onChange={updateBranding} onChange={updateBranding}
/> />
</Flex> </Flex>
<ProgressBarForm <Accordion allowToggle>
progressBar={generalTheme?.progressBar} <AccordionItem>
onProgressBarChange={updateProgressBar} <AccordionButton justifyContent="space-between">
/> Progress Bar
<Stack> <AccordionIcon />
<Text>{t('theme.sideMenu.global.font')}</Text> </AccordionButton>
<RadioButtons <AccordionPanel>
options={fontTypes} <ProgressBarForm
defaultValue={fontType} progressBar={generalTheme?.progressBar}
onSelect={updateFontType} onProgressBarChange={updateProgressBar}
/> />
<FontForm font={generalTheme?.font} onFontChange={updateFont} /> </AccordionPanel>
</Stack> </AccordionItem>
<BackgroundSelector <AccordionItem>
background={generalTheme?.background} <AccordionButton justifyContent="space-between">
onBackgroundChange={handleBackgroundChange} {t('theme.sideMenu.global.font')}
/> <AccordionIcon />
</AccordionButton>
<AccordionPanel as={Stack}>
<RadioButtons
options={fontTypes}
defaultValue={fontType}
onSelect={updateFontType}
/>
<FontForm font={generalTheme?.font} onFontChange={updateFont} />
</AccordionPanel>
</AccordionItem>
<AccordionItem>
<AccordionButton justifyContent="space-between">
{t('theme.sideMenu.global.background')}
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
<BackgroundSelector
background={generalTheme?.background}
onBackgroundChange={handleBackgroundChange}
/>
</AccordionPanel>
</AccordionItem>
</Accordion>
</Stack> </Stack>
) )
} }

View File

@ -1,7 +1,7 @@
import { Select } from '@/components/inputs/Select' import { Select } from '@/components/inputs/Select'
import { env } from '@typebot.io/env' import { env } from '@typebot.io/env'
import { GoogleFont } from '@typebot.io/schemas' import { GoogleFont } from '@typebot.io/schemas'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants' import { defaultFontFamily } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
type Props = { type Props = {
@ -11,8 +11,7 @@ type Props = {
export const GoogleFontForm = ({ font, onFontChange }: Props) => { export const GoogleFontForm = ({ font, onFontChange }: Props) => {
const [currentFont, setCurrentFont] = useState( const [currentFont, setCurrentFont] = useState(
(typeof font === 'string' ? font : font?.family) ?? (typeof font === 'string' ? font : font?.family) ?? defaultFontFamily
defaultTheme.general.font.family
) )
const [googleFonts, setGoogleFonts] = useState<string[]>([]) const [googleFonts, setGoogleFonts] = useState<string[]>([])

View File

@ -5,7 +5,11 @@ import { NumberInput } from '@/components/inputs'
import { FormLabel, HStack } from '@chakra-ui/react' import { FormLabel, HStack } from '@chakra-ui/react'
import { ProgressBar } from '@typebot.io/schemas' import { ProgressBar } from '@typebot.io/schemas'
import { import {
defaultTheme, defaultProgressBarColor,
defaultProgressBarIsEnabled,
defaultProgressBarPlacement,
defaultProgressBarPosition,
defaultProgressBarThickness,
progressBarPlacements, progressBarPlacements,
progressBarPositions, progressBarPositions,
} from '@typebot.io/schemas/features/typebot/theme/constants' } from '@typebot.io/schemas/features/typebot/theme/constants'
@ -37,18 +41,14 @@ export const ProgressBarForm = ({
return ( return (
<SwitchWithRelatedSettings <SwitchWithRelatedSettings
label={'Enable progress bar?'} label={'Enable progress bar?'}
initialValue={ initialValue={progressBar?.isEnabled ?? defaultProgressBarIsEnabled}
progressBar?.isEnabled ?? defaultTheme.general.progressBar.isEnabled
}
onCheckChange={updateEnabled} onCheckChange={updateEnabled}
> >
<DropdownList <DropdownList
size="sm" size="sm"
direction="row" direction="row"
label="Placement:" label="Placement:"
currentItem={ currentItem={progressBar?.placement ?? defaultProgressBarPlacement}
progressBar?.placement ?? defaultTheme.general.progressBar.placement
}
onItemSelect={updatePlacement} onItemSelect={updatePlacement}
items={progressBarPlacements} items={progressBarPlacements}
/> />
@ -58,9 +58,7 @@ export const ProgressBarForm = ({
Color: Color:
</FormLabel> </FormLabel>
<ColorPicker <ColorPicker
defaultValue={ defaultValue={progressBar?.color ?? defaultProgressBarColor}
progressBar?.color ?? defaultTheme.general.progressBar.color
}
onColorChange={updateColor} onColorChange={updateColor}
/> />
</HStack> </HStack>
@ -69,9 +67,7 @@ export const ProgressBarForm = ({
direction="row" direction="row"
withVariableButton={false} withVariableButton={false}
maxW="100px" maxW="100px"
defaultValue={ defaultValue={progressBar?.thickness ?? defaultProgressBarThickness}
progressBar?.thickness ?? defaultTheme.general.progressBar.thickness
}
onValueChange={updateThickness} onValueChange={updateThickness}
size="sm" size="sm"
/> />
@ -80,9 +76,7 @@ export const ProgressBarForm = ({
direction="row" direction="row"
label="Position when embedded:" label="Position when embedded:"
moreInfoTooltip='Select "fixed" to always position the progress bar at the top of the window even though your bot is embedded. Select "absolute" to position the progress bar at the top of the chat container.' moreInfoTooltip='Select "fixed" to always position the progress bar at the top of the window even though your bot is embedded. Select "absolute" to position the progress bar at the top of the chat container.'
currentItem={ currentItem={progressBar?.position ?? defaultProgressBarPosition}
progressBar?.position ?? defaultTheme.general.progressBar.position
}
onItemSelect={updatePosition} onItemSelect={updatePosition}
items={progressBarPositions} items={progressBarPositions}
/> />

View File

@ -3,6 +3,10 @@ import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2' import { createId } from '@paralleldrive/cuid2'
import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions' import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
import { freeWorkspaceId } from '@typebot.io/playwright/databaseSetup' import { freeWorkspaceId } from '@typebot.io/playwright/databaseSetup'
import {
defaultContainerMaxHeight,
defaultContainerMaxWidth,
} from '@typebot.io/schemas/features/typebot/theme/constants'
const hostAvatarUrl = const hostAvatarUrl =
'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80' 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80'
@ -30,6 +34,7 @@ test.describe.parallel('Theme page', () => {
await expect(page.locator('a:has-text("Made with Typebot")')).toBeHidden() await expect(page.locator('a:has-text("Made with Typebot")')).toBeHidden()
// Font // Font
await page.getByRole('button', { name: 'Font' }).click()
await page.getByRole('textbox').fill('Roboto Slab') await page.getByRole('textbox').fill('Roboto Slab')
await page.getByRole('menuitem', { name: 'Roboto Slab' }).click() await page.getByRole('menuitem', { name: 'Roboto Slab' }).click()
await expect(page.locator('.typebot-container')).toHaveCSS( await expect(page.locator('.typebot-container')).toHaveCSS(
@ -42,6 +47,7 @@ test.describe.parallel('Theme page', () => {
'background-color', 'background-color',
'rgba(0, 0, 0, 0)' 'rgba(0, 0, 0, 0)'
) )
await page.getByRole('button', { name: 'Background' }).click()
await page.click('text=Color') await page.click('text=Color')
await page.waitForTimeout(100) await page.waitForTimeout(100)
await page.getByRole('button', { name: 'Pick a color' }).click() await page.getByRole('button', { name: 'Pick a color' }).click()
@ -82,6 +88,38 @@ test.describe.parallel('Theme page', () => {
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible() await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
await page.click('button:has-text("Chat")') await page.click('button:has-text("Chat")')
// Container
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'max-width',
defaultContainerMaxWidth
)
await page
.locator('div')
.filter({ hasText: /^Max width:px$/ })
.getByRole('spinbutton')
.fill('600')
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'max-width',
'600px'
)
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'max-height',
defaultContainerMaxHeight
)
await page
.locator('div')
.filter({ hasText: /^Max height:%$/ })
.getByRole('spinbutton')
.fill('80')
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'max-height',
'80%'
)
await expect(page.locator('.typebot-chat-view')).toHaveCSS(
'color',
'rgb(48, 50, 53)'
)
// Host avatar // Host avatar
await expect( await expect(
page.locator('[data-testid="default-avatar"]').nth(1) page.locator('[data-testid="default-avatar"]').nth(1)
@ -102,39 +140,13 @@ test.describe.parallel('Theme page', () => {
await expect(page.locator('.typebot-container img')).toBeHidden() await expect(page.locator('.typebot-container img')).toBeHidden()
// Roundness
await expect(page.getByRole('button', { name: 'Go' })).toHaveCSS(
'border-radius',
'6px'
)
await page
.getByRole('region', { name: 'Chat' })
.getByRole('radiogroup')
.locator('div')
.first()
.click()
await expect(page.getByRole('button', { name: 'Go' })).toHaveCSS(
'border-radius',
'0px'
)
await page
.getByRole('region', { name: 'Chat' })
.getByRole('radiogroup')
.locator('div')
.nth(2)
.click()
await expect(page.getByRole('button', { name: 'Go' })).toHaveCSS(
'border-radius',
'20px'
)
// Host bubbles // Host bubbles
await page.click( await page.click(
'[data-testid="host-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=0' '[data-testid="hostBubblesTheme"] >> [aria-label="Pick a color"] >> nth=0'
) )
await page.fill('input[value="#F7F8FF"]', '#2a9d8f') await page.fill('input[value="#F7F8FF"]', '#2a9d8f')
await page.click( await page.click(
'[data-testid="host-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1' '[data-testid="hostBubblesTheme"] >> [aria-label="Pick a color"] >> nth=1'
) )
await page.fill('input[value="#303235"]', '#ffffff') await page.fill('input[value="#303235"]', '#ffffff')
const hostBubble = page.locator('[data-testid="host-bubble"] >> nth=-1') const hostBubble = page.locator('[data-testid="host-bubble"] >> nth=-1')
@ -146,11 +158,11 @@ test.describe.parallel('Theme page', () => {
// Buttons // Buttons
await page.click( await page.click(
'[data-testid="buttons-theme"] >> [aria-label="Pick a color"] >> nth=0' '[data-testid="buttonsTheme"] >> [aria-label="Pick a color"] >> nth=0'
) )
await page.fill('input[value="#0042DA"]', '#7209b7') await page.fill('input[value="#0042DA"]', '#7209b7')
await page.click( await page.click(
'[data-testid="buttons-theme"] >> [aria-label="Pick a color"] >> nth=1' '[data-testid="buttonsTheme"] >> [aria-label="Pick a color"] >> nth=1'
) )
await page.fill('input[value="#FFFFFF"]', '#e9c46a') await page.fill('input[value="#FFFFFF"]', '#e9c46a')
const button = page.getByRole('button', { name: 'Go' }) const button = page.getByRole('button', { name: 'Go' })
@ -159,11 +171,11 @@ test.describe.parallel('Theme page', () => {
// Guest bubbles // Guest bubbles
await page.click( await page.click(
'[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=0' '[data-testid="guestBubblesTheme"] >> [aria-label="Pick a color"] >> nth=0'
) )
await page.fill('input[value="#FF8E21"]', '#d8f3dc') await page.fill('input[value="#FF8E21"]', '#d8f3dc')
await page.click( await page.click(
'[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1' '[data-testid="guestBubblesTheme"] >> [aria-label="Pick a color"] >> nth=1'
) )
await page.fill('input[value="#FFFFFF"]', '#264653') await page.fill('input[value="#FFFFFF"]', '#264653')
await page.getByRole('button', { name: 'Go' }).click() await page.getByRole('button', { name: 'Go' }).click()
@ -192,11 +204,11 @@ test.describe.parallel('Theme page', () => {
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
// Input // Input
await page.click( await page.click(
'[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=0' '[data-testid="inputsTheme"] >> [aria-label="Pick a color"] >> nth=0'
) )
await page.fill('input[value="#FFFFFF"]', '#ffe8d6') await page.fill('input[value="#FFFFFF"]', '#ffe8d6')
await page.click( await page.click(
'[data-testid="inputs-theme"] >> [aria-label="Pick a color"] >> nth=1' '[data-testid="inputsTheme"] >> [aria-label="Pick a color"] >> nth=1'
) )
await page.fill('input[value="#303235"]', '#023e8a') await page.fill('input[value="#303235"]', '#023e8a')
const input = page.locator('.typebot-input') const input = page.locator('.typebot-input')

View File

@ -21342,6 +21342,70 @@
"chat": { "chat": {
"type": "object", "type": "object",
"properties": { "properties": {
"container": {
"type": "object",
"properties": {
"maxWidth": {
"type": "string"
},
"maxHeight": {
"type": "string"
},
"backgroundColor": {
"type": "string"
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
"hostAvatar": { "hostAvatar": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -21372,6 +21436,53 @@
}, },
"color": { "color": {
"type": "string" "type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
} }
} }
}, },
@ -21383,6 +21494,53 @@
}, },
"color": { "color": {
"type": "string" "type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
} }
} }
}, },
@ -21394,6 +21552,53 @@
}, },
"color": { "color": {
"type": "string" "type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
} }
} }
}, },
@ -21406,6 +21611,53 @@
"color": { "color": {
"type": "string" "type": "string"
}, },
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
},
"placeholderColor": { "placeholderColor": {
"type": "string" "type": "string"
} }
@ -21417,7 +21669,8 @@
"none", "none",
"medium", "medium",
"large" "large"
] ],
"description": "Deprecated, use `container.border.roundeness` instead"
} }
} }
}, },

View File

@ -6821,6 +6821,70 @@
"chat": { "chat": {
"type": "object", "type": "object",
"properties": { "properties": {
"container": {
"type": "object",
"properties": {
"maxWidth": {
"type": "string"
},
"maxHeight": {
"type": "string"
},
"backgroundColor": {
"type": "string"
},
"color": {
"type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
}
}
},
"hostAvatar": { "hostAvatar": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -6851,6 +6915,53 @@
}, },
"color": { "color": {
"type": "string" "type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
} }
} }
}, },
@ -6862,6 +6973,53 @@
}, },
"color": { "color": {
"type": "string" "type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
} }
} }
}, },
@ -6873,6 +7031,53 @@
}, },
"color": { "color": {
"type": "string" "type": "string"
},
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
} }
} }
}, },
@ -6885,6 +7090,53 @@
"color": { "color": {
"type": "string" "type": "string"
}, },
"blur": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"shadow": {
"type": "string",
"enum": [
"none",
"sm",
"md",
"lg",
"xl",
"2xl"
]
},
"border": {
"type": "object",
"properties": {
"thickness": {
"type": "number"
},
"color": {
"type": "string"
},
"roundeness": {
"type": "string",
"enum": [
"none",
"medium",
"large",
"custom"
]
},
"customRoundeness": {
"type": "number"
},
"opacity": {
"type": "number",
"minimum": 0,
"maximum": 1
}
}
},
"placeholderColor": { "placeholderColor": {
"type": "string" "type": "string"
} }
@ -6896,7 +7148,8 @@
"none", "none",
"medium", "medium",
"large" "large"
] ],
"description": "Deprecated, use `container.border.roundeness` instead"
} }
} }
}, },

View File

@ -7,8 +7,11 @@ import { TypebotPageProps, TypebotPageV2 } from '@/components/TypebotPageV2'
import { TypebotPageV3, TypebotV3PageProps } from '@/components/TypebotPageV3' import { TypebotPageV3, TypebotV3PageProps } from '@/components/TypebotPageV3'
import { env } from '@typebot.io/env' import { env } from '@typebot.io/env'
import prisma from '@typebot.io/lib/prisma' import prisma from '@typebot.io/lib/prisma'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants' import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import {
defaultBackgroundColor,
defaultBackgroundType,
} from '@typebot.io/schemas/features/typebot/theme/constants'
// Browsers that doesn't support ES modules and/or web components // Browsers that doesn't support ES modules and/or web components
const incompatibleBrowsers = [ const incompatibleBrowsers = [
@ -109,9 +112,10 @@ const getTypebotFromPublicId = async (publicId?: string) => {
? ({ ? ({
name: publishedTypebot.typebot.name, name: publishedTypebot.typebot.name,
publicId: publishedTypebot.typebot.publicId ?? null, publicId: publishedTypebot.typebot.publicId ?? null,
background: background: publishedTypebot.theme.general?.background ?? {
publishedTypebot.theme.general?.background ?? type: defaultBackgroundType,
defaultTheme.general.background, content: defaultBackgroundColor,
},
isHideQueryParamsEnabled: isHideQueryParamsEnabled:
publishedTypebot.settings.general?.isHideQueryParamsEnabled ?? publishedTypebot.settings.general?.isHideQueryParamsEnabled ??
defaultSettings.general.isHideQueryParamsEnabled, defaultSettings.general.isHideQueryParamsEnabled,
@ -156,9 +160,10 @@ const getTypebotFromCustomDomain = async (customDomain: string) => {
? ({ ? ({
name: publishedTypebot.typebot.name, name: publishedTypebot.typebot.name,
publicId: publishedTypebot.typebot.publicId ?? null, publicId: publishedTypebot.typebot.publicId ?? null,
background: background: publishedTypebot.theme.general?.background ?? {
publishedTypebot.theme.general?.background ?? type: defaultBackgroundType,
defaultTheme.general.background, content: defaultBackgroundColor,
},
isHideQueryParamsEnabled: isHideQueryParamsEnabled:
publishedTypebot.settings.general?.isHideQueryParamsEnabled ?? publishedTypebot.settings.general?.isHideQueryParamsEnabled ??
defaultSettings.general.isHideQueryParamsEnabled, defaultSettings.general.isHideQueryParamsEnabled,
@ -233,7 +238,10 @@ const App = ({
defaultSettings.general.isHideQueryParamsEnabled defaultSettings.general.isHideQueryParamsEnabled
} }
background={ background={
publishedTypebot.background ?? defaultTheme.general.background publishedTypebot.background ?? {
type: defaultBackgroundType,
content: defaultBackgroundColor,
}
} }
metadata={publishedTypebot.metadata ?? {}} metadata={publishedTypebot.metadata ?? {}}
font={publishedTypebot.font} font={publishedTypebot.font}

View File

@ -34,11 +34,14 @@ import { continueBotFlow } from './continueBotFlow'
import { parseVariables } from '@typebot.io/variables/parseVariables' import { parseVariables } from '@typebot.io/variables/parseVariables'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants' import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants' import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { VisitedEdge } from '@typebot.io/prisma' import { VisitedEdge } from '@typebot.io/prisma'
import { env } from '@typebot.io/env' import { env } from '@typebot.io/env'
import { getFirstEdgeId } from './getFirstEdgeId' import { getFirstEdgeId } from './getFirstEdgeId'
import { Reply } from './types' import { Reply } from './types'
import {
defaultGuestAvatarIsEnabled,
defaultHostAvatarIsEnabled,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type StartParams = type StartParams =
| ({ | ({
@ -385,12 +388,11 @@ const getResult = async ({
const parseDynamicThemeInState = (theme: Theme) => { const parseDynamicThemeInState = (theme: Theme) => {
const hostAvatarUrl = const hostAvatarUrl =
theme.chat?.hostAvatar?.isEnabled ?? defaultTheme.chat.hostAvatar.isEnabled theme.chat?.hostAvatar?.isEnabled ?? defaultHostAvatarIsEnabled
? theme.chat?.hostAvatar?.url ? theme.chat?.hostAvatar?.url
: undefined : undefined
const guestAvatarUrl = const guestAvatarUrl =
theme.chat?.guestAvatar?.isEnabled ?? theme.chat?.guestAvatar?.isEnabled ?? defaultGuestAvatarIsEnabled
defaultTheme.chat.guestAvatar.isEnabled
? theme.chat?.guestAvatar?.url ? theme.chat?.guestAvatar?.url
: undefined : undefined
if (!hostAvatarUrl?.startsWith('{{') && !guestAvatarUrl?.startsWith('{{')) if (!hostAvatarUrl?.startsWith('{{') && !guestAvatarUrl?.startsWith('{{'))

View File

@ -1,9 +1,9 @@
import { import {
Background, Background,
ChatTheme, ChatTheme,
ContainerColors, ContainerTheme,
GeneralTheme, GeneralTheme,
InputColors, InputTheme,
Theme, Theme,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { BackgroundType } from '@typebot.io/schemas/features/typebot/theme/constants' import { BackgroundType } from '@typebot.io/schemas/features/typebot/theme/constants'
@ -66,7 +66,7 @@ const setChatTheme = (
} }
const setHostBubbles = ( const setHostBubbles = (
hostBubbles: ContainerColors, hostBubbles: ContainerTheme,
documentStyle: CSSStyleDeclaration documentStyle: CSSStyleDeclaration
) => { ) => {
if (hostBubbles.backgroundColor) if (hostBubbles.backgroundColor)
@ -82,7 +82,7 @@ const setHostBubbles = (
} }
const setGuestBubbles = ( const setGuestBubbles = (
guestBubbles: ContainerColors, guestBubbles: any,
documentStyle: CSSStyleDeclaration documentStyle: CSSStyleDeclaration
) => { ) => {
if (guestBubbles.backgroundColor) if (guestBubbles.backgroundColor)
@ -98,7 +98,7 @@ const setGuestBubbles = (
} }
const setButtons = ( const setButtons = (
buttons: ContainerColors, buttons: ContainerTheme,
documentStyle: CSSStyleDeclaration documentStyle: CSSStyleDeclaration
) => { ) => {
if (buttons.backgroundColor) if (buttons.backgroundColor)
@ -113,7 +113,7 @@ const setButtons = (
) )
} }
const setInputs = (inputs: InputColors, documentStyle: CSSStyleDeclaration) => { const setInputs = (inputs: InputTheme, documentStyle: CSSStyleDeclaration) => {
if (inputs.backgroundColor) if (inputs.backgroundColor)
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.chat.inputs.bgColor, cssVariableNames.chat.inputs.bgColor,

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/js", "name": "@typebot.io/js",
"version": "0.2.64", "version": "0.2.65",
"description": "Javascript library to display typebots on your website", "description": "Javascript library to display typebots on your website",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
@ -31,6 +31,7 @@
"@typebot.io/env": "workspace:*", "@typebot.io/env": "workspace:*",
"@typebot.io/lib": "workspace:*", "@typebot.io/lib": "workspace:*",
"@typebot.io/schemas": "workspace:*", "@typebot.io/schemas": "workspace:*",
"@typebot.io/theme": "workspace:*",
"@typebot.io/tsconfig": "workspace:*", "@typebot.io/tsconfig": "workspace:*",
"@types/dompurify": "3.0.3", "@types/dompurify": "3.0.3",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",

View File

@ -2,47 +2,6 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
:host {
--typebot-container-bg-image: none;
--typebot-container-bg-color: transparent;
--typebot-container-font-family: 'Open Sans';
--typebot-container-color: #303235;
--typebot-button-bg-color: #0042da;
--typebot-button-bg-color-rgb: 0, 66, 218;
--typebot-button-color: #ffffff;
--typebot-checkbox-bg-color: #ffffff;
--typebot-host-bubble-bg-color: #f7f8ff;
--typebot-host-bubble-color: #303235;
--typebot-guest-bubble-bg-color: #ff8e21;
--typebot-guest-bubble-color: #ffffff;
--typebot-input-bg-color: #ffffff;
--typebot-input-color: #303235;
--typebot-input-placeholder-color: #9095a0;
--typebot-header-bg-color: #ffffff;
--typebot-header-color: #303235;
--selectable-base-alpha: 0;
--typebot-border-radius: 6px;
--typebot-progress-bar-position: fixed;
--typebot-progress-bar-bg-color: #f7f8ff;
--typebot-progress-bar-color: #0042da;
--typebot-progress-bar-height: 6px;
--typebot-progress-bar-top: 0;
--typebot-progress-bar-bottom: auto;
/* Phone input */
--PhoneInputCountryFlag-borderColor: transparent;
--PhoneInput-color--focus: transparent;
}
/* Hide scrollbar for Chrome, Safari and Opera */ /* Hide scrollbar for Chrome, Safari and Opera */
.scrollable-container::-webkit-scrollbar { .scrollable-container::-webkit-scrollbar {
display: none; display: none;
@ -161,67 +120,100 @@ pre {
.typebot-container { .typebot-container {
background-image: var(--typebot-container-bg-image); background-image: var(--typebot-container-bg-image);
background-color: var(--typebot-container-bg-color); background-color: var(--typebot-container-bg-color);
background-position: center;
background-size: cover;
font-family: var(--typebot-container-font-family), -apple-system, font-family: var(--typebot-container-font-family), -apple-system,
BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
container-type: inline-size;
}
.typebot-chat-view {
max-width: var(--typebot-chat-container-max-width);
background-color: rgba(
var(--typebot-chat-container-bg-rgb),
var(--typebot-chat-container-opacity)
);
color: rgb(var(--typebot-chat-container-color));
min-height: 100%;
backdrop-filter: blur(var(--typebot-chat-container-blur));
border-width: var(--typebot-chat-container-border-width);
border-color: rgba(
var(--typebot-chat-container-border-rgb),
var(--typebot-chat-container-border-opacity)
);
padding-left: 20px;
padding-right: 20px;
box-shadow: var(--typebot-chat-container-box-shadow);
}
@container (min-width: 480px) {
.typebot-chat-view {
min-height: var(--typebot-chat-container-max-height);
max-height: var(--typebot-chat-container-max-height);
border-radius: var(--typebot-chat-container-border-radius);
}
} }
.typebot-button { .typebot-button {
color: var(--typebot-button-color); color: var(--typebot-button-color);
background-color: var(--typebot-button-bg-color); background-color: rgba(
border: 1px solid var(--typebot-button-bg-color); var(--typebot-button-bg-rgb),
border-radius: var(--typebot-border-radius); var(--typebot-button-opacity)
);
border-width: var(--typebot-button-border-width);
border-color: rgba(
var(--typebot-button-border-rgb),
var(--typebot-button-border-opacity)
);
border-radius: var(--typebot-button-border-radius);
box-shadow: var(--typebot-button-box-shadow);
backdrop-filter: blur(var(--typebot-button-blur));
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.typebot-button.selectable {
color: var(--typebot-host-bubble-color);
background-color: var(--typebot-host-bubble-bg-color);
border: 1px solid var(--typebot-button-bg-color);
}
.typebot-selectable { .typebot-selectable {
border: 1px solid border-width: var(--typebot-button-border-width);
rgba( border-color: rgba(
var(--typebot-button-bg-color-rgb), var(--typebot-button-border-rgb),
calc(var(--selectable-base-alpha) + 0.25) calc(var(--selectable-alpha-ratio) * 0.25)
); );
border-radius: var(--typebot-border-radius); border-radius: var(--typebot-button-border-radius);
color: var(--typebot-container-color); color: rgb(var(--typebot-chat-container-color));
background-color: rgba( background-color: rgba(
var(--typebot-button-bg-color-rgb), var(--typebot-button-bg-rgb),
calc(var(--selectable-base-alpha) + 0.08) calc(var(--selectable-alpha-ratio) * 0.08)
); );
transition: all 0.3s ease; transition: all 0.3s ease;
backdrop-filter: blur(2px);
} }
.typebot-selectable:hover { .typebot-selectable:hover {
background-color: rgba( background-color: rgba(
var(--typebot-button-bg-color-rgb), var(--typebot-button-bg-rgb),
calc(var(--selectable-base-alpha) + 0.12) calc(var(--selectable-alpha-ratio) * 0.12)
); );
border-color: rgba( border-color: rgba(
var(--typebot-button-bg-color-rgb), var(--typebot-button-border-rgb),
calc(var(--selectable-base-alpha) + 0.3) calc(var(--selectable-alpha-ratio) * 0.3)
); );
} }
.typebot-selectable.selected { .typebot-selectable.selected {
background-color: rgba( background-color: rgba(
var(--typebot-button-bg-color-rgb), var(--typebot-button-bg-rgb),
calc(var(--selectable-base-alpha) + 0.18) calc(var(--selectable-alpha-ratio) * 0.18)
); );
border-color: rgba( border-color: rgba(
var(--typebot-button-bg-color-rgb), var(--typebot-button-border-rgb),
calc(var(--selectable-base-alpha) + 0.35) calc(var(--selectable-alpha-ratio) * 0.35)
); );
} }
.typebot-checkbox { .typebot-checkbox {
border: 1px solid var(--typebot-button-bg-color); border: 1px solid
border-radius: var(--typebot-border-radius); rgba(var(--typebot-button-bg-rgb), var(--typebot-button-opacity));
background-color: var(--typebot-checkbox-bg-color); border-radius: var(--typebot-button-border-radius);
background-color: rgba(var(--typebot-checkbox-bg-rgb));
color: var(--typebot-button-color); color: var(--typebot-button-color);
padding: 1px; padding: 1px;
border-radius: 2px; border-radius: 2px;
@ -229,7 +221,7 @@ pre {
} }
.typebot-checkbox.checked { .typebot-checkbox.checked {
background-color: var(--typebot-button-bg-color); background-color: rgb(var(--typebot-button-bg-rgb));
} }
.typebot-host-bubble { .typebot-host-bubble {
@ -237,22 +229,56 @@ pre {
} }
.typebot-host-bubble > .bubble-typing { .typebot-host-bubble > .bubble-typing {
background-color: var(--typebot-host-bubble-bg-color); background-color: rgba(
border: var(--typebot-host-bubble-border); var(--typebot-host-bubble-bg-rgb),
var(--typebot-host-bubble-opacity)
);
border-width: var(--typebot-host-bubble-border-width);
border-color: rgba(
var(--typebot-host-bubble-border-rgb),
var(--typebot-host-bubble-border-opacity)
);
border-radius: var(--typebot-host-bubble-border-radius);
box-shadow: var(--typebot-host-bubble-box-shadow);
backdrop-filter: blur(var(--typebot-host-bubble-blur));
}
.typebot-host-bubble img,
.typebot-host-bubble video,
.typebot-host-bubble iframe {
border-radius: 6px; border-radius: 6px;
} }
.typebot-guest-bubble { .typebot-guest-bubble {
color: var(--typebot-guest-bubble-color); color: var(--typebot-guest-bubble-color);
background-color: var(--typebot-guest-bubble-bg-color); background-color: rgba(
border-radius: 6px; var(--typebot-guest-bubble-bg-rgb),
var(--typebot-guest-bubble-opacity)
);
border-width: var(--typebot-guest-bubble-border-width);
border-color: rgba(
var(--typebot-guest-bubble-border-rgb),
var(--typebot-guest-bubble-border-opacity)
);
border-radius: var(--typebot-guest-bubble-border-radius);
box-shadow: var(--typebot-guest-bubble-box-shadow);
backdrop-filter: blur(var(--typebot-guest-bubble-blur));
} }
.typebot-input { .typebot-input {
color: var(--typebot-input-color); color: var(--typebot-input-color);
background-color: var(--typebot-input-bg-color); background-color: rgba(
box-shadow: 0 2px 6px -1px rgba(0, 0, 0, 0.1); var(--typebot-input-bg-rgb),
border-radius: var(--typebot-border-radius); var(--typebot-input-opacity)
);
border-width: var(--typebot-input-border-width);
border-color: rgba(
var(--typebot-input-border-rgb),
var(--typebot-input-border-opacity)
);
border-radius: var(--typebot-input-border-radius);
box-shadow: var(--typebot-input-box-shadow);
backdrop-filter: blur(var(--typebot-input-blur));
} }
.typebot-input-error-message { .typebot-input-error-message {
@ -263,24 +289,20 @@ pre {
fill: var(--typebot-button-color); fill: var(--typebot-button-color);
} }
.typebot-chat-view {
max-width: 900px;
}
.ping span { .ping span {
background-color: var(--typebot-button-bg-color); background-color: rgb(var(--typebot-button-bg-rgb));
} }
.rating-icon-container svg { .rating-icon-container svg {
width: 42px; width: 42px;
height: 42px; height: 42px;
stroke: var(--typebot-button-bg-color); stroke: rgb(var(--typebot-button-bg-rgb));
fill: var(--typebot-host-bubble-bg-color); fill: var(--typebot-host-bubble-bg-color);
transition: fill 100ms ease-out; transition: fill 100ms ease-out;
} }
.rating-icon-container.selected svg { .rating-icon-container.selected svg {
fill: var(--typebot-button-bg-color); fill: rgb(var(--typebot-button-bg-rgb));
} }
.rating-icon-container:hover svg { .rating-icon-container:hover svg {
@ -292,59 +314,60 @@ pre {
} }
.upload-progress-bar { .upload-progress-bar {
background-color: var(--typebot-button-bg-color); background-color: rgb(var(--typebot-button-bg-rgb));
border-radius: var(--typebot-border-radius); border-radius: var(--typebot-input-border-radius);
} }
.total-files-indicator { .total-files-indicator {
background-color: var(--typebot-button-bg-color); background-color: rgb(var(--typebot-button-bg-rgb));
color: var(--typebot-button-color); color: var(--typebot-button-color);
font-size: 10px; font-size: 10px;
} }
.typebot-upload-input { .typebot-upload-input {
transition: border-color 100ms ease-out; transition: border-color 100ms ease-out;
border-radius: var(--typebot-border-radius); border-radius: var(--typebot-input-border-radius);
} }
.typebot-upload-input.dragging-over { .typebot-upload-input.dragging-over {
border-color: var(--typebot-button-bg-color); border-color: rgb(var(--typebot-button-bg-rgb));
} }
.secondary-button { .secondary-button {
background-color: var(--typebot-host-bubble-bg-color); background-color: var(--typebot-host-bubble-bg-color);
color: var(--typebot-host-bubble-color); color: var(--typebot-host-bubble-color);
border-radius: var(--typebot-border-radius); border-radius: var(--typebot-button-border-radius);
} }
.typebot-country-select { .typebot-country-select {
color: var(--typebot-input-color); color: var(--typebot-input-color);
background-color: var(--typebot-input-bg-color); background-color: var(--typebot-input-bg-color);
border-radius: var(--typebot-border-radius); border-radius: var(--typebot-button-border-radius);
} }
.typebot-date-input { .typebot-date-input {
color-scheme: light; color-scheme: light;
color: var(--typebot-input-color); color: var(--typebot-input-color);
background-color: var(--typebot-input-bg-color); background-color: var(--typebot-input-bg-color);
border-radius: var(--typebot-border-radius); border-radius: var(--typebot-input-border-radius);
} }
.typebot-popup-blocked-toast { .typebot-popup-blocked-toast {
border-radius: var(--typebot-border-radius); border-radius: var(--typebot-input-border-radius);
} }
.typebot-picture-button { .typebot-picture-button {
color: var(--typebot-button-color); color: var(--typebot-button-color);
background-color: var(--typebot-button-bg-color); background-color: rgb(var(--typebot-button-bg-rgb));
border-radius: var(--typebot-border-radius); border-radius: var(--typebot-button-border-radius);
transition: all 0.3s ease; transition: all 0.3s ease;
width: 236px; width: 236px;
} }
.typebot-picture-button > img, .typebot-picture-button > img,
.typebot-selectable-picture > img { .typebot-selectable-picture > img {
border-radius: var(--typebot-border-radius) var(--typebot-border-radius) 0 0; border-radius: var(--typebot-button-border-radius)
var(--typebot-button-border-radius) 0 0;
min-width: 200px; min-width: 200px;
width: 100%; width: 100%;
max-height: 200px; max-height: 200px;
@ -362,14 +385,14 @@ pre {
.typebot-selectable-picture { .typebot-selectable-picture {
border: 1px solid border: 1px solid
rgba( rgba(
var(--typebot-button-bg-color-rgb), var(--typebot-button-bg-rgb),
calc(var(--selectable-base-alpha) + 0.25) calc(var(--selectable-alpha-ratio) * 0.25)
); );
border-radius: var(--typebot-border-radius); border-radius: var(--typebot-button-border-radius);
color: var(--typebot-container-color); color: rgb(var(--typebot-chat-container-color));
background-color: rgba( background-color: rgba(
var(--typebot-button-bg-color-rgb), var(--typebot-button-bg-rgb),
calc(var(--selectable-base-alpha) + 0.08) calc(var(--selectable-alpha-ratio) * 0.08)
); );
transition: all 0.3s ease; transition: all 0.3s ease;
width: 236px; width: 236px;
@ -377,23 +400,23 @@ pre {
.typebot-selectable-picture:hover { .typebot-selectable-picture:hover {
background-color: rgba( background-color: rgba(
var(--typebot-button-bg-color-rgb), var(--typebot-button-bg-rgb),
calc(var(--selectable-base-alpha) + 0.12) calc(var(--selectable-alpha-ratio) * 0.12)
); );
border-color: rgba( border-color: rgba(
var(--typebot-button-bg-color-rgb), var(--typebot-button-bg-rgb),
calc(var(--selectable-base-alpha) + 0.3) calc(var(--selectable-alpha-ratio) * 0.3)
); );
} }
.typebot-selectable-picture.selected { .typebot-selectable-picture.selected {
background-color: rgba( background-color: rgba(
var(--typebot-button-bg-color-rgb), var(--typebot-button-bg-rgb),
calc(var(--selectable-base-alpha) + 0.18) calc(var(--selectable-alpha-ratio) * 0.18)
); );
border-color: rgba( border-color: rgba(
var(--typebot-button-bg-color-rgb), var(--typebot-button-bg-rgb),
calc(var(--selectable-base-alpha) + 0.35) calc(var(--selectable-alpha-ratio) * 0.35)
); );
} }
@ -404,8 +427,8 @@ select option {
.typebot-progress-bar-container { .typebot-progress-bar-container {
background-color: rgba( background-color: rgba(
var(--typebot-button-bg-color-rgb), var(--typebot-button-bg-rgb),
calc(var(--selectable-base-alpha) + 0.12) calc(var(--selectable-alpha-ratio) * 0.12)
); );
height: var(--typebot-progress-bar-height); height: var(--typebot-progress-bar-height);

View File

@ -16,7 +16,6 @@ import {
import { setCssVariablesValue } from '@/utils/setCssVariablesValue' import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
import immutableCss from '../assets/immutable.css' import immutableCss from '../assets/immutable.css'
import { Font, InputBlock, StartFrom } from '@typebot.io/schemas' import { Font, InputBlock, StartFrom } from '@typebot.io/schemas'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { HTTPError } from 'ky' import { HTTPError } from 'ky'
import { injectFont } from '@/utils/injectFont' import { injectFont } from '@/utils/injectFont'
@ -25,6 +24,11 @@ import { Portal } from 'solid-js/web'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants' import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import { persist } from '@/utils/persist' import { persist } from '@/utils/persist'
import { setBotContainerHeight } from '@/utils/botContainerHeightSignal' import { setBotContainerHeight } from '@/utils/botContainerHeightSignal'
import {
defaultFontFamily,
defaultFontType,
defaultProgressBarPosition,
} from '@typebot.io/schemas/features/typebot/theme/constants'
export type BotProps = { export type BotProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -262,8 +266,10 @@ const BotContent = (props: BotContentProps) => {
createEffect(() => { createEffect(() => {
injectFont( injectFont(
props.initialChatReply.typebot.theme.general?.font ?? props.initialChatReply.typebot.theme.general?.font ?? {
defaultTheme.general.font type: defaultFontType,
family: defaultFontFamily,
}
) )
if (!botContainer) return if (!botContainer) return
setCssVariablesValue( setCssVariablesValue(
@ -282,7 +288,7 @@ const BotContent = (props: BotContentProps) => {
<div <div
ref={botContainer} ref={botContainer}
class={clsx( class={clsx(
'relative flex w-full h-full text-base overflow-hidden bg-cover bg-center flex-col items-center typebot-container @container', 'relative flex w-full h-full text-base overflow-hidden flex-col justify-center items-center typebot-container',
props.class props.class
)} )}
> >
@ -296,8 +302,7 @@ const BotContent = (props: BotContentProps) => {
when={ when={
props.progressBarRef && props.progressBarRef &&
(props.initialChatReply.typebot.theme.general?.progressBar (props.initialChatReply.typebot.theme.general?.progressBar
?.position ?? defaultTheme.general.progressBar.position) === ?.position ?? defaultProgressBarPosition) === 'fixed'
'fixed'
} }
fallback={<ProgressBar value={progressValue() as number} />} fallback={<ProgressBar value={progressValue() as number} />}
> >
@ -306,7 +311,7 @@ const BotContent = (props: BotContentProps) => {
</Portal> </Portal>
</Show> </Show>
</Show> </Show>
<div class="flex w-full h-full justify-center"> <div class="flex w-full h-full justify-center items-center">
<ConversationContainer <ConversationContainer
context={props.context} context={props.context}
initialChatReply={props.initialChatReply} initialChatReply={props.initialChatReply}

View File

@ -6,8 +6,11 @@ import { HostBubble } from '../bubbles/HostBubble'
import { InputChatBlock } from '../InputChatBlock' import { InputChatBlock } from '../InputChatBlock'
import { AvatarSideContainer } from './AvatarSideContainer' import { AvatarSideContainer } from './AvatarSideContainer'
import { StreamingBubble } from '../bubbles/StreamingBubble' import { StreamingBubble } from '../bubbles/StreamingBubble'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants' import { defaultSettings } from '@typebot.io/schemas/features/typebot/settings/constants'
import {
defaultGuestAvatarIsEnabled,
defaultHostAvatarIsEnabled,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = Pick<ContinueChatResponse, 'messages' | 'input'> & { type Props = Pick<ContinueChatResponse, 'messages' | 'input'> & {
theme: Theme theme: Theme
@ -77,7 +80,7 @@ export const ChatChunk = (props: Props) => {
<Show <Show
when={ when={
(props.theme.chat?.hostAvatar?.isEnabled ?? (props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled) && defaultHostAvatarIsEnabled) &&
props.messages.length > 0 props.messages.length > 0
} }
> >
@ -93,7 +96,7 @@ export const ChatChunk = (props: Props) => {
style={{ style={{
'max-width': 'max-width':
props.theme.chat?.guestAvatar?.isEnabled ?? props.theme.chat?.guestAvatar?.isEnabled ??
defaultTheme.chat.guestAvatar.isEnabled defaultGuestAvatarIsEnabled
? isMobile() ? isMobile()
? 'calc(100% - 32px - 32px)' ? 'calc(100% - 32px - 32px)'
: 'calc(100% - 48px - 48px)' : 'calc(100% - 48px - 48px)'
@ -131,7 +134,7 @@ export const ChatChunk = (props: Props) => {
chunkIndex={props.index} chunkIndex={props.index}
hasHostAvatar={ hasHostAvatar={
props.theme.chat?.hostAvatar?.isEnabled ?? props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled defaultHostAvatarIsEnabled
} }
guestAvatar={props.theme.chat?.guestAvatar} guestAvatar={props.theme.chat?.guestAvatar}
context={props.context} context={props.context}
@ -151,7 +154,7 @@ export const ChatChunk = (props: Props) => {
<Show <Show
when={ when={
props.theme.chat?.hostAvatar?.isEnabled ?? props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled defaultHostAvatarIsEnabled
} }
> >
<AvatarSideContainer <AvatarSideContainer
@ -165,7 +168,7 @@ export const ChatChunk = (props: Props) => {
style={{ style={{
'max-width': 'max-width':
props.theme.chat?.hostAvatar?.isEnabled ?? props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled defaultHostAvatarIsEnabled
? isMobile() ? isMobile()
? 'calc(100% - 32px - 32px)' ? 'calc(100% - 32px - 32px)'
: 'calc(100% - 48px - 48px)' : 'calc(100% - 48px - 48px)'

View File

@ -284,7 +284,7 @@ export const ConversationContainer = (props: Props) => {
return ( return (
<div <div
ref={chatContainer} ref={chatContainer}
class="flex flex-col overflow-y-auto w-full min-h-full px-3 pt-10 relative scrollable-container typebot-chat-view scroll-smooth gap-2" class="flex flex-col overflow-y-auto w-full px-3 pt-10 relative scrollable-container typebot-chat-view scroll-smooth gap-2"
> >
<For each={chatChunks()}> <For each={chatChunks()}>
{(chatChunk, index) => ( {(chatChunk, index) => (

View File

@ -2,7 +2,7 @@ import { Theme } from '@typebot.io/schemas'
import { Show } from 'solid-js' import { Show } from 'solid-js'
import { LoadingBubble } from '../bubbles/LoadingBubble' import { LoadingBubble } from '../bubbles/LoadingBubble'
import { AvatarSideContainer } from './AvatarSideContainer' import { AvatarSideContainer } from './AvatarSideContainer'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants' import { defaultHostAvatarIsEnabled } from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = { type Props = {
theme: Theme theme: Theme
@ -15,7 +15,7 @@ export const LoadingChunk = (props: Props) => (
<Show <Show
when={ when={
props.theme.chat?.hostAvatar?.isEnabled ?? props.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled defaultHostAvatarIsEnabled
} }
> >
<AvatarSideContainer <AvatarSideContainer

View File

@ -35,8 +35,8 @@ import { MultiplePictureChoice } from '@/features/blocks/inputs/pictureChoice/Mu
import { formattedMessages } from '@/utils/formattedMessagesSignal' import { formattedMessages } from '@/utils/formattedMessagesSignal'
import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants' import { InputBlockType } from '@typebot.io/schemas/features/blocks/inputs/constants'
import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants' import { defaultPaymentInputOptions } from '@typebot.io/schemas/features/blocks/inputs/payment/constants'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { persist } from '@/utils/persist' import { persist } from '@/utils/persist'
import { defaultGuestAvatarIsEnabled } from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = { type Props = {
ref: HTMLDivElement | undefined ref: HTMLDivElement | undefined
@ -82,8 +82,7 @@ export const InputChatBlock = (props: Props) => {
<GuestBubble <GuestBubble
message={formattedMessage() ?? (answer() as string)} message={formattedMessage() ?? (answer() as string)}
showAvatar={ showAvatar={
props.guestAvatar?.isEnabled ?? props.guestAvatar?.isEnabled ?? defaultGuestAvatarIsEnabled
defaultTheme.chat.guestAvatar.isEnabled
} }
avatarSrc={props.guestAvatar?.url && props.guestAvatar.url} avatarSrc={props.guestAvatar?.url && props.guestAvatar.url}
/> />

View File

@ -1,7 +1,7 @@
import { SendButton } from '@/components/SendButton' import { SendButton } from '@/components/SendButton'
import { InputSubmitContent } from '@/types' import { InputSubmitContent } from '@/types'
import type { RatingInputBlock } from '@typebot.io/schemas' import type { RatingInputBlock } from '@typebot.io/schemas'
import { createSignal, For, Match, Switch } from 'solid-js' import { createSignal, For, Match, Switch, Show } from 'solid-js'
import { isDefined, isEmpty, isNotDefined } from '@typebot.io/lib' import { isDefined, isEmpty, isNotDefined } from '@typebot.io/lib'
import { Button } from '@/components/Button' import { Button } from '@/components/Button'
import { defaultRatingInputOptions } from '@typebot.io/schemas/features/blocks/inputs/rating/constants' import { defaultRatingInputOptions } from '@typebot.io/schemas/features/blocks/inputs/rating/constants'
@ -107,17 +107,24 @@ const RatingButton = (props: RatingButtonProps) => {
'Numbers' 'Numbers'
} }
> >
<Button <Show when={props.isOneClickSubmitEnabled}>
on:click={handleClick} <Button on:click={handleClick}>{props.idx}</Button>
class={ </Show>
props.isOneClickSubmitEnabled || <Show when={!props.isOneClickSubmitEnabled}>
(isDefined(props.rating) && props.idx <= props.rating) <div
? '' role="checkbox"
: 'selectable' aria-checked={isDefined(props.rating) && props.idx <= props.rating}
} on:click={handleClick}
> class={
{props.idx} 'py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable' +
</Button> (isDefined(props.rating) && props.idx <= props.rating
? ' selected'
: '')
}
>
{props.idx}
</div>
</Show>
</Match> </Match>
<Match <Match
when={ when={

View File

@ -6,6 +6,7 @@ import { isMobile } from '@/utils/isMobileSignal'
import type { TextInputBlock } from '@typebot.io/schemas' import type { TextInputBlock } from '@typebot.io/schemas'
import { createSignal, onCleanup, onMount } from 'solid-js' import { createSignal, onCleanup, onMount } from 'solid-js'
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants' import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
import clsx from 'clsx'
type Props = { type Props = {
block: TextInputBlock block: TextInputBlock
@ -55,7 +56,10 @@ export const TextInput = (props: Props) => {
return ( return (
<div <div
class={'flex items-end justify-between pr-2 typebot-input w-full'} class={clsx(
'flex justify-between pr-2 typebot-input w-full',
props.block.options?.isLong ? 'items-end' : 'items-center'
)}
data-testid="input" data-testid="input"
style={{ style={{
'max-width': props.block.options?.isLong ? undefined : '350px', 'max-width': props.block.options?.isLong ? undefined : '350px',

View File

@ -1,6 +1,6 @@
import { isNotEmpty } from '@typebot.io/lib' import { isNotEmpty } from '@typebot.io/lib'
import { Font } from '@typebot.io/schemas' import { Font } from '@typebot.io/schemas'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants' import { defaultFontFamily } from '@typebot.io/schemas/features/typebot/theme/constants'
const googleFontCdnBaseUrl = 'https://fonts.bunny.net/css2' const googleFontCdnBaseUrl = 'https://fonts.bunny.net/css2'
const elementId = 'typebot-font' const elementId = 'typebot-font'
@ -10,8 +10,7 @@ export const injectFont = (font: Font) => {
if (typeof font === 'string' || font.type === 'Google') { if (typeof font === 'string' || font.type === 'Google') {
const fontFamily = const fontFamily =
(typeof font === 'string' ? font : font.family) ?? (typeof font === 'string' ? font : font.family) ?? defaultFontFamily
defaultTheme.general.font.family
if (existingFont?.getAttribute('href')?.includes(fontFamily)) return if (existingFont?.getAttribute('href')?.includes(fontFamily)) return
existingFont?.remove() existingFont?.remove()
const fontElement = document.createElement('link') const fontElement = document.createElement('link')

View File

@ -1,24 +1,51 @@
import { import {
Background, Background,
ChatTheme, ChatTheme,
ContainerColors, ContainerBorderTheme,
ContainerTheme,
GeneralTheme, GeneralTheme,
InputColors, InputTheme,
Theme, Theme,
} from '@typebot.io/schemas' } from '@typebot.io/schemas'
import { isLight, hexToRgb } from '@typebot.io/lib/hexToRgb' import { isLight, hexToRgb } from '@typebot.io/lib/hexToRgb'
import { isNotEmpty } from '@typebot.io/lib' import { isDefined, isEmpty } from '@typebot.io/lib'
import { import {
BackgroundType, BackgroundType,
defaultTheme, defaultBackgroundColor,
defaultBackgroundType,
defaultButtonsBackgroundColor,
defaultButtonsColor,
defaultButtonsBorderThickness,
defaultContainerBackgroundColor,
defaultContainerMaxHeight,
defaultContainerMaxWidth,
defaultDarkTextColor,
defaultFontFamily,
defaultGuestBubblesBackgroundColor,
defaultGuestBubblesColor,
defaultHostBubblesBackgroundColor,
defaultHostBubblesColor,
defaultInputsBackgroundColor,
defaultInputsColor,
defaultInputsPlaceholderColor,
defaultLightTextColor,
defaultProgressBarBackgroundColor,
defaultProgressBarColor,
defaultProgressBarPlacement,
defaultProgressBarPosition,
defaultProgressBarThickness,
defaultInputsShadow,
defaultOpacity,
defaultBlur,
defaultRoundness,
} from '@typebot.io/schemas/features/typebot/theme/constants' } from '@typebot.io/schemas/features/typebot/theme/constants'
import { isChatContainerLight } from '@typebot.io/theme/isChatContainerLight'
const cssVariableNames = { const cssVariableNames = {
general: { general: {
bgImage: '--typebot-container-bg-image', bgImage: '--typebot-container-bg-image',
bgColor: '--typebot-container-bg-color', bgColor: '--typebot-container-bg-color',
fontFamily: '--typebot-container-font-family', fontFamily: '--typebot-container-font-family',
color: '--typebot-container-color',
progressBar: { progressBar: {
position: '--typebot-progress-bar-position', position: '--typebot-progress-bar-position',
color: '--typebot-progress-bar-color', color: '--typebot-progress-bar-color',
@ -29,28 +56,67 @@ const cssVariableNames = {
}, },
}, },
chat: { chat: {
container: {
maxWidth: '--typebot-chat-container-max-width',
maxHeight: '--typebot-chat-container-max-height',
bgColor: '--typebot-chat-container-bg-rgb',
color: '--typebot-chat-container-color',
borderRadius: '--typebot-chat-container-border-radius',
borderWidth: '--typebot-chat-container-border-width',
borderColor: '--typebot-chat-container-border-rgb',
borderOpacity: '--typebot-chat-container-border-opacity',
opacity: '--typebot-chat-container-opacity',
blur: '--typebot-chat-container-blur',
boxShadow: '--typebot-chat-container-box-shadow',
},
hostBubbles: { hostBubbles: {
bgColor: '--typebot-host-bubble-bg-color', bgColor: '--typebot-host-bubble-bg-rgb',
color: '--typebot-host-bubble-color', color: '--typebot-host-bubble-color',
borderRadius: '--typebot-host-bubble-border-radius',
borderWidth: '--typebot-host-bubble-border-width',
borderColor: '--typebot-host-bubble-border-rgb',
borderOpacity: '--typebot-host-bubble-border-opacity',
opacity: '--typebot-host-bubble-opacity',
blur: '--typebot-host-bubble-blur',
boxShadow: '--typebot-host-bubble-box-shadow',
}, },
guestBubbles: { guestBubbles: {
bgColor: '--typebot-guest-bubble-bg-color', bgColor: '--typebot-guest-bubble-bg-rgb',
color: '--typebot-guest-bubble-color', color: '--typebot-guest-bubble-color',
borderRadius: '--typebot-guest-bubble-border-radius',
borderWidth: '--typebot-guest-bubble-border-width',
borderColor: '--typebot-guest-bubble-border-rgb',
borderOpacity: '--typebot-guest-bubble-border-opacity',
opacity: '--typebot-guest-bubble-opacity',
blur: '--typebot-guest-bubble-blur',
boxShadow: '--typebot-guest-bubble-box-shadow',
}, },
inputs: { inputs: {
bgColor: '--typebot-input-bg-color', bgColor: '--typebot-input-bg-rgb',
color: '--typebot-input-color', color: '--typebot-input-color',
placeholderColor: '--typebot-input-placeholder-color', placeholderColor: '--typebot-input-placeholder-color',
borderRadius: '--typebot-input-border-radius',
borderWidth: '--typebot-input-border-width',
borderColor: '--typebot-input-border-rgb',
borderOpacity: '--typebot-input-border-opacity',
opacity: '--typebot-input-opacity',
blur: '--typebot-input-blur',
boxShadow: '--typebot-input-box-shadow',
}, },
buttons: { buttons: {
bgColor: '--typebot-button-bg-color', bgRgb: '--typebot-button-bg-rgb',
bgColorRgb: '--typebot-button-bg-color-rgb',
color: '--typebot-button-color', color: '--typebot-button-color',
borderRadius: '--typebot-button-border-radius',
borderWidth: '--typebot-button-border-width',
borderColor: '--typebot-button-border-rgb',
borderOpacity: '--typebot-button-border-opacity',
opacity: '--typebot-button-opacity',
blur: '--typebot-button-blur',
boxShadow: '--typebot-button-box-shadow',
}, },
checkbox: { checkbox: {
bgColor: '--typebot-checkbox-bg-color', bgRgb: '--typebot-checkbox-bg-rgb',
color: '--typebot-checkbox-color', alphaRatio: '--selectable-alpha-ratio',
baseAlpha: '--selectable-base-alpha',
}, },
}, },
} as const } as const
@ -63,43 +129,31 @@ export const setCssVariablesValue = (
if (!theme) return if (!theme) return
const documentStyle = container?.style const documentStyle = container?.style
if (!documentStyle) return if (!documentStyle) return
setGeneralTheme( setGeneralTheme(theme.general, documentStyle, isPreview)
theme.general ?? defaultTheme.general, setChatTheme(theme.chat, theme.general?.background, documentStyle)
documentStyle,
isPreview
)
setChatTheme(theme.chat ?? defaultTheme.chat, documentStyle)
} }
const setGeneralTheme = ( const setGeneralTheme = (
generalTheme: GeneralTheme, generalTheme: GeneralTheme | undefined,
documentStyle: CSSStyleDeclaration, documentStyle: CSSStyleDeclaration,
isPreview?: boolean isPreview?: boolean
) => { ) => {
setTypebotBackground( setGeneralBackground(generalTheme?.background, documentStyle)
generalTheme.background ?? defaultTheme.general.background,
documentStyle
)
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.general.fontFamily, cssVariableNames.general.fontFamily,
(typeof generalTheme.font === 'string' (typeof generalTheme?.font === 'string'
? generalTheme.font ? generalTheme.font
: generalTheme.font?.family) ?? defaultTheme.general.font.family : generalTheme?.font?.family) ?? defaultFontFamily
)
setProgressBar(
generalTheme.progressBar ?? defaultTheme.general.progressBar,
documentStyle,
isPreview
) )
setProgressBar(generalTheme?.progressBar, documentStyle, isPreview)
} }
const setProgressBar = ( const setProgressBar = (
progressBar: NonNullable<GeneralTheme['progressBar']>, progressBar: GeneralTheme['progressBar'],
documentStyle: CSSStyleDeclaration, documentStyle: CSSStyleDeclaration,
isPreview?: boolean isPreview?: boolean
) => { ) => {
const position = const position = progressBar?.position ?? defaultProgressBarPosition
progressBar.position ?? defaultTheme.general.progressBar.position
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.general.progressBar.position, cssVariableNames.general.progressBar.position,
@ -107,22 +161,20 @@ const setProgressBar = (
) )
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.general.progressBar.color, cssVariableNames.general.progressBar.color,
progressBar.color ?? defaultTheme.general.progressBar.color progressBar?.color ?? defaultProgressBarColor
) )
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.general.progressBar.colorRgb, cssVariableNames.general.progressBar.colorRgb,
hexToRgb( hexToRgb(
progressBar.backgroundColor ?? progressBar?.backgroundColor ?? defaultProgressBarBackgroundColor
defaultTheme.general.progressBar.backgroundColor
).join(', ') ).join(', ')
) )
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.general.progressBar.height, cssVariableNames.general.progressBar.height,
`${progressBar.thickness ?? defaultTheme.general.progressBar.thickness}px` `${progressBar?.thickness ?? defaultProgressBarThickness}px`
) )
const placement = const placement = progressBar?.placement ?? defaultProgressBarPlacement
progressBar.placement ?? defaultTheme.general.progressBar.placement
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.general.progressBar.top, cssVariableNames.general.progressBar.top,
@ -136,123 +188,428 @@ const setProgressBar = (
} }
const setChatTheme = ( const setChatTheme = (
chatTheme: ChatTheme, chatTheme: ChatTheme | undefined,
generalBackground: GeneralTheme['background'],
documentStyle: CSSStyleDeclaration documentStyle: CSSStyleDeclaration
) => { ) => {
setHostBubbles( setChatContainer(
chatTheme.hostBubbles ?? defaultTheme.chat.hostBubbles, chatTheme?.container,
documentStyle generalBackground,
documentStyle,
chatTheme?.roundness
) )
setGuestBubbles( setHostBubbles(chatTheme?.hostBubbles, documentStyle, chatTheme?.roundness)
chatTheme.guestBubbles ?? defaultTheme.chat.guestBubbles, setGuestBubbles(chatTheme?.guestBubbles, documentStyle, chatTheme?.roundness)
documentStyle setButtons(chatTheme?.buttons, documentStyle, chatTheme?.roundness)
setInputs(chatTheme?.inputs, documentStyle, chatTheme?.roundness)
setCheckbox(chatTheme?.container, generalBackground, documentStyle)
}
const setChatContainer = (
container: ChatTheme['container'],
generalBackground: GeneralTheme['background'],
documentStyle: CSSStyleDeclaration,
legacyRoundness?: ChatTheme['roundness']
) => {
const chatContainerBgColor =
container?.backgroundColor ?? defaultContainerBackgroundColor
const isBgDisabled =
chatContainerBgColor === 'transparent' || isEmpty(chatContainerBgColor)
documentStyle.setProperty(
cssVariableNames.chat.container.bgColor,
isBgDisabled ? '0, 0, 0' : hexToRgb(chatContainerBgColor).join(', ')
) )
setButtons(chatTheme.buttons ?? defaultTheme.chat.buttons, documentStyle)
setInputs(chatTheme.inputs ?? defaultTheme.chat.inputs, documentStyle) documentStyle.setProperty(
setRoundness( cssVariableNames.chat.container.color,
chatTheme.roundness ?? defaultTheme.chat.roundness, hexToRgb(
documentStyle container?.color ??
(isChatContainerLight({
chatContainer: container,
generalBackground,
})
? defaultLightTextColor
: defaultDarkTextColor)
).join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.container.maxWidth,
container?.maxWidth ?? defaultContainerMaxWidth
)
documentStyle.setProperty(
cssVariableNames.chat.container.maxHeight,
container?.maxHeight ?? defaultContainerMaxHeight
)
const opacity = isBgDisabled
? '1'
: (container?.opacity ?? defaultOpacity).toString()
documentStyle.setProperty(
cssVariableNames.chat.container.opacity,
isBgDisabled ? '0' : (container?.opacity ?? defaultOpacity).toString()
)
documentStyle.setProperty(
cssVariableNames.chat.container.blur,
opacity === '1' || isBgDisabled
? '0xp'
: `${container?.blur ?? defaultBlur}px`
)
setShadow(
container?.shadow,
documentStyle,
cssVariableNames.chat.container.boxShadow
)
setBorderRadius(
container?.border ?? {
roundeness: legacyRoundness ?? defaultRoundness,
},
documentStyle,
cssVariableNames.chat.container.borderRadius
)
documentStyle.setProperty(
cssVariableNames.chat.container.borderWidth,
isDefined(container?.border?.thickness)
? `${container?.border?.thickness}px`
: '0'
)
documentStyle.setProperty(
cssVariableNames.chat.container.borderOpacity,
isDefined(container?.border?.opacity)
? container.border.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.container.borderColor,
hexToRgb(container?.border?.color ?? '').join(', ')
) )
} }
const setHostBubbles = ( const setHostBubbles = (
hostBubbles: ContainerColors, hostBubbles: ContainerTheme | undefined,
documentStyle: CSSStyleDeclaration documentStyle: CSSStyleDeclaration,
legacyRoundness?: ChatTheme['roundness']
) => { ) => {
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.bgColor, cssVariableNames.chat.hostBubbles.bgColor,
hostBubbles.backgroundColor ?? defaultTheme.chat.hostBubbles.backgroundColor hexToRgb(
hostBubbles?.backgroundColor ?? defaultHostBubblesBackgroundColor
).join(', ')
) )
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.color, cssVariableNames.chat.hostBubbles.color,
hostBubbles.color ?? defaultTheme.chat.hostBubbles.color hostBubbles?.color ?? defaultHostBubblesColor
)
setBorderRadius(
hostBubbles?.border ?? {
roundeness: legacyRoundness ?? defaultRoundness,
},
documentStyle,
cssVariableNames.chat.hostBubbles.borderRadius
)
documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.borderWidth,
isDefined(hostBubbles?.border?.thickness)
? `${hostBubbles?.border?.thickness}px`
: '0'
)
documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.borderColor,
hexToRgb(hostBubbles?.border?.color ?? '').join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.opacity,
isDefined(hostBubbles?.opacity)
? hostBubbles.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.borderOpacity,
isDefined(hostBubbles?.border?.opacity)
? hostBubbles.border.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.blur,
isDefined(hostBubbles?.blur)
? `${hostBubbles.blur ?? 0}px`
: defaultBlur.toString()
)
setShadow(
hostBubbles?.shadow,
documentStyle,
cssVariableNames.chat.hostBubbles.boxShadow
) )
} }
const setGuestBubbles = ( const setGuestBubbles = (
guestBubbles: ContainerColors, guestBubbles: ContainerTheme | undefined,
documentStyle: CSSStyleDeclaration documentStyle: CSSStyleDeclaration,
legacyRoundness?: ChatTheme['roundness']
) => { ) => {
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.bgColor, cssVariableNames.chat.guestBubbles.bgColor,
guestBubbles.backgroundColor ?? hexToRgb(
defaultTheme.chat.guestBubbles.backgroundColor guestBubbles?.backgroundColor ?? defaultGuestBubblesBackgroundColor
).join(', ')
) )
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.color, cssVariableNames.chat.guestBubbles.color,
guestBubbles.color ?? defaultTheme.chat.guestBubbles.color guestBubbles?.color ?? defaultGuestBubblesColor
)
setBorderRadius(
guestBubbles?.border ?? {
roundeness: legacyRoundness ?? defaultRoundness,
},
documentStyle,
cssVariableNames.chat.guestBubbles.borderRadius
)
documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.borderWidth,
isDefined(guestBubbles?.border?.thickness)
? `${guestBubbles?.border?.thickness}px`
: '0'
)
documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.borderColor,
hexToRgb(guestBubbles?.border?.color ?? '').join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.borderOpacity,
isDefined(guestBubbles?.border?.opacity)
? guestBubbles.border.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.opacity,
isDefined(guestBubbles?.opacity)
? guestBubbles.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.blur,
isDefined(guestBubbles?.blur)
? `${guestBubbles.blur ?? 0}px`
: defaultBlur.toString()
)
setShadow(
guestBubbles?.shadow,
documentStyle,
cssVariableNames.chat.guestBubbles.boxShadow
) )
} }
const setButtons = ( const setButtons = (
buttons: ContainerColors, buttons: ContainerTheme | undefined,
documentStyle: CSSStyleDeclaration documentStyle: CSSStyleDeclaration,
legacyRoundness?: ChatTheme['roundness']
) => { ) => {
const bgColor = const bgColor = buttons?.backgroundColor ?? defaultButtonsBackgroundColor
buttons.backgroundColor ?? defaultTheme.chat.buttons.backgroundColor
documentStyle.setProperty(cssVariableNames.chat.buttons.bgColor, bgColor)
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.chat.buttons.bgColorRgb, cssVariableNames.chat.buttons.bgRgb,
hexToRgb(bgColor).join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.bgRgb,
hexToRgb(bgColor).join(', ') hexToRgb(bgColor).join(', ')
) )
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.chat.buttons.color, cssVariableNames.chat.buttons.color,
buttons.color ?? defaultTheme.chat.buttons.color buttons?.color ?? defaultButtonsColor
)
setBorderRadius(
buttons?.border ?? {
roundeness: legacyRoundness ?? defaultRoundness,
},
documentStyle,
cssVariableNames.chat.buttons.borderRadius
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.borderWidth,
isDefined(buttons?.border?.thickness)
? `${buttons?.border?.thickness}px`
: `${defaultButtonsBorderThickness}px`
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.borderColor,
hexToRgb(
buttons?.border?.color ??
buttons?.backgroundColor ??
defaultButtonsBackgroundColor
).join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.borderOpacity,
isDefined(buttons?.border?.opacity)
? buttons.border.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.opacity,
isDefined(buttons?.opacity)
? buttons.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.blur,
isDefined(buttons?.blur) ? `${buttons.blur ?? 0}px` : defaultBlur.toString()
)
setShadow(
buttons?.shadow,
documentStyle,
cssVariableNames.chat.buttons.boxShadow
) )
} }
const setInputs = (inputs: InputColors, documentStyle: CSSStyleDeclaration) => { const setInputs = (
inputs: InputTheme | undefined,
documentStyle: CSSStyleDeclaration,
legacyRoundness?: ChatTheme['roundness']
) => {
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.chat.inputs.bgColor, cssVariableNames.chat.inputs.bgColor,
inputs.backgroundColor ?? defaultTheme.chat.inputs.backgroundColor hexToRgb(inputs?.backgroundColor ?? defaultInputsBackgroundColor).join(', ')
) )
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.chat.inputs.color, cssVariableNames.chat.inputs.color,
inputs.color ?? defaultTheme.chat.inputs.color inputs?.color ?? defaultInputsColor
) )
documentStyle.setProperty( documentStyle.setProperty(
cssVariableNames.chat.inputs.placeholderColor, cssVariableNames.chat.inputs.placeholderColor,
inputs.placeholderColor ?? defaultTheme.chat.inputs.placeholderColor inputs?.placeholderColor ?? defaultInputsPlaceholderColor
)
setBorderRadius(
inputs?.border ?? {
roundeness: legacyRoundness ?? defaultRoundness,
},
documentStyle,
cssVariableNames.chat.inputs.borderRadius
)
documentStyle.setProperty(
cssVariableNames.chat.inputs.borderWidth,
isDefined(inputs?.border?.thickness)
? `${inputs?.border?.thickness}px`
: '0'
)
documentStyle.setProperty(
cssVariableNames.chat.inputs.borderColor,
hexToRgb(inputs?.border?.color ?? '').join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.inputs.opacity,
isDefined(inputs?.opacity)
? inputs.opacity.toString()
: defaultOpacity.toString()
)
documentStyle.setProperty(
cssVariableNames.chat.inputs.blur,
isDefined(inputs?.blur) ? `${inputs.blur ?? 0}px` : defaultBlur.toString()
)
setShadow(
inputs?.shadow ?? defaultInputsShadow,
documentStyle,
cssVariableNames.chat.inputs.boxShadow
) )
} }
const setTypebotBackground = ( const setCheckbox = (
background: Background, container: ChatTheme['container'],
generalBackground: GeneralTheme['background'],
documentStyle: CSSStyleDeclaration
) => {
const chatContainerBgColor =
container?.backgroundColor ?? defaultContainerBackgroundColor
const isChatBgTransparent =
chatContainerBgColor === 'transparent' ||
isEmpty(chatContainerBgColor) ||
(container?.opacity ?? defaultOpacity) <= 0.2
if (isChatBgTransparent) {
const bgType = generalBackground?.type ?? defaultBackgroundType
documentStyle.setProperty(
cssVariableNames.chat.checkbox.bgRgb,
bgType === BackgroundType.IMAGE
? 'rgba(255, 255, 255, 0.75)'
: hexToRgb(
(bgType === BackgroundType.COLOR
? generalBackground?.content
: '#ffffff') ?? '#ffffff'
).join(', ')
)
if (bgType === BackgroundType.IMAGE) {
documentStyle.setProperty(cssVariableNames.chat.checkbox.alphaRatio, '3')
} else {
documentStyle.setProperty(
cssVariableNames.chat.checkbox.alphaRatio,
generalBackground?.content && isLight(generalBackground?.content)
? '1'
: '2'
)
}
} else {
documentStyle.setProperty(
cssVariableNames.chat.checkbox.bgRgb,
hexToRgb(chatContainerBgColor)
.concat(container?.opacity ?? 1)
.join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.checkbox.alphaRatio,
isLight(chatContainerBgColor) ? '1' : '2'
)
}
}
const setGeneralBackground = (
background: Background | undefined,
documentStyle: CSSStyleDeclaration documentStyle: CSSStyleDeclaration
) => { ) => {
documentStyle.setProperty(cssVariableNames.general.bgImage, null) documentStyle.setProperty(cssVariableNames.general.bgImage, null)
documentStyle.setProperty(cssVariableNames.general.bgColor, null) documentStyle.setProperty(cssVariableNames.general.bgColor, null)
documentStyle.setProperty( documentStyle.setProperty(
background?.type === BackgroundType.IMAGE (background?.type ?? defaultBackgroundType) === BackgroundType.IMAGE
? cssVariableNames.general.bgImage ? cssVariableNames.general.bgImage
: cssVariableNames.general.bgColor, : cssVariableNames.general.bgColor,
parseBackgroundValue(background) parseBackgroundValue({
type: background?.type ?? defaultBackgroundType,
content: background?.content ?? defaultBackgroundColor,
})
) )
documentStyle.setProperty(
cssVariableNames.chat.checkbox.bgColor,
background?.type === BackgroundType.IMAGE
? 'rgba(255, 255, 255, 0.75)'
: (background?.type === BackgroundType.COLOR
? background.content
: '#ffffff') ?? '#ffffff'
)
const backgroundColor =
background.type === BackgroundType.IMAGE
? '#000000'
: background?.type === BackgroundType.COLOR &&
isNotEmpty(background.content)
? background.content
: '#ffffff'
documentStyle.setProperty(
cssVariableNames.general.color,
isLight(backgroundColor) ? '#303235' : '#ffffff'
)
if (background.type === BackgroundType.IMAGE) {
documentStyle.setProperty(cssVariableNames.chat.checkbox.baseAlpha, '0.40')
} else {
documentStyle.setProperty(cssVariableNames.chat.checkbox.baseAlpha, '0')
}
} }
const parseBackgroundValue = ({ type, content }: Background = {}) => { const parseBackgroundValue = ({ type, content }: Background = {}) => {
@ -261,25 +618,80 @@ const parseBackgroundValue = ({ type, content }: Background = {}) => {
return 'transparent' return 'transparent'
case undefined: case undefined:
case BackgroundType.COLOR: case BackgroundType.COLOR:
return content ?? defaultTheme.general.background.content return content ?? defaultBackgroundColor
case BackgroundType.IMAGE: case BackgroundType.IMAGE:
return `url(${content})` return `url(${content})`
} }
} }
const setRoundness = ( const setBorderRadius = (
roundness: NonNullable<ChatTheme['roundness']>, border: ContainerBorderTheme,
documentStyle: CSSStyleDeclaration documentStyle: CSSStyleDeclaration,
variableName: string
) => { ) => {
switch (roundness) { switch (border?.roundeness ?? defaultRoundness) {
case 'none': {
documentStyle.setProperty(variableName, '0')
break
}
case 'medium': {
documentStyle.setProperty(variableName, '6px')
break
}
case 'large': {
documentStyle.setProperty(variableName, '20px')
break
}
case 'custom': {
documentStyle.setProperty(
variableName,
`${border.customRoundeness ?? 6}px`
)
break
}
}
}
// Props taken from https://tailwindcss.com/docs/box-shadow
const setShadow = (
shadow: ContainerTheme['shadow'],
documentStyle: CSSStyleDeclaration,
variableName: string
) => {
if (shadow === undefined) {
documentStyle.setProperty(variableName, '0 0 #0000')
return
}
switch (shadow) {
case 'none': case 'none':
documentStyle.setProperty('--typebot-border-radius', '0') documentStyle.setProperty(variableName, '0 0 #0000')
break break
case 'medium': case 'sm':
documentStyle.setProperty('--typebot-border-radius', '6px') documentStyle.setProperty(variableName, '0 1px 2px 0 rgb(0 0 0 / 0.05)')
break break
case 'large': case 'md':
documentStyle.setProperty('--typebot-border-radius', '20px') documentStyle.setProperty(
variableName,
'0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'
)
break
case 'lg':
documentStyle.setProperty(
variableName,
'0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)'
)
break
case 'xl':
documentStyle.setProperty(
variableName,
'0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)'
)
break
case '2xl':
documentStyle.setProperty(
variableName,
'0 25px 50px -12px rgb(0 0 0 / 0.25)'
)
break break
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/nextjs", "name": "@typebot.io/nextjs",
"version": "0.2.64", "version": "0.2.65",
"description": "Convenient library to display typebots on your Next.js website", "description": "Convenient library to display typebots on your Next.js website",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -1,6 +1,6 @@
{ {
"name": "@typebot.io/react", "name": "@typebot.io/react",
"version": "0.2.64", "version": "0.2.65",
"description": "Convenient library to display typebots on your React app", "description": "Convenient library to display typebots on your React app",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@ -1,5 +1,3 @@
import { Theme } from './schema'
export enum BackgroundType { export enum BackgroundType {
COLOR = 'Color', COLOR = 'Color',
IMAGE = 'Image', IMAGE = 'Image',
@ -11,37 +9,63 @@ export const fontTypes = ['Google', 'Custom'] as const
export const progressBarPlacements = ['Top', 'Bottom'] as const export const progressBarPlacements = ['Top', 'Bottom'] as const
export const progressBarPositions = ['fixed', 'absolute'] as const export const progressBarPositions = ['fixed', 'absolute'] as const
export const defaultTheme = { export const shadows = ['none', 'sm', 'md', 'lg', 'xl', '2xl'] as const
chat: { export const borderRoundness = ['none', 'medium', 'large', 'custom'] as const
roundness: 'medium',
hostBubbles: { backgroundColor: '#F7F8FF', color: '#303235' }, export const defaultLightTextColor = '#303235'
guestBubbles: { backgroundColor: '#FF8E21', color: '#FFFFFF' }, export const defaultDarkTextColor = '#FFFFFF'
buttons: { backgroundColor: '#0042DA', color: '#FFFFFF' },
inputs: { /*---- General ----*/
backgroundColor: '#FFFFFF',
color: '#303235', // Font
placeholderColor: '#9095A0', export const defaultFontType = 'Google'
}, export const defaultFontFamily = 'Open Sans'
hostAvatar: {
isEnabled: true, // Background
}, export const defaultBackgroundType = BackgroundType.COLOR
guestAvatar: { export const defaultBackgroundColor = '#ffffff'
isEnabled: false,
}, // Progress bar
}, export const defaultProgressBarIsEnabled = false
general: { export const defaultProgressBarColor = '#0042DA'
font: { export const defaultProgressBarBackgroundColor = '#e0edff'
type: 'Google', export const defaultProgressBarThickness = 4
family: 'Open Sans', export const defaultProgressBarPosition = 'absolute'
}, export const defaultProgressBarPlacement = 'Top'
background: { type: BackgroundType.COLOR, content: '#ffffff' },
progressBar: { export const defaultRoundness = 'medium'
isEnabled: false, export const defaultOpacity = 1
color: '#0042DA', export const defaultBlur = 0
backgroundColor: '#e0edff',
thickness: 4, /*---- Chat ----*/
position: 'absolute',
placement: 'Top', // Container
}, export const defaultContainerMaxWidth = '800px'
}, export const defaultContainerMaxHeight = '100%'
} as const satisfies Theme export const defaultContainerBackgroundColor = 'transparent'
export const defaultContainerColor = '#27272A'
// Host bubbles
export const defaultHostBubblesBackgroundColor = '#F7F8FF'
export const defaultHostBubblesColor = defaultLightTextColor
// Guest bubbles
export const defaultGuestBubblesBackgroundColor = '#FF8E21'
export const defaultGuestBubblesColor = defaultDarkTextColor
// Buttons
export const defaultButtonsBackgroundColor = '#0042DA'
export const defaultButtonsColor = defaultDarkTextColor
export const defaultButtonsBorderThickness = 1
// Inputs
export const defaultInputsBackgroundColor = '#FFFFFF'
export const defaultInputsColor = defaultLightTextColor
export const defaultInputsPlaceholderColor = '#9095A0'
export const defaultInputsShadow = 'md'
// Host avatar
export const defaultHostAvatarIsEnabled = true
// Guest avatar
export const defaultGuestAvatarIsEnabled = false

View File

@ -2,9 +2,11 @@ import { ThemeTemplate as ThemeTemplatePrisma } from '@typebot.io/prisma'
import { z } from '../../../zod' import { z } from '../../../zod'
import { import {
BackgroundType, BackgroundType,
borderRoundness,
fontTypes, fontTypes,
progressBarPlacements, progressBarPlacements,
progressBarPositions, progressBarPositions,
shadows,
} from './constants' } from './constants'
const avatarPropsSchema = z.object({ const avatarPropsSchema = z.object({
@ -12,25 +14,48 @@ const avatarPropsSchema = z.object({
url: z.string().optional(), url: z.string().optional(),
}) })
const containerColorsSchema = z.object({ const containerBorderThemeSchema = z.object({
backgroundColor: z.string().optional(), thickness: z.number().optional(),
color: z.string().optional(), color: z.string().optional(),
roundeness: z.enum(borderRoundness).optional(),
customRoundeness: z.number().optional(),
opacity: z.number().min(0).max(1).optional(),
}) })
const inputColorsSchema = containerColorsSchema.merge( export type ContainerBorderTheme = z.infer<typeof containerBorderThemeSchema>
z.object({
placeholderColor: z.string().optional(), const containerThemeSchema = z.object({
backgroundColor: z.string().optional(),
color: z.string().optional(),
blur: z.number().optional(),
opacity: z.number().min(0).max(1).optional(),
shadow: z.enum(shadows).optional(),
border: containerBorderThemeSchema.optional(),
})
const inputThemeSchema = containerThemeSchema.extend({
placeholderColor: z.string().optional(),
})
const chatContainerSchema = z
.object({
maxWidth: z.string().optional(),
maxHeight: z.string().optional(),
}) })
) .merge(containerThemeSchema)
export const chatThemeSchema = z.object({ export const chatThemeSchema = z.object({
container: chatContainerSchema.optional(),
hostAvatar: avatarPropsSchema.optional(), hostAvatar: avatarPropsSchema.optional(),
guestAvatar: avatarPropsSchema.optional(), guestAvatar: avatarPropsSchema.optional(),
hostBubbles: containerColorsSchema.optional(), hostBubbles: containerThemeSchema.optional(),
guestBubbles: containerColorsSchema.optional(), guestBubbles: containerThemeSchema.optional(),
buttons: containerColorsSchema.optional(), buttons: containerThemeSchema.optional(),
inputs: inputColorsSchema.optional(), inputs: inputThemeSchema.optional(),
roundness: z.enum(['none', 'medium', 'large']).optional(), roundness: z
.enum(['none', 'medium', 'large'])
.optional()
.describe('Deprecated, use `container.border.roundeness` instead'),
}) })
const backgroundSchema = z.object({ const backgroundSchema = z.object({
@ -98,6 +123,6 @@ export type ChatTheme = z.infer<typeof chatThemeSchema>
export type AvatarProps = z.infer<typeof avatarPropsSchema> export type AvatarProps = z.infer<typeof avatarPropsSchema>
export type GeneralTheme = z.infer<typeof generalThemeSchema> export type GeneralTheme = z.infer<typeof generalThemeSchema>
export type Background = z.infer<typeof backgroundSchema> export type Background = z.infer<typeof backgroundSchema>
export type ContainerColors = z.infer<typeof containerColorsSchema> export type ContainerTheme = z.infer<typeof containerThemeSchema>
export type InputColors = z.infer<typeof inputColorsSchema> export type InputTheme = z.infer<typeof inputThemeSchema>
export type ThemeTemplate = z.infer<typeof themeTemplateSchema> export type ThemeTemplate = z.infer<typeof themeTemplateSchema>

View File

@ -0,0 +1,41 @@
import { isLight } from '@typebot.io/lib/hexToRgb'
import { ContainerTheme, GeneralTheme } from '@typebot.io/schemas'
import {
BackgroundType,
defaultBackgroundColor,
defaultBackgroundType,
defaultContainerBackgroundColor,
defaultOpacity,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { isEmpty, isNotEmpty } from '@typebot.io/lib'
type Props = {
chatContainer: ContainerTheme | undefined
generalBackground: GeneralTheme['background']
}
export const isChatContainerLight = ({
chatContainer,
generalBackground,
}: Props): boolean => {
const chatContainerBgColor =
chatContainer?.backgroundColor ?? defaultContainerBackgroundColor
const ignoreChatBackground =
(chatContainer?.opacity ?? defaultOpacity) <= 0.3 ||
chatContainerBgColor === 'transparent' ||
isEmpty(chatContainerBgColor)
if (ignoreChatBackground) {
const bgType = generalBackground?.type ?? defaultBackgroundType
const backgroundColor =
bgType === BackgroundType.IMAGE
? '#000000'
: bgType === BackgroundType.COLOR &&
isNotEmpty(generalBackground?.content)
? generalBackground.content
: '#ffffff'
return isLight(backgroundColor)
}
return isLight(chatContainer?.backgroundColor ?? defaultBackgroundColor)
}

View File

@ -0,0 +1,13 @@
{
"name": "@typebot.io/theme",
"version": "1.0.0",
"description": "",
"scripts": {},
"keywords": [],
"author": "Baptiste Arnaud",
"license": "ISC",
"dependencies": {
"@typebot.io/schemas": "workspace:*",
"@typebot.io/lib": "workspace:*"
}
}

15
pnpm-lock.yaml generated
View File

@ -122,6 +122,9 @@ importers:
'@typebot.io/nextjs': '@typebot.io/nextjs':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/embeds/nextjs version: link:../../packages/embeds/nextjs
'@typebot.io/theme':
specifier: workspace:*
version: link:../../packages/theme
'@udecode/cn': '@udecode/cn':
specifier: 29.0.1 specifier: 29.0.1
version: 29.0.1(@types/react@18.2.15)(class-variance-authority@0.7.0)(react-dom@18.2.0)(react@18.2.0)(tailwind-merge@2.2.1) version: 29.0.1(@types/react@18.2.15)(class-variance-authority@0.7.0)(react-dom@18.2.0)(react@18.2.0)(tailwind-merge@2.2.1)
@ -1048,6 +1051,9 @@ importers:
'@typebot.io/schemas': '@typebot.io/schemas':
specifier: workspace:* specifier: workspace:*
version: link:../../schemas version: link:../../schemas
'@typebot.io/theme':
specifier: workspace:*
version: link:../../theme
'@typebot.io/tsconfig': '@typebot.io/tsconfig':
specifier: workspace:* specifier: workspace:*
version: link:../../tsconfig version: link:../../tsconfig
@ -1925,6 +1931,15 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../tsconfig version: link:../tsconfig
packages/theme:
dependencies:
'@typebot.io/lib':
specifier: workspace:*
version: link:../lib
'@typebot.io/schemas':
specifier: workspace:*
version: link:../schemas
packages/transactional: packages/transactional:
dependencies: dependencies:
'@react-email/components': '@react-email/components':