diff --git a/.env.dev.example b/.env.dev.example index cb029f9bc..532c58d04 100644 --- a/.env.dev.example +++ b/.env.dev.example @@ -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 diff --git a/.vscode/settings.json b/.vscode/settings.json index aa7b8308b..45e1d931e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" diff --git a/apps/builder/package.json b/apps/builder/package.json index 56ce27422..ff59b75f0 100644 --- a/apps/builder/package.json +++ b/apps/builder/package.json @@ -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", diff --git a/apps/builder/src/components/ColorPicker.tsx b/apps/builder/src/components/ColorPicker.tsx index 5dfa8b679..9fe01a6b0 100644 --- a/apps/builder/src/components/ColorPicker.tsx +++ b/apps/builder/src/components/ColorPicker.tsx @@ -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} > diff --git a/apps/builder/src/components/ImageUploadContent/UnsplashPicker.tsx b/apps/builder/src/components/ImageUploadContent/UnsplashPicker.tsx index 219db0101..d2c043ad3 100644 --- a/apps/builder/src/components/ImageUploadContent/UnsplashPicker.tsx +++ b/apps/builder/src/components/ImageUploadContent/UnsplashPicker.tsx @@ -141,6 +141,8 @@ export const UnsplashPicker = ({ imageSize, onImageSelect }: Props) => { fetchNewImages(query, 0) }} withVariableButton={false} + debounceTimeout={500} + forceDebounce /> 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(() => { diff --git a/apps/builder/src/features/preview/components/PreviewDrawer.tsx b/apps/builder/src/features/preview/components/PreviewDrawer.tsx index ee5acade9..82d3431d3 100644 --- a/apps/builder/src/features/preview/components/PreviewDrawer.tsx +++ b/apps/builder/src/features/preview/components/PreviewDrawer.tsx @@ -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` }} > ({ button: { backgroundColor: typebot?.theme.chat?.buttons?.backgroundColor ?? - defaultTheme.chat.buttons.backgroundColor, + defaultButtonsBackgroundColor, }, }) diff --git a/apps/builder/src/features/theme/components/DefaultAvatar.tsx b/apps/builder/src/features/theme/components/DefaultAvatar.tsx index 277320c14..79a013fe4 100644 --- a/apps/builder/src/features/theme/components/DefaultAvatar.tsx +++ b/apps/builder/src/features/theme/components/DefaultAvatar.tsx @@ -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} > diff --git a/apps/builder/src/features/theme/components/ThemeSideMenu.tsx b/apps/builder/src/features/theme/components/ThemeSideMenu.tsx index de11331a6..d395131a8 100644 --- a/apps/builder/src/features/theme/components/ThemeSideMenu.tsx +++ b/apps/builder/src/features/theme/components/ThemeSideMenu.tsx @@ -35,7 +35,7 @@ export const ThemeSideMenu = () => { typebot && updateTypebot({ updates: { theme: { ...typebot.theme, customCss } } }) - const selectedTemplate = ( + const selectTemplate = ( selectedTemplate: Partial> ) => { if (!typebot) return @@ -56,6 +56,8 @@ export const ThemeSideMenu = () => { }, }) + const templateId = typebot?.selectedThemeTemplateId ?? undefined + return ( { {typebot && ( )} @@ -106,6 +106,7 @@ export const ThemeSideMenu = () => { {typebot && ( { {typebot && ( )} @@ -147,6 +150,7 @@ export const ThemeSideMenu = () => { {typebot && ( diff --git a/apps/builder/src/features/theme/components/ThemeTemplateCard.tsx b/apps/builder/src/features/theme/components/ThemeTemplateCard.tsx index 5a6574de1..dbdd69473 100644 --- a/apps/builder/src/features/theme/components/ThemeTemplateCard.tsx +++ b/apps/builder/src/features/theme/components/ThemeTemplateCard.tsx @@ -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 ( 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 ( + + + + Max width: + + + + + + + + + + Max height: + + + + + + + + + + ) +} + +const parseValueAndUnit = (valueWithUnit: string) => { + const value = parseFloat(valueWithUnit) + const unit = valueWithUnit.replace(value.toString(), '') + return { value, unit } +} diff --git a/apps/builder/src/features/theme/components/chat/ChatThemeSettings.tsx b/apps/builder/src/features/theme/components/chat/ChatThemeSettings.tsx index 8e85b2813..c4fa0517a 100644 --- a/apps/builder/src/features/theme/components/chat/ChatThemeSettings.tsx +++ b/apps/builder/src/features/theme/components/chat/ChatThemeSettings.tsx @@ -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['inputs']) => onChatThemeChange({ ...chatTheme, inputs }) + const updateChatContainer = ( + container: NonNullable['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 ( + + Container + + @@ -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} /> {t('theme.sideMenu.chat.botBubbles')} - {t('theme.sideMenu.chat.userBubbles')} - {t('theme.sideMenu.chat.buttons')} - {t('theme.sideMenu.chat.inputs')} - - - - - {t('theme.sideMenu.chat.cornersRoundness')} - - , - value: 'none', + , - value: 'medium', - }, - { - label: , - value: 'large', - }, - ]} - value={chatTheme?.roundness ?? defaultTheme.chat.roundness} - onSelect={(roundness) => - onChatThemeChange({ ...chatTheme, roundness }) - } + }} /> diff --git a/apps/builder/src/features/theme/components/chat/ContainerThemeForm.tsx b/apps/builder/src/features/theme/components/chat/ContainerThemeForm.tsx new file mode 100644 index 000000000..cb59de878 --- /dev/null +++ b/apps/builder/src/features/theme/components/chat/ContainerThemeForm.tsx @@ -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 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) => { + 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 ( + + + + {t('theme.sideMenu.chat.theme.background')} + + + + + + + + + + {t('theme.sideMenu.chat.theme.text')} + + + + {onPlaceholderColorChange && ( + + + {t('theme.sideMenu.chat.theme.placeholder')} + + + + )} + + + + + Border + + + + + + + + + Advanced + + + + {backgroundColor !== 'transparent' && ( + <> + + {(theme?.opacity ?? defaultTheme?.opacity) !== 1 && ( + + )} + + )} + + + Shadow: + + + + + + + + + + ) +} + +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 ( + + + + Roundness: + + + + {(border?.roundeness ?? defaultBorder?.roundeness) === 'custom' && ( + + )} + + + + + + Thickness: + + + + + {thickness > 0 && ( + <> + + + Color: + + + + + + )} + + ) +} diff --git a/apps/builder/src/features/theme/components/chat/GuestBubbles.tsx b/apps/builder/src/features/theme/components/chat/GuestBubbles.tsx index 8661230a7..8ebab7ac4 100644 --- a/apps/builder/src/features/theme/components/chat/GuestBubbles.tsx +++ b/apps/builder/src/features/theme/components/chat/GuestBubbles.tsx @@ -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) => { {t('theme.sideMenu.chat.theme.background')} @@ -34,7 +36,7 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => { {t('theme.sideMenu.chat.theme.text')} diff --git a/apps/builder/src/features/theme/components/chat/HostBubbles.tsx b/apps/builder/src/features/theme/components/chat/HostBubbles.tsx index 98c5adfbf..cf81f1941 100644 --- a/apps/builder/src/features/theme/components/chat/HostBubbles.tsx +++ b/apps/builder/src/features/theme/components/chat/HostBubbles.tsx @@ -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) => { {t('theme.sideMenu.chat.theme.background')} @@ -33,7 +35,7 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => { {t('theme.sideMenu.chat.theme.text')} diff --git a/apps/builder/src/features/theme/components/chat/InputsTheme.tsx b/apps/builder/src/features/theme/components/chat/InputsTheme.tsx deleted file mode 100644 index 9fb574e5b..000000000 --- a/apps/builder/src/features/theme/components/chat/InputsTheme.tsx +++ /dev/null @@ -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['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 ( - - - {t('theme.sideMenu.chat.theme.background')} - - - - {t('theme.sideMenu.chat.theme.text')} - - - - {t('theme.sideMenu.chat.theme.placeholder')} - - - - ) -} diff --git a/apps/builder/src/features/theme/components/general/BackgroundContent.tsx b/apps/builder/src/features/theme/components/general/BackgroundContent.tsx index 483074b37..7aa5ed0f2 100644 --- a/apps/builder/src/features/theme/components/general/BackgroundContent.tsx +++ b/apps/builder/src/features/theme/components/general/BackgroundContent.tsx @@ -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 ( @@ -76,15 +74,12 @@ export const BackgroundContent = ({ ) } - if ( - (background?.type ?? defaultTheme.general.background.type) === - BackgroundType.COLOR - ) { + if ((background?.type ?? defaultBackgroundType) === BackgroundType.COLOR) { return ( {t('theme.sideMenu.global.background.color')} diff --git a/apps/builder/src/features/theme/components/general/BackgroundSelector.tsx b/apps/builder/src/features/theme/components/general/BackgroundSelector.tsx index 2bbb4b322..d0d4133c0 100644 --- a/apps/builder/src/features/theme/components/general/BackgroundSelector.tsx +++ b/apps/builder/src/features/theme/components/general/BackgroundSelector.tsx @@ -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 ( - {t('theme.sideMenu.global.background')} @@ -115,23 +119,46 @@ export const GeneralSettings = ({ onChange={updateBranding} /> - - - {t('theme.sideMenu.global.font')} - - - - + + + + Progress Bar + + + + + + + + + {t('theme.sideMenu.global.font')} + + + + + + + + + + {t('theme.sideMenu.global.background')} + + + + + + + ) } diff --git a/apps/builder/src/features/theme/components/general/GoogleFontForm.tsx b/apps/builder/src/features/theme/components/general/GoogleFontForm.tsx index b601139e5..37bf42315 100644 --- a/apps/builder/src/features/theme/components/general/GoogleFontForm.tsx +++ b/apps/builder/src/features/theme/components/general/GoogleFontForm.tsx @@ -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([]) diff --git a/apps/builder/src/features/theme/components/general/ProgressBarForm.tsx b/apps/builder/src/features/theme/components/general/ProgressBarForm.tsx index 2dc2f55a8..f290649ed 100644 --- a/apps/builder/src/features/theme/components/general/ProgressBarForm.tsx +++ b/apps/builder/src/features/theme/components/general/ProgressBarForm.tsx @@ -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 ( @@ -58,9 +58,7 @@ export const ProgressBarForm = ({ Color: @@ -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} /> diff --git a/apps/builder/src/features/theme/theme.spec.ts b/apps/builder/src/features/theme/theme.spec.ts index 1bb26f04a..8513a9247 100644 --- a/apps/builder/src/features/theme/theme.spec.ts +++ b/apps/builder/src/features/theme/theme.spec.ts @@ -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') diff --git a/apps/docs/openapi/builder.json b/apps/docs/openapi/builder.json index a9a77f9ce..00a19d87d 100644 --- a/apps/docs/openapi/builder.json +++ b/apps/docs/openapi/builder.json @@ -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" } } }, diff --git a/apps/docs/openapi/viewer.json b/apps/docs/openapi/viewer.json index 61256d9dc..7010fb9ec 100644 --- a/apps/docs/openapi/viewer.json +++ b/apps/docs/openapi/viewer.json @@ -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" } } }, diff --git a/apps/viewer/src/pages/[[...publicId]].tsx b/apps/viewer/src/pages/[[...publicId]].tsx index 5726cfae9..c2df30d1e 100644 --- a/apps/viewer/src/pages/[[...publicId]].tsx +++ b/apps/viewer/src/pages/[[...publicId]].tsx @@ -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} diff --git a/packages/bot-engine/startSession.ts b/packages/bot-engine/startSession.ts index 76333a497..845ff33dd 100644 --- a/packages/bot-engine/startSession.ts +++ b/packages/bot-engine/startSession.ts @@ -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('{{')) diff --git a/packages/deprecated/bot-engine/src/features/theme/utils/setCssVariablesValue.ts b/packages/deprecated/bot-engine/src/features/theme/utils/setCssVariablesValue.ts index e0ce0f698..2068bdad1 100644 --- a/packages/deprecated/bot-engine/src/features/theme/utils/setCssVariablesValue.ts +++ b/packages/deprecated/bot-engine/src/features/theme/utils/setCssVariablesValue.ts @@ -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, diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index 04e6069ff..e836acc6d 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -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", diff --git a/packages/embeds/js/src/assets/index.css b/packages/embeds/js/src/assets/index.css index b7de5455a..416f02736 100644 --- a/packages/embeds/js/src/assets/index.css +++ b/packages/embeds/js/src/assets/index.css @@ -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); diff --git a/packages/embeds/js/src/components/Bot.tsx b/packages/embeds/js/src/components/Bot.tsx index 6e4896fc5..37e4a1f3f 100644 --- a/packages/embeds/js/src/components/Bot.tsx +++ b/packages/embeds/js/src/components/Bot.tsx @@ -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) => {
@@ -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={} > @@ -306,7 +311,7 @@ const BotContent = (props: BotContentProps) => { -
+
& { theme: Theme @@ -77,7 +80,7 @@ export const ChatChunk = (props: Props) => { 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) => { { style={{ 'max-width': props.theme.chat?.hostAvatar?.isEnabled ?? - defaultTheme.chat.hostAvatar.isEnabled + defaultHostAvatarIsEnabled ? isMobile() ? 'calc(100% - 32px - 32px)' : 'calc(100% - 48px - 48px)' diff --git a/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx b/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx index 7ca2e6aa2..8b03a431a 100644 --- a/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx +++ b/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx @@ -284,7 +284,7 @@ export const ConversationContainer = (props: Props) => { return (
{(chatChunk, index) => ( diff --git a/packages/embeds/js/src/components/ConversationContainer/LoadingChunk.tsx b/packages/embeds/js/src/components/ConversationContainer/LoadingChunk.tsx index 3011d6a15..0304fbb0c 100644 --- a/packages/embeds/js/src/components/ConversationContainer/LoadingChunk.tsx +++ b/packages/embeds/js/src/components/ConversationContainer/LoadingChunk.tsx @@ -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) => ( { diff --git a/packages/embeds/js/src/features/blocks/inputs/rating/components/RatingForm.tsx b/packages/embeds/js/src/features/blocks/inputs/rating/components/RatingForm.tsx index 91cbba120..0eff01ea4 100644 --- a/packages/embeds/js/src/features/blocks/inputs/rating/components/RatingForm.tsx +++ b/packages/embeds/js/src/features/blocks/inputs/rating/components/RatingForm.tsx @@ -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' } > - + + + + + + { return (
{ 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') diff --git a/packages/embeds/js/src/utils/setCssVariablesValue.ts b/packages/embeds/js/src/utils/setCssVariablesValue.ts index 417339a45..2e7bb059c 100644 --- a/packages/embeds/js/src/utils/setCssVariablesValue.ts +++ b/packages/embeds/js/src/utils/setCssVariablesValue.ts @@ -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, + 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, - 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 } } diff --git a/packages/embeds/nextjs/package.json b/packages/embeds/nextjs/package.json index 2aa2ff854..1562082e3 100644 --- a/packages/embeds/nextjs/package.json +++ b/packages/embeds/nextjs/package.json @@ -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", diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index d80f3ff6b..1f54f82f7 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -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", diff --git a/packages/schemas/features/typebot/theme/constants.ts b/packages/schemas/features/typebot/theme/constants.ts index 78ca74600..b2b5bccb2 100644 --- a/packages/schemas/features/typebot/theme/constants.ts +++ b/packages/schemas/features/typebot/theme/constants.ts @@ -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 diff --git a/packages/schemas/features/typebot/theme/schema.ts b/packages/schemas/features/typebot/theme/schema.ts index 2d99176a6..d5cb86ace 100644 --- a/packages/schemas/features/typebot/theme/schema.ts +++ b/packages/schemas/features/typebot/theme/schema.ts @@ -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 + +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 export type AvatarProps = z.infer export type GeneralTheme = z.infer export type Background = z.infer -export type ContainerColors = z.infer -export type InputColors = z.infer +export type ContainerTheme = z.infer +export type InputTheme = z.infer export type ThemeTemplate = z.infer diff --git a/packages/theme/isChatContainerLight.ts b/packages/theme/isChatContainerLight.ts new file mode 100644 index 000000000..78662cfbe --- /dev/null +++ b/packages/theme/isChatContainerLight.ts @@ -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) +} diff --git a/packages/theme/package.json b/packages/theme/package.json new file mode 100644 index 000000000..c9caec17a --- /dev/null +++ b/packages/theme/package.json @@ -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:*" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a612ca09..eb173c666 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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':