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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,13 +5,13 @@ import { Typebot } from '@typebot.io/schemas'
import { useState } from 'react'
import { BubbleSettings } from '../../../settings/BubbleSettings/BubbleSettings'
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) => ({
button: {
backgroundColor:
typebot?.theme.chat?.buttons?.backgroundColor ??
defaultTheme.chat.buttons.backgroundColor,
defaultButtonsBackgroundColor,
},
})

View File

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

View File

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

View File

@ -19,8 +19,13 @@ import { Theme, ThemeTemplate } from '@typebot.io/schemas'
import { useState } from 'react'
import { DefaultAvatar } from './DefaultAvatar'
import {
defaultButtonsBackgroundColor,
BackgroundType,
defaultTheme,
defaultGuestAvatarIsEnabled,
defaultGuestBubblesBackgroundColor,
defaultHostAvatarIsEnabled,
defaultBackgroundColor,
defaultHostBubblesBackgroundColor,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
@ -71,28 +76,28 @@ export const ThemeTemplateCard = ({
const hostAvatar = {
isEnabled:
themeTemplate.theme.chat?.hostAvatar?.isEnabled ??
defaultTheme.chat.hostAvatar.isEnabled,
defaultHostAvatarIsEnabled,
url: themeTemplate.theme.chat?.hostAvatar?.url,
}
const hostBubbleBgColor =
themeTemplate.theme.chat?.hostBubbles?.backgroundColor ??
defaultTheme.chat.hostBubbles.backgroundColor
defaultHostBubblesBackgroundColor
const guestAvatar = {
isEnabled:
themeTemplate.theme.chat?.guestAvatar?.isEnabled ??
defaultTheme.chat.guestAvatar.isEnabled,
defaultGuestAvatarIsEnabled,
url: themeTemplate.theme.chat?.guestAvatar?.url,
}
const guestBubbleBgColor =
themeTemplate.theme.chat?.guestBubbles?.backgroundColor ??
defaultTheme.chat.guestBubbles.backgroundColor
defaultGuestBubblesBackgroundColor
const buttonBgColor =
themeTemplate.theme.chat?.buttons?.backgroundColor ??
defaultTheme.chat.buttons.backgroundColor
defaultButtonsBackgroundColor
return (
<Stack
@ -197,8 +202,7 @@ const parseBackground = (
case undefined:
case BackgroundType.COLOR:
return {
backgroundColor:
background?.content ?? defaultTheme.general.background.content,
backgroundColor: background?.content ?? defaultBackgroundColor,
}
case BackgroundType.IMAGE:
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 { AvatarProps, ChatTheme, Theme } from '@typebot.io/schemas'
import {
AvatarProps,
ChatTheme,
GeneralTheme,
Theme,
} from '@typebot.io/schemas'
import React from 'react'
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 { 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 = {
workspaceId: string
typebotId: string
generalBackground: GeneralTheme['background']
chatTheme: Theme['chat']
onChatThemeChange: (chatTheme: ChatTheme) => void
}
@ -26,6 +39,7 @@ export const ChatThemeSettings = ({
workspaceId,
typebotId,
chatTheme,
generalBackground,
onChatThemeChange,
}: Props) => {
const { t } = useTranslate()
@ -44,6 +58,16 @@ export const ChatThemeSettings = ({
const updateInputs = (inputs: NonNullable<Theme['chat']>['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) =>
onChatThemeChange({ ...chatTheme, hostAvatar })
@ -52,6 +76,14 @@ export const ChatThemeSettings = ({
return (
<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
uploadFileProps={{
workspaceId,
@ -59,7 +91,7 @@ export const ChatThemeSettings = ({
fileName: 'hostAvatar',
}}
title={t('theme.sideMenu.chat.botAvatar')}
avatarProps={chatTheme?.hostAvatar ?? defaultTheme.chat.hostAvatar}
avatarProps={chatTheme?.hostAvatar}
isDefaultCheck
onAvatarChange={updateHostAvatar}
/>
@ -70,63 +102,83 @@ export const ChatThemeSettings = ({
fileName: 'guestAvatar',
}}
title={t('theme.sideMenu.chat.userAvatar')}
avatarProps={chatTheme?.guestAvatar ?? defaultTheme.chat.guestAvatar}
avatarProps={chatTheme?.guestAvatar}
onAvatarChange={updateGuestAvatar}
/>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.botBubbles')}</Heading>
<HostBubbles
hostBubbles={chatTheme?.hostBubbles ?? defaultTheme.chat.hostBubbles}
onHostBubblesChange={updateHostBubbles}
<ContainerThemeForm
testId="hostBubblesTheme"
theme={chatTheme?.hostBubbles}
onThemeChange={updateHostBubbles}
defaultTheme={{
backgroundColor: defaultHostBubblesBackgroundColor,
color: defaultHostBubblesColor,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
},
}}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.userBubbles')}</Heading>
<GuestBubbles
guestBubbles={
chatTheme?.guestBubbles ?? defaultTheme.chat.guestBubbles
}
onGuestBubblesChange={updateGuestBubbles}
<ContainerThemeForm
testId="guestBubblesTheme"
theme={chatTheme?.guestBubbles}
onThemeChange={updateGuestBubbles}
defaultTheme={{
backgroundColor: defaultGuestBubblesBackgroundColor,
color: defaultGuestBubblesColor,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
},
}}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.buttons')}</Heading>
<ButtonsTheme
buttons={chatTheme?.buttons ?? defaultTheme.chat.buttons}
onButtonsChange={updateButtons}
<ContainerThemeForm
testId="buttonsTheme"
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 borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">{t('theme.sideMenu.chat.inputs')}</Heading>
<InputsTheme
inputs={chatTheme?.inputs ?? defaultTheme.chat.inputs}
onInputsChange={updateInputs}
/>
</Stack>
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
<Heading fontSize="lg">
{t('theme.sideMenu.chat.cornersRoundness')}
</Heading>
<RadioButtons
options={[
{
label: <NoRadiusIcon />,
value: 'none',
<ContainerThemeForm
testId="inputsTheme"
theme={chatTheme?.inputs}
onThemeChange={updateInputs}
onPlaceholderColorChange={updateInputsPlaceholderColor}
defaultTheme={{
backgroundColor: defaultInputsBackgroundColor,
color: defaultInputsColor,
placeholderColor: defaultInputsPlaceholderColor,
shadow: defaultInputsShadow,
opacity: defaultOpacity,
blur: defaultBlur,
border: {
roundeness: defaultRoundness,
},
{
label: <MediumRadiusIcon />,
value: 'medium',
},
{
label: <LargeRadiusIcon />,
value: 'large',
},
]}
value={chatTheme?.roundness ?? defaultTheme.chat.roundness}
onSelect={(roundness) =>
onChatThemeChange({ ...chatTheme, roundness })
}
}}
/>
</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 { ContainerColors } from '@typebot.io/schemas'
import { ContainerTheme } from '@typebot.io/schemas'
import React from 'react'
import { ColorPicker } from '../../../../components/ColorPicker'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
import {
defaultGuestBubblesBackgroundColor,
defaultGuestBubblesColor,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = {
guestBubbles: ContainerColors | undefined
onGuestBubblesChange: (hostBubbles: ContainerColors | undefined) => void
guestBubbles: ContainerTheme | undefined
onGuestBubblesChange: (hostBubbles: ContainerTheme | undefined) => void
}
export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
@ -25,8 +28,7 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
<Text>{t('theme.sideMenu.chat.theme.background')}</Text>
<ColorPicker
value={
guestBubbles?.backgroundColor ??
defaultTheme.chat.guestBubbles.backgroundColor
guestBubbles?.backgroundColor ?? defaultGuestBubblesBackgroundColor
}
onColorChange={updateBackground}
/>
@ -34,7 +36,7 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.text')}</Text>
<ColorPicker
value={guestBubbles?.color ?? defaultTheme.chat.guestBubbles.color}
value={guestBubbles?.color ?? defaultGuestBubblesColor}
onColorChange={updateText}
/>
</Flex>

View File

@ -1,13 +1,16 @@
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 { ColorPicker } from '../../../../components/ColorPicker'
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
import {
defaultHostBubblesBackgroundColor,
defaultHostBubblesColor,
} from '@typebot.io/schemas/features/typebot/theme/constants'
type Props = {
hostBubbles: ContainerColors | undefined
onHostBubblesChange: (hostBubbles: ContainerColors | undefined) => void
hostBubbles: ContainerTheme | undefined
onHostBubblesChange: (hostBubbles: ContainerTheme | undefined) => void
}
export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
@ -24,8 +27,7 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
<Text>{t('theme.sideMenu.chat.theme.background')}</Text>
<ColorPicker
value={
hostBubbles?.backgroundColor ??
defaultTheme.chat.hostBubbles.backgroundColor
hostBubbles?.backgroundColor ?? defaultHostBubblesBackgroundColor
}
onColorChange={handleBackgroundChange}
/>
@ -33,7 +35,7 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.chat.theme.text')}</Text>
<ColorPicker
value={hostBubbles?.color ?? defaultTheme.chat.hostBubbles.color}
value={hostBubbles?.color ?? defaultHostBubblesColor}
onColorChange={handleTextChange}
/>
</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 {
BackgroundType,
defaultTheme,
defaultBackgroundColor,
defaultBackgroundType,
} from '@typebot.io/schemas/features/typebot/theme/constants'
import { useTranslate } from '@tolgee/react'
@ -34,10 +35,7 @@ export const BackgroundContent = ({
const handleContentChange = (content: string) =>
onBackgroundContentChange(content)
if (
(background?.type ?? defaultTheme.general.background.type) ===
BackgroundType.IMAGE
) {
if ((background?.type ?? defaultBackgroundType) === BackgroundType.IMAGE) {
if (!typebot) return null
return (
<Popover isLazy placement="top">
@ -76,15 +74,12 @@ export const BackgroundContent = ({
</Popover>
)
}
if (
(background?.type ?? defaultTheme.general.background.type) ===
BackgroundType.COLOR
) {
if ((background?.type ?? defaultBackgroundType) === BackgroundType.COLOR) {
return (
<Flex justify="space-between" align="center">
<Text>{t('theme.sideMenu.global.background.color')}</Text>
<ColorPicker
value={background?.content ?? defaultTheme.general.background.content}
value={background?.content ?? defaultBackgroundColor}
onColorChange={handleContentChange}
/>
</Flex>

View File

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

View File

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

View File

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

View File

@ -5,7 +5,11 @@ import { NumberInput } from '@/components/inputs'
import { FormLabel, HStack } from '@chakra-ui/react'
import { ProgressBar } from '@typebot.io/schemas'
import {
defaultTheme,
defaultProgressBarColor,
defaultProgressBarIsEnabled,
defaultProgressBarPlacement,
defaultProgressBarPosition,
defaultProgressBarThickness,
progressBarPlacements,
progressBarPositions,
} from '@typebot.io/schemas/features/typebot/theme/constants'
@ -37,18 +41,14 @@ export const ProgressBarForm = ({
return (
<SwitchWithRelatedSettings
label={'Enable progress bar?'}
initialValue={
progressBar?.isEnabled ?? defaultTheme.general.progressBar.isEnabled
}
initialValue={progressBar?.isEnabled ?? defaultProgressBarIsEnabled}
onCheckChange={updateEnabled}
>
<DropdownList
size="sm"
direction="row"
label="Placement:"
currentItem={
progressBar?.placement ?? defaultTheme.general.progressBar.placement
}
currentItem={progressBar?.placement ?? defaultProgressBarPlacement}
onItemSelect={updatePlacement}
items={progressBarPlacements}
/>
@ -58,9 +58,7 @@ export const ProgressBarForm = ({
Color:
</FormLabel>
<ColorPicker
defaultValue={
progressBar?.color ?? defaultTheme.general.progressBar.color
}
defaultValue={progressBar?.color ?? defaultProgressBarColor}
onColorChange={updateColor}
/>
</HStack>
@ -69,9 +67,7 @@ export const ProgressBarForm = ({
direction="row"
withVariableButton={false}
maxW="100px"
defaultValue={
progressBar?.thickness ?? defaultTheme.general.progressBar.thickness
}
defaultValue={progressBar?.thickness ?? defaultProgressBarThickness}
onValueChange={updateThickness}
size="sm"
/>
@ -80,9 +76,7 @@ export const ProgressBarForm = ({
direction="row"
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.'
currentItem={
progressBar?.position ?? defaultTheme.general.progressBar.position
}
currentItem={progressBar?.position ?? defaultProgressBarPosition}
onItemSelect={updatePosition}
items={progressBarPositions}
/>

View File

@ -3,6 +3,10 @@ import test, { expect } from '@playwright/test'
import { createId } from '@paralleldrive/cuid2'
import { importTypebotInDatabase } from '@typebot.io/playwright/databaseActions'
import { freeWorkspaceId } from '@typebot.io/playwright/databaseSetup'
import {
defaultContainerMaxHeight,
defaultContainerMaxWidth,
} from '@typebot.io/schemas/features/typebot/theme/constants'
const hostAvatarUrl =
'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()
// Font
await page.getByRole('button', { name: 'Font' }).click()
await page.getByRole('textbox').fill('Roboto Slab')
await page.getByRole('menuitem', { name: 'Roboto Slab' }).click()
await expect(page.locator('.typebot-container')).toHaveCSS(
@ -42,6 +47,7 @@ test.describe.parallel('Theme page', () => {
'background-color',
'rgba(0, 0, 0, 0)'
)
await page.getByRole('button', { name: 'Background' }).click()
await page.click('text=Color')
await page.waitForTimeout(100)
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 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
await expect(
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()
// 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
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.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')
const hostBubble = page.locator('[data-testid="host-bubble"] >> nth=-1')
@ -146,11 +158,11 @@ test.describe.parallel('Theme page', () => {
// Buttons
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.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')
const button = page.getByRole('button', { name: 'Go' })
@ -159,11 +171,11 @@ test.describe.parallel('Theme page', () => {
// Guest bubbles
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.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.getByRole('button', { name: 'Go' }).click()
@ -192,11 +204,11 @@ test.describe.parallel('Theme page', () => {
await page.waitForTimeout(1000)
// Input
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.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')
const input = page.locator('.typebot-input')

View File

@ -21342,6 +21342,70 @@
"chat": {
"type": "object",
"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": {
"type": "object",
"properties": {
@ -21372,6 +21436,53 @@
},
"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
}
}
}
}
},
@ -21383,6 +21494,53 @@
},
"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
}
}
}
}
},
@ -21394,6 +21552,53 @@
},
"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
}
}
}
}
},
@ -21406,6 +21611,53 @@
"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
}
}
},
"placeholderColor": {
"type": "string"
}
@ -21417,7 +21669,8 @@
"none",
"medium",
"large"
]
],
"description": "Deprecated, use `container.border.roundeness` instead"
}
}
},

View File

@ -6821,6 +6821,70 @@
"chat": {
"type": "object",
"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": {
"type": "object",
"properties": {
@ -6851,6 +6915,53 @@
},
"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
}
}
}
}
},
@ -6862,6 +6973,53 @@
},
"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
}
}
}
}
},
@ -6873,6 +7031,53 @@
},
"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
}
}
}
}
},
@ -6885,6 +7090,53 @@
"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
}
}
},
"placeholderColor": {
"type": "string"
}
@ -6896,7 +7148,8 @@
"none",
"medium",
"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 { env } from '@typebot.io/env'
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 {
defaultBackgroundColor,
defaultBackgroundType,
} from '@typebot.io/schemas/features/typebot/theme/constants'
// Browsers that doesn't support ES modules and/or web components
const incompatibleBrowsers = [
@ -109,9 +112,10 @@ const getTypebotFromPublicId = async (publicId?: string) => {
? ({
name: publishedTypebot.typebot.name,
publicId: publishedTypebot.typebot.publicId ?? null,
background:
publishedTypebot.theme.general?.background ??
defaultTheme.general.background,
background: publishedTypebot.theme.general?.background ?? {
type: defaultBackgroundType,
content: defaultBackgroundColor,
},
isHideQueryParamsEnabled:
publishedTypebot.settings.general?.isHideQueryParamsEnabled ??
defaultSettings.general.isHideQueryParamsEnabled,
@ -156,9 +160,10 @@ const getTypebotFromCustomDomain = async (customDomain: string) => {
? ({
name: publishedTypebot.typebot.name,
publicId: publishedTypebot.typebot.publicId ?? null,
background:
publishedTypebot.theme.general?.background ??
defaultTheme.general.background,
background: publishedTypebot.theme.general?.background ?? {
type: defaultBackgroundType,
content: defaultBackgroundColor,
},
isHideQueryParamsEnabled:
publishedTypebot.settings.general?.isHideQueryParamsEnabled ??
defaultSettings.general.isHideQueryParamsEnabled,
@ -233,7 +238,10 @@ const App = ({
defaultSettings.general.isHideQueryParamsEnabled
}
background={
publishedTypebot.background ?? defaultTheme.general.background
publishedTypebot.background ?? {
type: defaultBackgroundType,
content: defaultBackgroundColor,
}
}
metadata={publishedTypebot.metadata ?? {}}
font={publishedTypebot.font}