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
DATABASE_URL=postgresql://postgres:typebot@localhost:5432/typebot

View File

@ -12,9 +12,10 @@
"editor.tabSize": 2,
"typescript.updateImportsOnFileMove.enabled": "always",
"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",
"NEXTAUTH_URL": "http://localhost:3000"
"NEXTAUTH_URL": "http://localhost:3000",
"ENCRYPTION_SECRET": "H+KbL/OFrqbEuDy/1zX8bsPG+spXri3S"
},
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"

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}

View File

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

View File

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

View File

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

View File

@ -2,47 +2,6 @@
@tailwind components;
@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 */
.scrollable-container::-webkit-scrollbar {
display: none;
@ -161,67 +120,100 @@ pre {
.typebot-container {
background-image: var(--typebot-container-bg-image);
background-color: var(--typebot-container-bg-color);
background-position: center;
background-size: cover;
font-family: var(--typebot-container-font-family), -apple-system,
BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
'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 {
color: var(--typebot-button-color);
background-color: var(--typebot-button-bg-color);
border: 1px solid var(--typebot-button-bg-color);
border-radius: var(--typebot-border-radius);
background-color: rgba(
var(--typebot-button-bg-rgb),
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;
}
.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 {
border: 1px solid
rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.25)
);
border-radius: var(--typebot-border-radius);
color: var(--typebot-container-color);
border-width: var(--typebot-button-border-width);
border-color: rgba(
var(--typebot-button-border-rgb),
calc(var(--selectable-alpha-ratio) * 0.25)
);
border-radius: var(--typebot-button-border-radius);
color: rgb(var(--typebot-chat-container-color));
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.08)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.08)
);
transition: all 0.3s ease;
backdrop-filter: blur(2px);
}
.typebot-selectable:hover {
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.12)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.12)
);
border-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.3)
var(--typebot-button-border-rgb),
calc(var(--selectable-alpha-ratio) * 0.3)
);
}
.typebot-selectable.selected {
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.18)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.18)
);
border-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.35)
var(--typebot-button-border-rgb),
calc(var(--selectable-alpha-ratio) * 0.35)
);
}
.typebot-checkbox {
border: 1px solid var(--typebot-button-bg-color);
border-radius: var(--typebot-border-radius);
background-color: var(--typebot-checkbox-bg-color);
border: 1px solid
rgba(var(--typebot-button-bg-rgb), var(--typebot-button-opacity));
border-radius: var(--typebot-button-border-radius);
background-color: rgba(var(--typebot-checkbox-bg-rgb));
color: var(--typebot-button-color);
padding: 1px;
border-radius: 2px;
@ -229,7 +221,7 @@ pre {
}
.typebot-checkbox.checked {
background-color: var(--typebot-button-bg-color);
background-color: rgb(var(--typebot-button-bg-rgb));
}
.typebot-host-bubble {
@ -237,22 +229,56 @@ pre {
}
.typebot-host-bubble > .bubble-typing {
background-color: var(--typebot-host-bubble-bg-color);
border: var(--typebot-host-bubble-border);
background-color: rgba(
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;
}
.typebot-guest-bubble {
color: var(--typebot-guest-bubble-color);
background-color: var(--typebot-guest-bubble-bg-color);
border-radius: 6px;
background-color: rgba(
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 {
color: var(--typebot-input-color);
background-color: var(--typebot-input-bg-color);
box-shadow: 0 2px 6px -1px rgba(0, 0, 0, 0.1);
border-radius: var(--typebot-border-radius);
background-color: rgba(
var(--typebot-input-bg-rgb),
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 {
@ -263,24 +289,20 @@ pre {
fill: var(--typebot-button-color);
}
.typebot-chat-view {
max-width: 900px;
}
.ping span {
background-color: var(--typebot-button-bg-color);
background-color: rgb(var(--typebot-button-bg-rgb));
}
.rating-icon-container svg {
width: 42px;
height: 42px;
stroke: var(--typebot-button-bg-color);
stroke: rgb(var(--typebot-button-bg-rgb));
fill: var(--typebot-host-bubble-bg-color);
transition: fill 100ms ease-out;
}
.rating-icon-container.selected svg {
fill: var(--typebot-button-bg-color);
fill: rgb(var(--typebot-button-bg-rgb));
}
.rating-icon-container:hover svg {
@ -292,59 +314,60 @@ pre {
}
.upload-progress-bar {
background-color: var(--typebot-button-bg-color);
border-radius: var(--typebot-border-radius);
background-color: rgb(var(--typebot-button-bg-rgb));
border-radius: var(--typebot-input-border-radius);
}
.total-files-indicator {
background-color: var(--typebot-button-bg-color);
background-color: rgb(var(--typebot-button-bg-rgb));
color: var(--typebot-button-color);
font-size: 10px;
}
.typebot-upload-input {
transition: border-color 100ms ease-out;
border-radius: var(--typebot-border-radius);
border-radius: var(--typebot-input-border-radius);
}
.typebot-upload-input.dragging-over {
border-color: var(--typebot-button-bg-color);
border-color: rgb(var(--typebot-button-bg-rgb));
}
.secondary-button {
background-color: var(--typebot-host-bubble-bg-color);
color: var(--typebot-host-bubble-color);
border-radius: var(--typebot-border-radius);
border-radius: var(--typebot-button-border-radius);
}
.typebot-country-select {
color: var(--typebot-input-color);
background-color: var(--typebot-input-bg-color);
border-radius: var(--typebot-border-radius);
border-radius: var(--typebot-button-border-radius);
}
.typebot-date-input {
color-scheme: light;
color: var(--typebot-input-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 {
border-radius: var(--typebot-border-radius);
border-radius: var(--typebot-input-border-radius);
}
.typebot-picture-button {
color: var(--typebot-button-color);
background-color: var(--typebot-button-bg-color);
border-radius: var(--typebot-border-radius);
background-color: rgb(var(--typebot-button-bg-rgb));
border-radius: var(--typebot-button-border-radius);
transition: all 0.3s ease;
width: 236px;
}
.typebot-picture-button > 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;
width: 100%;
max-height: 200px;
@ -362,14 +385,14 @@ pre {
.typebot-selectable-picture {
border: 1px solid
rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.25)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.25)
);
border-radius: var(--typebot-border-radius);
color: var(--typebot-container-color);
border-radius: var(--typebot-button-border-radius);
color: rgb(var(--typebot-chat-container-color));
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.08)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.08)
);
transition: all 0.3s ease;
width: 236px;
@ -377,23 +400,23 @@ pre {
.typebot-selectable-picture:hover {
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.12)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.12)
);
border-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.3)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.3)
);
}
.typebot-selectable-picture.selected {
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.18)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.18)
);
border-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.35)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.35)
);
}
@ -404,8 +427,8 @@ select option {
.typebot-progress-bar-container {
background-color: rgba(
var(--typebot-button-bg-color-rgb),
calc(var(--selectable-base-alpha) + 0.12)
var(--typebot-button-bg-rgb),
calc(var(--selectable-alpha-ratio) * 0.12)
);
height: var(--typebot-progress-bar-height);

View File

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

View File

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

View File

@ -284,7 +284,7 @@ export const ConversationContainer = (props: Props) => {
return (
<div
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()}>
{(chatChunk, index) => (

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import { isMobile } from '@/utils/isMobileSignal'
import type { TextInputBlock } from '@typebot.io/schemas'
import { createSignal, onCleanup, onMount } from 'solid-js'
import { defaultTextInputOptions } from '@typebot.io/schemas/features/blocks/inputs/text/constants'
import clsx from 'clsx'
type Props = {
block: TextInputBlock
@ -55,7 +56,10 @@ export const TextInput = (props: Props) => {
return (
<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"
style={{
'max-width': props.block.options?.isLong ? undefined : '350px',

View File

@ -1,6 +1,6 @@
import { isNotEmpty } from '@typebot.io/lib'
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 elementId = 'typebot-font'
@ -10,8 +10,7 @@ export const injectFont = (font: Font) => {
if (typeof font === 'string' || font.type === 'Google') {
const fontFamily =
(typeof font === 'string' ? font : font.family) ??
defaultTheme.general.font.family
(typeof font === 'string' ? font : font.family) ?? defaultFontFamily
if (existingFont?.getAttribute('href')?.includes(fontFamily)) return
existingFont?.remove()
const fontElement = document.createElement('link')

View File

@ -1,24 +1,51 @@
import {
Background,
ChatTheme,
ContainerColors,
ContainerBorderTheme,
ContainerTheme,
GeneralTheme,
InputColors,
InputTheme,
Theme,
} from '@typebot.io/schemas'
import { isLight, hexToRgb } from '@typebot.io/lib/hexToRgb'
import { isNotEmpty } from '@typebot.io/lib'
import { isDefined, isEmpty } from '@typebot.io/lib'
import {
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'
import { isChatContainerLight } from '@typebot.io/theme/isChatContainerLight'
const cssVariableNames = {
general: {
bgImage: '--typebot-container-bg-image',
bgColor: '--typebot-container-bg-color',
fontFamily: '--typebot-container-font-family',
color: '--typebot-container-color',
progressBar: {
position: '--typebot-progress-bar-position',
color: '--typebot-progress-bar-color',
@ -29,28 +56,67 @@ const cssVariableNames = {
},
},
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: {
bgColor: '--typebot-host-bubble-bg-color',
bgColor: '--typebot-host-bubble-bg-rgb',
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: {
bgColor: '--typebot-guest-bubble-bg-color',
bgColor: '--typebot-guest-bubble-bg-rgb',
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: {
bgColor: '--typebot-input-bg-color',
bgColor: '--typebot-input-bg-rgb',
color: '--typebot-input-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: {
bgColor: '--typebot-button-bg-color',
bgColorRgb: '--typebot-button-bg-color-rgb',
bgRgb: '--typebot-button-bg-rgb',
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: {
bgColor: '--typebot-checkbox-bg-color',
color: '--typebot-checkbox-color',
baseAlpha: '--selectable-base-alpha',
bgRgb: '--typebot-checkbox-bg-rgb',
alphaRatio: '--selectable-alpha-ratio',
},
},
} as const
@ -63,43 +129,31 @@ export const setCssVariablesValue = (
if (!theme) return
const documentStyle = container?.style
if (!documentStyle) return
setGeneralTheme(
theme.general ?? defaultTheme.general,
documentStyle,
isPreview
)
setChatTheme(theme.chat ?? defaultTheme.chat, documentStyle)
setGeneralTheme(theme.general, documentStyle, isPreview)
setChatTheme(theme.chat, theme.general?.background, documentStyle)
}
const setGeneralTheme = (
generalTheme: GeneralTheme,
generalTheme: GeneralTheme | undefined,
documentStyle: CSSStyleDeclaration,
isPreview?: boolean
) => {
setTypebotBackground(
generalTheme.background ?? defaultTheme.general.background,
documentStyle
)
setGeneralBackground(generalTheme?.background, documentStyle)
documentStyle.setProperty(
cssVariableNames.general.fontFamily,
(typeof generalTheme.font === 'string'
(typeof generalTheme?.font === 'string'
? generalTheme.font
: generalTheme.font?.family) ?? defaultTheme.general.font.family
)
setProgressBar(
generalTheme.progressBar ?? defaultTheme.general.progressBar,
documentStyle,
isPreview
: generalTheme?.font?.family) ?? defaultFontFamily
)
setProgressBar(generalTheme?.progressBar, documentStyle, isPreview)
}
const setProgressBar = (
progressBar: NonNullable<GeneralTheme['progressBar']>,
progressBar: GeneralTheme['progressBar'],
documentStyle: CSSStyleDeclaration,
isPreview?: boolean
) => {
const position =
progressBar.position ?? defaultTheme.general.progressBar.position
const position = progressBar?.position ?? defaultProgressBarPosition
documentStyle.setProperty(
cssVariableNames.general.progressBar.position,
@ -107,22 +161,20 @@ const setProgressBar = (
)
documentStyle.setProperty(
cssVariableNames.general.progressBar.color,
progressBar.color ?? defaultTheme.general.progressBar.color
progressBar?.color ?? defaultProgressBarColor
)
documentStyle.setProperty(
cssVariableNames.general.progressBar.colorRgb,
hexToRgb(
progressBar.backgroundColor ??
defaultTheme.general.progressBar.backgroundColor
progressBar?.backgroundColor ?? defaultProgressBarBackgroundColor
).join(', ')
)
documentStyle.setProperty(
cssVariableNames.general.progressBar.height,
`${progressBar.thickness ?? defaultTheme.general.progressBar.thickness}px`
`${progressBar?.thickness ?? defaultProgressBarThickness}px`
)
const placement =
progressBar.placement ?? defaultTheme.general.progressBar.placement
const placement = progressBar?.placement ?? defaultProgressBarPlacement
documentStyle.setProperty(
cssVariableNames.general.progressBar.top,
@ -136,123 +188,428 @@ const setProgressBar = (
}
const setChatTheme = (
chatTheme: ChatTheme,
chatTheme: ChatTheme | undefined,
generalBackground: GeneralTheme['background'],
documentStyle: CSSStyleDeclaration
) => {
setHostBubbles(
chatTheme.hostBubbles ?? defaultTheme.chat.hostBubbles,
documentStyle
setChatContainer(
chatTheme?.container,
generalBackground,
documentStyle,
chatTheme?.roundness
)
setGuestBubbles(
chatTheme.guestBubbles ?? defaultTheme.chat.guestBubbles,
documentStyle
setHostBubbles(chatTheme?.hostBubbles, documentStyle, chatTheme?.roundness)
setGuestBubbles(chatTheme?.guestBubbles, documentStyle, chatTheme?.roundness)
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)
setRoundness(
chatTheme.roundness ?? defaultTheme.chat.roundness,
documentStyle
documentStyle.setProperty(
cssVariableNames.chat.container.color,
hexToRgb(
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 = (
hostBubbles: ContainerColors,
documentStyle: CSSStyleDeclaration
hostBubbles: ContainerTheme | undefined,
documentStyle: CSSStyleDeclaration,
legacyRoundness?: ChatTheme['roundness']
) => {
documentStyle.setProperty(
cssVariableNames.chat.hostBubbles.bgColor,
hostBubbles.backgroundColor ?? defaultTheme.chat.hostBubbles.backgroundColor
hexToRgb(
hostBubbles?.backgroundColor ?? defaultHostBubblesBackgroundColor
).join(', ')
)
documentStyle.setProperty(
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 = (
guestBubbles: ContainerColors,
documentStyle: CSSStyleDeclaration
guestBubbles: ContainerTheme | undefined,
documentStyle: CSSStyleDeclaration,
legacyRoundness?: ChatTheme['roundness']
) => {
documentStyle.setProperty(
cssVariableNames.chat.guestBubbles.bgColor,
guestBubbles.backgroundColor ??
defaultTheme.chat.guestBubbles.backgroundColor
hexToRgb(
guestBubbles?.backgroundColor ?? defaultGuestBubblesBackgroundColor
).join(', ')
)
documentStyle.setProperty(
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 = (
buttons: ContainerColors,
documentStyle: CSSStyleDeclaration
buttons: ContainerTheme | undefined,
documentStyle: CSSStyleDeclaration,
legacyRoundness?: ChatTheme['roundness']
) => {
const bgColor =
buttons.backgroundColor ?? defaultTheme.chat.buttons.backgroundColor
documentStyle.setProperty(cssVariableNames.chat.buttons.bgColor, bgColor)
const bgColor = buttons?.backgroundColor ?? defaultButtonsBackgroundColor
documentStyle.setProperty(
cssVariableNames.chat.buttons.bgColorRgb,
cssVariableNames.chat.buttons.bgRgb,
hexToRgb(bgColor).join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.buttons.bgRgb,
hexToRgb(bgColor).join(', ')
)
documentStyle.setProperty(
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(
cssVariableNames.chat.inputs.bgColor,
inputs.backgroundColor ?? defaultTheme.chat.inputs.backgroundColor
hexToRgb(inputs?.backgroundColor ?? defaultInputsBackgroundColor).join(', ')
)
documentStyle.setProperty(
cssVariableNames.chat.inputs.color,
inputs.color ?? defaultTheme.chat.inputs.color
inputs?.color ?? defaultInputsColor
)
documentStyle.setProperty(
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 = (
background: Background,
const setCheckbox = (
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.setProperty(cssVariableNames.general.bgImage, null)
documentStyle.setProperty(cssVariableNames.general.bgColor, null)
documentStyle.setProperty(
background?.type === BackgroundType.IMAGE
(background?.type ?? defaultBackgroundType) === BackgroundType.IMAGE
? cssVariableNames.general.bgImage
: 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 = {}) => {
@ -261,25 +618,80 @@ const parseBackgroundValue = ({ type, content }: Background = {}) => {
return 'transparent'
case undefined:
case BackgroundType.COLOR:
return content ?? defaultTheme.general.background.content
return content ?? defaultBackgroundColor
case BackgroundType.IMAGE:
return `url(${content})`
}
}
const setRoundness = (
roundness: NonNullable<ChatTheme['roundness']>,
documentStyle: CSSStyleDeclaration
const setBorderRadius = (
border: ContainerBorderTheme,
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':
documentStyle.setProperty('--typebot-border-radius', '0')
documentStyle.setProperty(variableName, '0 0 #0000')
break
case 'medium':
documentStyle.setProperty('--typebot-border-radius', '6px')
case 'sm':
documentStyle.setProperty(variableName, '0 1px 2px 0 rgb(0 0 0 / 0.05)')
break
case 'large':
documentStyle.setProperty('--typebot-border-radius', '20px')
case 'md':
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
}
}

View File

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

View File

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

View File

@ -1,5 +1,3 @@
import { Theme } from './schema'
export enum BackgroundType {
COLOR = 'Color',
IMAGE = 'Image',
@ -11,37 +9,63 @@ export const fontTypes = ['Google', 'Custom'] as const
export const progressBarPlacements = ['Top', 'Bottom'] as const
export const progressBarPositions = ['fixed', 'absolute'] as const
export const defaultTheme = {
chat: {
roundness: 'medium',
hostBubbles: { backgroundColor: '#F7F8FF', color: '#303235' },
guestBubbles: { backgroundColor: '#FF8E21', color: '#FFFFFF' },
buttons: { backgroundColor: '#0042DA', color: '#FFFFFF' },
inputs: {
backgroundColor: '#FFFFFF',
color: '#303235',
placeholderColor: '#9095A0',
},
hostAvatar: {
isEnabled: true,
},
guestAvatar: {
isEnabled: false,
},
},
general: {
font: {
type: 'Google',
family: 'Open Sans',
},
background: { type: BackgroundType.COLOR, content: '#ffffff' },
progressBar: {
isEnabled: false,
color: '#0042DA',
backgroundColor: '#e0edff',
thickness: 4,
position: 'absolute',
placement: 'Top',
},
},
} as const satisfies Theme
export const shadows = ['none', 'sm', 'md', 'lg', 'xl', '2xl'] as const
export const borderRoundness = ['none', 'medium', 'large', 'custom'] as const
export const defaultLightTextColor = '#303235'
export const defaultDarkTextColor = '#FFFFFF'
/*---- General ----*/
// Font
export const defaultFontType = 'Google'
export const defaultFontFamily = 'Open Sans'
// Background
export const defaultBackgroundType = BackgroundType.COLOR
export const defaultBackgroundColor = '#ffffff'
// Progress bar
export const defaultProgressBarIsEnabled = false
export const defaultProgressBarColor = '#0042DA'
export const defaultProgressBarBackgroundColor = '#e0edff'
export const defaultProgressBarThickness = 4
export const defaultProgressBarPosition = 'absolute'
export const defaultProgressBarPlacement = 'Top'
export const defaultRoundness = 'medium'
export const defaultOpacity = 1
export const defaultBlur = 0
/*---- Chat ----*/
// Container
export const defaultContainerMaxWidth = '800px'
export const defaultContainerMaxHeight = '100%'
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 {
BackgroundType,
borderRoundness,
fontTypes,
progressBarPlacements,
progressBarPositions,
shadows,
} from './constants'
const avatarPropsSchema = z.object({
@ -12,25 +14,48 @@ const avatarPropsSchema = z.object({
url: z.string().optional(),
})
const containerColorsSchema = z.object({
backgroundColor: z.string().optional(),
const containerBorderThemeSchema = z.object({
thickness: z.number().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(
z.object({
placeholderColor: z.string().optional(),
export type ContainerBorderTheme = z.infer<typeof containerBorderThemeSchema>
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({
container: chatContainerSchema.optional(),
hostAvatar: avatarPropsSchema.optional(),
guestAvatar: avatarPropsSchema.optional(),
hostBubbles: containerColorsSchema.optional(),
guestBubbles: containerColorsSchema.optional(),
buttons: containerColorsSchema.optional(),
inputs: inputColorsSchema.optional(),
roundness: z.enum(['none', 'medium', 'large']).optional(),
hostBubbles: containerThemeSchema.optional(),
guestBubbles: containerThemeSchema.optional(),
buttons: containerThemeSchema.optional(),
inputs: inputThemeSchema.optional(),
roundness: z
.enum(['none', 'medium', 'large'])
.optional()
.describe('Deprecated, use `container.border.roundeness` instead'),
})
const backgroundSchema = z.object({
@ -98,6 +123,6 @@ export type ChatTheme = z.infer<typeof chatThemeSchema>
export type AvatarProps = z.infer<typeof avatarPropsSchema>
export type GeneralTheme = z.infer<typeof generalThemeSchema>
export type Background = z.infer<typeof backgroundSchema>
export type ContainerColors = z.infer<typeof containerColorsSchema>
export type InputColors = z.infer<typeof inputColorsSchema>
export type ContainerTheme = z.infer<typeof containerThemeSchema>
export type InputTheme = z.infer<typeof inputThemeSchema>
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':
specifier: workspace:*
version: link:../../packages/embeds/nextjs
'@typebot.io/theme':
specifier: workspace:*
version: link:../../packages/theme
'@udecode/cn':
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)
@ -1048,6 +1051,9 @@ importers:
'@typebot.io/schemas':
specifier: workspace:*
version: link:../../schemas
'@typebot.io/theme':
specifier: workspace:*
version: link:../../theme
'@typebot.io/tsconfig':
specifier: workspace:*
version: link:../../tsconfig
@ -1925,6 +1931,15 @@ importers:
specifier: workspace:*
version: link:../tsconfig
packages/theme:
dependencies:
'@typebot.io/lib':
specifier: workspace:*
version: link:../lib
'@typebot.io/schemas':
specifier: workspace:*
version: link:../schemas
packages/transactional:
dependencies:
'@react-email/components':