feat(theme): ✨ Add chat theme settings
This commit is contained in:
@ -17,7 +17,7 @@ export const SettingsContent = () => {
|
||||
<Flex h="full" w="full" justifyContent="center" align="flex-start">
|
||||
<Stack p="6" rounded="md" borderWidth={1} w="600px" minH="500px" mt={10}>
|
||||
<TypingEmulation
|
||||
typingEmulation={typebot?.settings.typingEmulation}
|
||||
typingEmulation={typebot?.settings?.typingEmulation}
|
||||
onUpdate={handleTypingEmulationUpdate}
|
||||
/>
|
||||
</Stack>
|
||||
|
38
apps/builder/components/theme/ChatSettings/ButtonsTheme.tsx
Normal file
38
apps/builder/components/theme/ChatSettings/ButtonsTheme.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { Stack, Flex, Text } from '@chakra-ui/react'
|
||||
import { ContainerColors } from 'models'
|
||||
import React from 'react'
|
||||
import { ColorPicker } from '../GeneralSettings/ColorPicker'
|
||||
|
||||
type Props = {
|
||||
buttons?: ContainerColors
|
||||
onButtonsChange: (buttons: ContainerColors) => void
|
||||
}
|
||||
|
||||
const defaultBackgroundColor = '#0042da'
|
||||
const defaultTextColor = '#ffffff'
|
||||
|
||||
export const ButtonsTheme = ({ buttons, onButtonsChange }: Props) => {
|
||||
const handleBackgroundChange = (backgroundColor: string) =>
|
||||
onButtonsChange({ ...buttons, backgroundColor })
|
||||
const handleTextChange = (color: string) =>
|
||||
onButtonsChange({ ...buttons, color })
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
initialColor={buttons?.backgroundColor ?? defaultBackgroundColor}
|
||||
onColorChange={handleBackgroundChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={buttons?.color ?? defaultTextColor}
|
||||
onColorChange={handleTextChange}
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
import { Heading, Stack } from '@chakra-ui/react'
|
||||
import { ChatTheme, ContainerColors, InputColors } from 'models'
|
||||
import React from 'react'
|
||||
import { ButtonsTheme } from './ButtonsTheme'
|
||||
import { GuestBubbles } from './GuestBubbles'
|
||||
import { HostBubbles } from './HostBubbles'
|
||||
import { InputsTheme } from './InputsTheme'
|
||||
|
||||
type Props = {
|
||||
chatTheme?: ChatTheme
|
||||
onChatThemeChange: (chatTheme: ChatTheme) => void
|
||||
}
|
||||
|
||||
export const ChatThemeSettings = ({ chatTheme, onChatThemeChange }: Props) => {
|
||||
const handleHostBubblesChange = (hostBubbles: ContainerColors) =>
|
||||
onChatThemeChange({ ...chatTheme, hostBubbles })
|
||||
const handleGuestBubblesChange = (guestBubbles: ContainerColors) =>
|
||||
onChatThemeChange({ ...chatTheme, guestBubbles })
|
||||
const handleButtonsChange = (buttons: ContainerColors) =>
|
||||
onChatThemeChange({ ...chatTheme, buttons })
|
||||
const handleInputsChange = (inputs: InputColors) =>
|
||||
onChatThemeChange({ ...chatTheme, inputs })
|
||||
|
||||
return (
|
||||
<Stack spacing={6}>
|
||||
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
||||
<Heading fontSize="lg">Bot bubbles</Heading>
|
||||
<HostBubbles
|
||||
hostBubbles={chatTheme?.hostBubbles}
|
||||
onHostBubblesChange={handleHostBubblesChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
||||
<Heading fontSize="lg">User bubbles</Heading>
|
||||
<GuestBubbles
|
||||
guestBubbles={chatTheme?.guestBubbles}
|
||||
onGuestBubblesChange={handleGuestBubblesChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
||||
<Heading fontSize="lg">Buttons</Heading>
|
||||
<ButtonsTheme
|
||||
buttons={chatTheme?.buttons}
|
||||
onButtonsChange={handleButtonsChange}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
||||
<Heading fontSize="lg">Inputs</Heading>
|
||||
<InputsTheme
|
||||
inputs={chatTheme?.inputs}
|
||||
onInputsChange={handleInputsChange}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)
|
||||
}
|
37
apps/builder/components/theme/ChatSettings/GuestBubbles.tsx
Normal file
37
apps/builder/components/theme/ChatSettings/GuestBubbles.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Stack, Flex, Text } from '@chakra-ui/react'
|
||||
import { ContainerColors } from 'models'
|
||||
import React from 'react'
|
||||
import { ColorPicker } from '../GeneralSettings/ColorPicker'
|
||||
|
||||
type Props = {
|
||||
guestBubbles?: ContainerColors
|
||||
onGuestBubblesChange: (hostBubbles: ContainerColors) => void
|
||||
}
|
||||
|
||||
const defaultBackgroundColor = '#ff8e21'
|
||||
const defaultTextColor = '#ffffff'
|
||||
export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
|
||||
const handleBackgroundChange = (backgroundColor: string) =>
|
||||
onGuestBubblesChange({ ...guestBubbles, backgroundColor })
|
||||
const handleTextChange = (color: string) =>
|
||||
onGuestBubblesChange({ ...guestBubbles, color })
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
initialColor={guestBubbles?.backgroundColor ?? defaultBackgroundColor}
|
||||
onColorChange={handleBackgroundChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={guestBubbles?.color ?? defaultTextColor}
|
||||
onColorChange={handleTextChange}
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)
|
||||
}
|
38
apps/builder/components/theme/ChatSettings/HostBubbles.tsx
Normal file
38
apps/builder/components/theme/ChatSettings/HostBubbles.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { Stack, Flex, Text } from '@chakra-ui/react'
|
||||
import { ContainerColors } from 'models'
|
||||
import React from 'react'
|
||||
import { ColorPicker } from '../GeneralSettings/ColorPicker'
|
||||
|
||||
type Props = {
|
||||
hostBubbles?: ContainerColors
|
||||
onHostBubblesChange: (hostBubbles: ContainerColors) => void
|
||||
}
|
||||
|
||||
const defaultBackgroundColor = '#f7f8ff'
|
||||
const defaultTextColor = '#303235'
|
||||
|
||||
export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
|
||||
const handleBackgroundChange = (backgroundColor: string) =>
|
||||
onHostBubblesChange({ ...hostBubbles, backgroundColor })
|
||||
const handleTextChange = (color: string) =>
|
||||
onHostBubblesChange({ ...hostBubbles, color })
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
initialColor={hostBubbles?.backgroundColor ?? defaultBackgroundColor}
|
||||
onColorChange={handleBackgroundChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={hostBubbles?.color ?? defaultTextColor}
|
||||
onColorChange={handleTextChange}
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)
|
||||
}
|
48
apps/builder/components/theme/ChatSettings/InputsTheme.tsx
Normal file
48
apps/builder/components/theme/ChatSettings/InputsTheme.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { Stack, Flex, Text } from '@chakra-ui/react'
|
||||
import { InputColors } from 'models'
|
||||
import React from 'react'
|
||||
import { ColorPicker } from '../GeneralSettings/ColorPicker'
|
||||
|
||||
type Props = {
|
||||
inputs?: InputColors
|
||||
onInputsChange: (buttons: InputColors) => void
|
||||
}
|
||||
|
||||
const defaultBackgroundColor = '#ffffff'
|
||||
const defaultTextColor = '#303235'
|
||||
const defaultPlaceholderColor = '#9095A0'
|
||||
|
||||
export const InputsTheme = ({ inputs, onInputsChange }: Props) => {
|
||||
const handleBackgroundChange = (backgroundColor: string) =>
|
||||
onInputsChange({ ...inputs, backgroundColor })
|
||||
const handleTextChange = (color: string) =>
|
||||
onInputsChange({ ...inputs, color })
|
||||
const handlePlaceholderChange = (placeholderColor: string) =>
|
||||
onInputsChange({ ...inputs, placeholderColor })
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
initialColor={inputs?.backgroundColor ?? defaultBackgroundColor}
|
||||
onColorChange={handleBackgroundChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={inputs?.color ?? defaultTextColor}
|
||||
onColorChange={handleTextChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Placeholder text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={inputs?.placeholderColor ?? defaultPlaceholderColor}
|
||||
onColorChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Flex>
|
||||
</Stack>
|
||||
)
|
||||
}
|
1
apps/builder/components/theme/ChatSettings/index.tsx
Normal file
1
apps/builder/components/theme/ChatSettings/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { ChatThemeSettings } from './ChatThemeSettings'
|
@ -1,64 +0,0 @@
|
||||
import {
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
HStack,
|
||||
Heading,
|
||||
AccordionIcon,
|
||||
AccordionPanel,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { PencilIcon } from 'assets/icons'
|
||||
import { Background } from 'models'
|
||||
import { useTypebot } from 'contexts/TypebotContext/TypebotContext'
|
||||
import React from 'react'
|
||||
import { BackgroundSelector } from './BackgroundSelector'
|
||||
import { FontSelector } from './FontSelector'
|
||||
|
||||
export const GeneralContent = () => {
|
||||
const { typebot, updateTypebot } = useTypebot()
|
||||
|
||||
const handleSelectFont = (font: string) => {
|
||||
if (!typebot) return
|
||||
updateTypebot({
|
||||
theme: {
|
||||
...typebot.theme,
|
||||
general: { ...typebot.theme.general, font },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleBackgroundChange = (background: Background) => {
|
||||
if (!typebot) return
|
||||
updateTypebot({
|
||||
theme: {
|
||||
...typebot.theme,
|
||||
general: { ...typebot.theme.general, background },
|
||||
},
|
||||
})
|
||||
}
|
||||
return (
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<PencilIcon />
|
||||
<Heading fontSize="lg" color="gray.700">
|
||||
General
|
||||
</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
{typebot && (
|
||||
<AccordionPanel as={Stack} pb={4} spacing={6}>
|
||||
<FontSelector
|
||||
activeFont={typebot.theme.general.font}
|
||||
onSelectFont={handleSelectFont}
|
||||
/>
|
||||
<BackgroundSelector
|
||||
initialBackground={typebot.theme.general.background}
|
||||
onBackgroundChange={handleBackgroundChange}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
)}
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { GeneralContent } from './GeneralContent'
|
@ -4,10 +4,12 @@ import React from 'react'
|
||||
import { ColorPicker } from '../ColorPicker'
|
||||
|
||||
type BackgroundContentProps = {
|
||||
background: Background
|
||||
background?: Background
|
||||
onBackgroundContentChange: (content: string) => void
|
||||
}
|
||||
|
||||
const defaultBackgroundColor = '#ffffff'
|
||||
|
||||
export const BackgroundContent = ({
|
||||
background,
|
||||
onBackgroundContentChange,
|
||||
@ -15,13 +17,13 @@ export const BackgroundContent = ({
|
||||
const handleContentChange = (content: string) =>
|
||||
onBackgroundContentChange(content)
|
||||
|
||||
switch (background.type) {
|
||||
switch (background?.type) {
|
||||
case BackgroundType.COLOR:
|
||||
return (
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background color:</Text>
|
||||
<ColorPicker
|
||||
initialColor={background.content}
|
||||
initialColor={background.content ?? defaultBackgroundColor}
|
||||
onColorChange={handleContentChange}
|
||||
/>
|
||||
</Flex>
|
@ -1,43 +1,35 @@
|
||||
import { Stack, Text } from '@chakra-ui/react'
|
||||
import { Background, BackgroundType } from 'models'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { BackgroundContent } from './BackgroundContent'
|
||||
import { BackgroundTypeRadioButtons } from './BackgroundTypeRadioButtons'
|
||||
|
||||
type Props = {
|
||||
initialBackground?: Background
|
||||
background?: Background
|
||||
onBackgroundChange: (newBackground: Background) => void
|
||||
}
|
||||
|
||||
const defaultBackgroundType = BackgroundType.NONE
|
||||
|
||||
export const BackgroundSelector = ({
|
||||
initialBackground,
|
||||
background,
|
||||
onBackgroundChange,
|
||||
}: Props) => {
|
||||
const [currentBackground, setCurrentBackground] = useState<Background>(
|
||||
initialBackground ?? { type: BackgroundType.NONE, content: '' }
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (deepEqual(currentBackground, initialBackground)) return
|
||||
onBackgroundChange(currentBackground)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentBackground])
|
||||
|
||||
const handleBackgroundTypeChange = (type: BackgroundType) =>
|
||||
setCurrentBackground({ ...currentBackground, type })
|
||||
background && onBackgroundChange({ ...background, type })
|
||||
|
||||
const handleBackgroundContentChange = (content: string) =>
|
||||
setCurrentBackground({ ...currentBackground, content })
|
||||
background && onBackgroundChange({ ...background, content })
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<Text>Background</Text>
|
||||
<BackgroundTypeRadioButtons
|
||||
backgroundType={currentBackground.type}
|
||||
backgroundType={background?.type ?? defaultBackgroundType}
|
||||
onBackgroundTypeChange={handleBackgroundTypeChange}
|
||||
/>
|
||||
<BackgroundContent
|
||||
background={currentBackground}
|
||||
background={background}
|
||||
onBackgroundContentChange={handleBackgroundContentChange}
|
||||
/>
|
||||
</Stack>
|
@ -0,0 +1,36 @@
|
||||
import { Stack } from '@chakra-ui/react'
|
||||
import { Background, BackgroundType, GeneralTheme } from 'models'
|
||||
import React from 'react'
|
||||
import { BackgroundSelector } from './BackgroundSelector'
|
||||
import { FontSelector } from './FontSelector'
|
||||
|
||||
type Props = {
|
||||
generalTheme?: GeneralTheme
|
||||
onGeneralThemeChange: (general: GeneralTheme) => void
|
||||
}
|
||||
|
||||
const defaultFont = 'Open Sans'
|
||||
|
||||
export const GeneralSettings = ({
|
||||
generalTheme,
|
||||
onGeneralThemeChange,
|
||||
}: Props) => {
|
||||
const handleSelectFont = (font: string) =>
|
||||
onGeneralThemeChange({ ...generalTheme, font })
|
||||
|
||||
const handleBackgroundChange = (background: Background) =>
|
||||
onGeneralThemeChange({ ...generalTheme, background })
|
||||
|
||||
return (
|
||||
<Stack spacing={6}>
|
||||
<FontSelector
|
||||
activeFont={generalTheme?.font ?? defaultFont}
|
||||
onSelectFont={handleSelectFont}
|
||||
/>
|
||||
<BackgroundSelector
|
||||
background={generalTheme?.background ?? { type: BackgroundType.NONE }}
|
||||
onBackgroundChange={handleBackgroundChange}
|
||||
/>
|
||||
</Stack>
|
||||
)
|
||||
}
|
1
apps/builder/components/theme/GeneralSettings/index.ts
Normal file
1
apps/builder/components/theme/GeneralSettings/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { GeneralSettings } from './GeneralSettings'
|
@ -8,26 +8,58 @@ import {
|
||||
HStack,
|
||||
Stack,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChatIcon, CodeIcon, LayoutIcon } from 'assets/icons'
|
||||
import { ChatIcon, CodeIcon, LayoutIcon, PencilIcon } from 'assets/icons'
|
||||
import { headerHeight } from 'components/shared/TypebotHeader'
|
||||
import { useTypebot } from 'contexts/TypebotContext'
|
||||
import { ChatTheme, GeneralTheme } from 'models'
|
||||
import React from 'react'
|
||||
import { GeneralContent } from './GeneralContent'
|
||||
import { ChatThemeSettings } from './ChatSettings'
|
||||
import { GeneralSettings } from './GeneralSettings'
|
||||
|
||||
export const SideMenu = () => {
|
||||
const { typebot, updateTypebot } = useTypebot()
|
||||
|
||||
const handleChatThemeChange = (chat: ChatTheme) =>
|
||||
updateTypebot({ theme: { ...typebot?.theme, chat } })
|
||||
|
||||
const handleGeneralThemeChange = (general: GeneralTheme) =>
|
||||
updateTypebot({ theme: { ...typebot?.theme, general } })
|
||||
|
||||
return (
|
||||
<Stack flex="1" maxW="400px" borderRightWidth={1} pt={10} spacing={10}>
|
||||
<Stack
|
||||
flex="1"
|
||||
maxW="400px"
|
||||
height={`calc(100vh - ${headerHeight}px)`}
|
||||
borderRightWidth={1}
|
||||
pt={10}
|
||||
spacing={10}
|
||||
overflowY="scroll"
|
||||
pb="20"
|
||||
>
|
||||
<Heading fontSize="xl" textAlign="center">
|
||||
Customize the theme
|
||||
</Heading>
|
||||
<Accordion allowMultiple allowToggle>
|
||||
<GeneralContent />
|
||||
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<PencilIcon />
|
||||
<Heading fontSize="lg">General</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4}>
|
||||
<GeneralSettings
|
||||
generalTheme={typebot?.theme?.general}
|
||||
onGeneralThemeChange={handleGeneralThemeChange}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<LayoutIcon />
|
||||
<Heading fontSize="lg" color="gray.700">
|
||||
Layout
|
||||
</Heading>
|
||||
<Heading fontSize="lg">Layout</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
@ -42,26 +74,22 @@ export const SideMenu = () => {
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<ChatIcon />
|
||||
<Heading fontSize="lg" color="gray.700">
|
||||
Chat
|
||||
</Heading>
|
||||
<Heading fontSize="lg">Chat</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
|
||||
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
|
||||
ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
|
||||
aliquip ex ea commodo consequat.
|
||||
<ChatThemeSettings
|
||||
chatTheme={typebot?.theme?.chat}
|
||||
onChatThemeChange={handleChatThemeChange}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<CodeIcon />
|
||||
<Heading fontSize="lg" color="gray.700">
|
||||
Custom CSS
|
||||
</Heading>
|
||||
<Heading fontSize="lg">Custom CSS</Heading>
|
||||
</HStack>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
|
148
apps/builder/cypress/fixtures/typebots/theme/theme.json
Normal file
148
apps/builder/cypress/fixtures/typebots/theme/theme.json
Normal file
@ -0,0 +1,148 @@
|
||||
{
|
||||
"id": "typebot4",
|
||||
"createdAt": "2022-01-21T07:55:14.727Z",
|
||||
"updatedAt": "2022-01-21T07:55:14.727Z",
|
||||
"name": "My typebot",
|
||||
"ownerId": "user2",
|
||||
"publishedTypebotId": null,
|
||||
"folderId": null,
|
||||
"blocks": {
|
||||
"byId": {
|
||||
"3kH2sUjVThQDWmqdoKnGk5": {
|
||||
"id": "3kH2sUjVThQDWmqdoKnGk5",
|
||||
"title": "Start",
|
||||
"stepIds": ["oxTsU2C1RX5QHuyY8qjHAM"],
|
||||
"graphCoordinates": { "x": 42, "y": 13 }
|
||||
},
|
||||
"bdFW2HHjMoEFmqHtFre9Xi8": {
|
||||
"id": "bdFW2HHjMoEFmqHtFre9Xi8",
|
||||
"title": "Block #2",
|
||||
"stepIds": ["sgkADMK25y9P9V3vjwjBaac", "ssEiEECKSFkA44dGDceHxKw"],
|
||||
"graphCoordinates": { "x": 121, "y": 227 }
|
||||
},
|
||||
"bhKHKi1SQb5woZEy1y4fNsJ": {
|
||||
"id": "bhKHKi1SQb5woZEy1y4fNsJ",
|
||||
"title": "Block #3",
|
||||
"graphCoordinates": { "x": 605, "y": 454 },
|
||||
"stepIds": ["sseUQEWCMdiZquk8EbxHYtk"]
|
||||
}
|
||||
},
|
||||
"allIds": [
|
||||
"3kH2sUjVThQDWmqdoKnGk5",
|
||||
"bdFW2HHjMoEFmqHtFre9Xi8",
|
||||
"bhKHKi1SQb5woZEy1y4fNsJ"
|
||||
]
|
||||
},
|
||||
"steps": {
|
||||
"byId": {
|
||||
"oxTsU2C1RX5QHuyY8qjHAM": {
|
||||
"id": "oxTsU2C1RX5QHuyY8qjHAM",
|
||||
"type": "start",
|
||||
"label": "Start",
|
||||
"edgeId": "25yX9DnQgdafpdAjfAu5Fp",
|
||||
"blockId": "3kH2sUjVThQDWmqdoKnGk5"
|
||||
},
|
||||
"sgkADMK25y9P9V3vjwjBaac": {
|
||||
"id": "sgkADMK25y9P9V3vjwjBaac",
|
||||
"type": "text",
|
||||
"blockId": "bdFW2HHjMoEFmqHtFre9Xi8",
|
||||
"content": {
|
||||
"html": "<div>Ready?</div>",
|
||||
"richText": [{ "type": "p", "children": [{ "text": "Ready?" }] }],
|
||||
"plainText": "Ready?"
|
||||
}
|
||||
},
|
||||
"ssEiEECKSFkA44dGDceHxKw": {
|
||||
"id": "ssEiEECKSFkA44dGDceHxKw",
|
||||
"type": "choice input",
|
||||
"edgeId": "6e4Sbp8pGTvBQYtCk2qXbN",
|
||||
"blockId": "bdFW2HHjMoEFmqHtFre9Xi8",
|
||||
"options": { "itemIds": ["q69Ex7LacPrH9QUMeosRnB"] }
|
||||
},
|
||||
"sseUQEWCMdiZquk8EbxHYtk": {
|
||||
"id": "sseUQEWCMdiZquk8EbxHYtk",
|
||||
"blockId": "bhKHKi1SQb5woZEy1y4fNsJ",
|
||||
"type": "text input"
|
||||
}
|
||||
},
|
||||
"allIds": [
|
||||
"oxTsU2C1RX5QHuyY8qjHAM",
|
||||
"sgkADMK25y9P9V3vjwjBaac",
|
||||
"ssEiEECKSFkA44dGDceHxKw",
|
||||
"sseUQEWCMdiZquk8EbxHYtk"
|
||||
]
|
||||
},
|
||||
"choiceItems": {
|
||||
"byId": {
|
||||
"q69Ex7LacPrH9QUMeosRnB": {
|
||||
"id": "q69Ex7LacPrH9QUMeosRnB",
|
||||
"stepId": "ssEiEECKSFkA44dGDceHxKw",
|
||||
"content": "Go"
|
||||
}
|
||||
},
|
||||
"allIds": ["q69Ex7LacPrH9QUMeosRnB"]
|
||||
},
|
||||
"variables": {
|
||||
"byId": {
|
||||
"4tvkRmf32wiTsXrYoqyhfr": {
|
||||
"id": "4tvkRmf32wiTsXrYoqyhfr",
|
||||
"name": "secret 2"
|
||||
},
|
||||
"jEg1FvkCU5S5owNAxXFsHL": {
|
||||
"id": "jEg1FvkCU5S5owNAxXFsHL",
|
||||
"name": "secret 3"
|
||||
},
|
||||
"oASkBtoLqkYNqeakcjZH4L": {
|
||||
"id": "oASkBtoLqkYNqeakcjZH4L",
|
||||
"name": "secret 1"
|
||||
},
|
||||
"rEoE1ehHzgx8X3d3UPGDHg": {
|
||||
"id": "rEoE1ehHzgx8X3d3UPGDHg",
|
||||
"name": "secret 4"
|
||||
}
|
||||
},
|
||||
"allIds": [
|
||||
"oASkBtoLqkYNqeakcjZH4L",
|
||||
"4tvkRmf32wiTsXrYoqyhfr",
|
||||
"jEg1FvkCU5S5owNAxXFsHL",
|
||||
"rEoE1ehHzgx8X3d3UPGDHg"
|
||||
]
|
||||
},
|
||||
"webhooks": {
|
||||
"byId": {
|
||||
"4h4Kk3Q1qGy7gFzpZtWVpU": { "id": "4h4Kk3Q1qGy7gFzpZtWVpU", "url": "" }
|
||||
},
|
||||
"allIds": ["4h4Kk3Q1qGy7gFzpZtWVpU"]
|
||||
},
|
||||
"edges": {
|
||||
"byId": {
|
||||
"25yX9DnQgdafpdAjfAu5Fp": {
|
||||
"id": "25yX9DnQgdafpdAjfAu5Fp",
|
||||
"to": { "blockId": "bdFW2HHjMoEFmqHtFre9Xi8" },
|
||||
"from": {
|
||||
"stepId": "oxTsU2C1RX5QHuyY8qjHAM",
|
||||
"blockId": "3kH2sUjVThQDWmqdoKnGk5"
|
||||
}
|
||||
},
|
||||
"6e4Sbp8pGTvBQYtCk2qXbN": {
|
||||
"from": {
|
||||
"blockId": "bdFW2HHjMoEFmqHtFre9Xi8",
|
||||
"stepId": "ssEiEECKSFkA44dGDceHxKw"
|
||||
},
|
||||
"to": { "blockId": "bhKHKi1SQb5woZEy1y4fNsJ" },
|
||||
"id": "6e4Sbp8pGTvBQYtCk2qXbN"
|
||||
}
|
||||
},
|
||||
"allIds": ["25yX9DnQgdafpdAjfAu5Fp", "6e4Sbp8pGTvBQYtCk2qXbN"]
|
||||
},
|
||||
"theme": {
|
||||
"general": {
|
||||
"font": "Open Sans",
|
||||
"background": { "type": "None", "content": "#ffffff" }
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"typingEmulation": { "speed": 300, "enabled": true, "maxDelay": 1.5 }
|
||||
},
|
||||
"publicId": null
|
||||
}
|
90
apps/builder/cypress/tests/theme/chat.ts
Normal file
90
apps/builder/cypress/tests/theme/chat.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { getIframeBody } from 'cypress/support'
|
||||
|
||||
describe('General theme settings', () => {
|
||||
beforeEach(() => {
|
||||
cy.task('seed')
|
||||
cy.signOut()
|
||||
})
|
||||
|
||||
it('should reflect changes in real time', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots/typebot4/theme')
|
||||
getIframeBody().findByText('Ready?').should('exist')
|
||||
cy.findByRole('button', { name: 'Chat' }).click()
|
||||
|
||||
// Host bubbles
|
||||
cy.findAllByRole('button', { name: 'Pick a color' }).first().click()
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#2a9d8f')
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(1)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#ffffff')
|
||||
getIframeBody()
|
||||
.findByTestId('host-bubble')
|
||||
.should('have.css', 'background-color')
|
||||
.should('eq', 'rgb(42, 157, 143)')
|
||||
getIframeBody()
|
||||
.findByTestId('host-bubble')
|
||||
.should('have.css', 'color')
|
||||
.should('eq', 'rgb(255, 255, 255)')
|
||||
|
||||
// Buttons
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(4)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#7209b7')
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(5)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#e9c46a')
|
||||
getIframeBody()
|
||||
.findByTestId('button')
|
||||
.should('have.css', 'background-color')
|
||||
.should('eq', 'rgb(114, 9, 183)')
|
||||
getIframeBody()
|
||||
.findByTestId('button')
|
||||
.should('have.css', 'color')
|
||||
.should('eq', 'rgb(233, 196, 106)')
|
||||
|
||||
// Guest bubbles
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(2)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#d8f3dc')
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(3)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#264653')
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(6)
|
||||
.click({ force: true })
|
||||
getIframeBody().findByRole('button', { name: 'Go' }).click()
|
||||
getIframeBody()
|
||||
.findByTestId('guest-bubble')
|
||||
.should('have.css', 'background-color')
|
||||
.should('eq', 'rgb(216, 243, 220)')
|
||||
getIframeBody()
|
||||
.findByTestId('guest-bubble')
|
||||
.should('have.css', 'color')
|
||||
.should('eq', 'rgb(38, 70, 83)')
|
||||
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#ffe8d6')
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(7)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('#023e8a')
|
||||
cy.findAllByRole('button', { name: 'Pick a color' })
|
||||
.eq(8)
|
||||
.click({ force: true })
|
||||
cy.findByRole('textbox', { name: 'Color value' }).clear().type('red')
|
||||
getIframeBody()
|
||||
.findByTestId('input')
|
||||
.should('have.css', 'background-color')
|
||||
.should('eq', 'rgb(255, 232, 214)')
|
||||
getIframeBody()
|
||||
.findByTestId('input')
|
||||
.should('have.css', 'color')
|
||||
.should('eq', 'rgb(2, 61, 138)')
|
||||
})
|
||||
})
|
@ -6,8 +6,8 @@ describe('General theme settings', () => {
|
||||
cy.signOut()
|
||||
})
|
||||
|
||||
it.only('should reflect changes in real time', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/integrations/webhook.json')
|
||||
it('should reflect changes in real time', () => {
|
||||
cy.loadTypebotFixtureInDatabase('typebots/theme/theme.json')
|
||||
cy.signIn('test2@gmail.com')
|
||||
cy.visit('/typebots/typebot4/theme')
|
||||
cy.findByRole('button', { name: 'General' }).click()
|
||||
|
@ -2,10 +2,7 @@ import {
|
||||
Block,
|
||||
TextBubbleStep,
|
||||
PublicTypebot,
|
||||
BackgroundType,
|
||||
Settings,
|
||||
StartStep,
|
||||
Theme,
|
||||
BubbleStepType,
|
||||
InputStepType,
|
||||
ChoiceInputStep,
|
||||
@ -216,19 +213,6 @@ export const parseNewTypebot = ({
|
||||
graphCoordinates: { x: 0, y: 0 },
|
||||
stepIds: [startStepId],
|
||||
}
|
||||
const theme: Theme = {
|
||||
general: {
|
||||
font: 'Open Sans',
|
||||
background: { type: BackgroundType.NONE, content: '#ffffff' },
|
||||
},
|
||||
}
|
||||
const settings: Settings = {
|
||||
typingEmulation: {
|
||||
enabled: true,
|
||||
speed: 300,
|
||||
maxDelay: 1.5,
|
||||
},
|
||||
}
|
||||
return {
|
||||
folderId,
|
||||
name,
|
||||
@ -239,8 +223,6 @@ export const parseNewTypebot = ({
|
||||
variables: { byId: {}, allIds: [] },
|
||||
edges: { byId: {}, allIds: [] },
|
||||
webhooks: { byId: {}, allIds: [] },
|
||||
theme,
|
||||
settings,
|
||||
}
|
||||
}
|
||||
|
||||
|
13
packages/bot-engine/src/assets/phone.css
Normal file
13
packages/bot-engine/src/assets/phone.css
Normal file
@ -0,0 +1,13 @@
|
||||
.PhoneInputInput {
|
||||
padding: 1rem 0.5rem;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.PhoneInputCountry {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.PhoneInputCountryIcon,
|
||||
.PhoneInputCountryIconImg {
|
||||
border-radius: 3px;
|
||||
}
|
@ -6,38 +6,22 @@
|
||||
--typebot-container-bg-image: none;
|
||||
--typebot-container-bg-color: transparent;
|
||||
--typebot-container-font-family: 'Open Sans';
|
||||
--typebot-chat-view-max-width: 700px;
|
||||
--typebot-chat-view-color: #303235;
|
||||
|
||||
--typebot-button-active-bg-color: #0042da;
|
||||
--typebot-button-active-color: #ffffff;
|
||||
--typebot-button-inactive-bg-color: #edf2f7;
|
||||
--typebot-button-inactive-color: #303235;
|
||||
--typebot-button-border: 1px solid var(--typebot-button-active-bg-color);
|
||||
--typebot-button-shadow: none;
|
||||
--typebot-button-bg-color: #0042da;
|
||||
--typebot-button-color: #ffffff;
|
||||
|
||||
--typebot-host-bubble-bg-color: #f7f8ff;
|
||||
--typebot-host-bubble-color: #303235;
|
||||
--typebot-host-bubble-border: 1px solid var(--typebot-host-bubble-bg-color);
|
||||
--typebot-host-bubble-shadow: none;
|
||||
|
||||
--typebot-guest-bubble-bg-color: #ff8e21;
|
||||
--typebot-guest-bubble-color: #ffffff;
|
||||
--typebot-guest-bubble-border: 1px solid var(--typebot-guest-bubble-bg-color);
|
||||
--typebot-guest-bubble-shadow: none;
|
||||
|
||||
--typebot-input-bg-color: #ffffff;
|
||||
--typebot-input-color: #303235;
|
||||
--typebot-input-border: 1px solid var(--typebot-input-bg-color);
|
||||
--typebot-input-shadow: 0 2px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
--typebot-input-placeholder-color: #9095a0;
|
||||
|
||||
--typebot-header-bg-color: #ffffff;
|
||||
--typebot-header-color: #303235;
|
||||
--typebot-header-border: none;
|
||||
--typebot-header-shadow: none;
|
||||
--typebot-header-max-width: 1000px;
|
||||
|
||||
/* Phone input */
|
||||
--PhoneInputCountryFlag-borderColor: transparent;
|
||||
@ -55,12 +39,6 @@
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
.StripeElement {
|
||||
box-sizing: border-box;
|
||||
height: 40px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
.bubble-enter {
|
||||
opacity: 0;
|
||||
@ -149,159 +127,19 @@ textarea {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.comp-input::-webkit-input-placeholder {
|
||||
/* Chrome/Opera/Safari */
|
||||
.text-input::-webkit-input-placeholder {
|
||||
color: var(--typebot-input-placeholder-color) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.comp-input::-moz-placeholder {
|
||||
/* Firefox 19+ */
|
||||
.text-input::-moz-placeholder {
|
||||
color: var(--typebot-input-placeholder-color) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
.comp-input::placeholder {
|
||||
.text-input::placeholder {
|
||||
color: var(--typebot-input-placeholder-color) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.PhoneInput {
|
||||
/* This is done to stretch the contents of this component. */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.PhoneInput > input {
|
||||
color: var(--typebot-input-color);
|
||||
background-color: var(--typebot-input-bg-color);
|
||||
}
|
||||
|
||||
.PhoneInput > input::placeholder {
|
||||
color: var(--typebot-input-placeholder-color) !important;
|
||||
}
|
||||
|
||||
.PhoneInputInput {
|
||||
/* The phone number input stretches to fill all empty space */
|
||||
flex: 1;
|
||||
/* The phone number input should shrink
|
||||
to make room for the extension input */
|
||||
min-width: 0;
|
||||
padding: 1rem 1rem 1rem 0;
|
||||
}
|
||||
|
||||
.PhoneInputInput:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.PhoneInputCountryIcon {
|
||||
width: calc(
|
||||
var(--PhoneInputCountryFlag-height) *
|
||||
var(--PhoneInputCountryFlag-aspectRatio)
|
||||
);
|
||||
height: var(--PhoneInputCountryFlag-height);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.PhoneInputCountryIcon--square {
|
||||
width: var(--PhoneInputCountryFlag-height);
|
||||
}
|
||||
|
||||
.PhoneInputCountryIcon--border {
|
||||
/* Removed `background-color` because when an `<img/>` was still loading
|
||||
it would show a dark gray rectangle. */
|
||||
/* For some reason the `<img/>` is not stretched to 100% width and height
|
||||
and sometime there can be seen white pixels of the background at top and bottom. */
|
||||
background-color: var(--PhoneInputCountryFlag-backgroundColor--loading);
|
||||
/* Border is added via `box-shadow` because `border` interferes with `width`/`height`. */
|
||||
/* For some reason the `<img/>` is not stretched to 100% width and height
|
||||
and sometime there can be seen white pixels of the background at top and bottom,
|
||||
so an additional "inset" border is added. */
|
||||
box-shadow: 0 0 0 var(--PhoneInputCountryFlag-borderWidth)
|
||||
var(--PhoneInputCountryFlag-borderColor),
|
||||
inset 0 0 0 var(--PhoneInputCountryFlag-borderWidth)
|
||||
var(--PhoneInputCountryFlag-borderColor);
|
||||
}
|
||||
|
||||
.PhoneInputCountryIconImg {
|
||||
/* Fixes weird vertical space above the flag icon. */
|
||||
/* https://gitlab.com/catamphetamine/react-phone-number-input/-/issues/7#note_348586559 */
|
||||
display: block;
|
||||
/* 3rd party <SVG/> flag icons won't stretch if they have `width` and `height`.
|
||||
Also, if an <SVG/> icon's aspect ratio was different, it wouldn't fit too. */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.PhoneInputInternationalIconPhone {
|
||||
opacity: var(--PhoneInputInternationalIconPhone-opacity);
|
||||
}
|
||||
|
||||
.PhoneInputInternationalIconGlobe {
|
||||
opacity: var(--PhoneInputInternationalIconGlobe-opacity);
|
||||
}
|
||||
|
||||
/* Styling native country `<select/>`. */
|
||||
|
||||
.PhoneInputCountry {
|
||||
position: relative;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.PhoneInputCountrySelect {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.PhoneInputCountrySelect[disabled] {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.PhoneInputCountrySelectArrow {
|
||||
display: block;
|
||||
content: '';
|
||||
width: var(--PhoneInputCountrySelectArrow-width);
|
||||
height: var(--PhoneInputCountrySelectArrow-width);
|
||||
margin-left: var(--PhoneInputCountrySelectArrow-marginLeft);
|
||||
border-style: solid;
|
||||
border-color: var(--PhoneInputCountrySelectArrow-color);
|
||||
border-top-width: 0;
|
||||
border-bottom-width: var(--PhoneInputCountrySelectArrow-borderWidth);
|
||||
border-left-width: 0;
|
||||
border-right-width: var(--PhoneInputCountrySelectArrow-borderWidth);
|
||||
transform: var(--PhoneInputCountrySelectArrow-transform);
|
||||
opacity: var(--PhoneInputCountrySelectArrow-opacity);
|
||||
}
|
||||
|
||||
.PhoneInputCountrySelect:focus
|
||||
+ .PhoneInputCountryIcon
|
||||
+ .PhoneInputCountrySelectArrow {
|
||||
opacity: 1;
|
||||
color: var(--PhoneInputCountrySelectArrow-color--focus);
|
||||
}
|
||||
|
||||
.PhoneInputCountrySelect:focus + .PhoneInputCountryIcon--border {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.PhoneInputCountrySelect:focus
|
||||
+ .PhoneInputCountryIcon
|
||||
.PhoneInputInternationalIconGlobe {
|
||||
opacity: 1;
|
||||
color: var(--PhoneInputCountrySelectArrow-color--focus);
|
||||
}
|
||||
|
||||
.typebot-container {
|
||||
background-image: var(--typebot-container-bg-image);
|
||||
background-color: var(--typebot-container-bg-color);
|
||||
@ -311,28 +149,11 @@ textarea {
|
||||
.custom-header {
|
||||
color: var(--typebot-header-color);
|
||||
background-color: var(--typebot-header-bg-color);
|
||||
border-bottom: var(--typebot-header-border);
|
||||
box-shadow: var(--typebot-header-shadow);
|
||||
}
|
||||
|
||||
.custom-header-content {
|
||||
max-width: var(--typebot-header-max-width);
|
||||
}
|
||||
|
||||
.typebot-chat-view {
|
||||
max-width: var(--typebot-chat-view-max-width);
|
||||
}
|
||||
|
||||
.typebot-button.active {
|
||||
color: var(--typebot-button-active-color);
|
||||
background-color: var(--typebot-button-active-bg-color);
|
||||
}
|
||||
|
||||
.typebot-button {
|
||||
color: var(--typebot-button-inactive-color);
|
||||
background-color: var(--typebot-button-inactive-bg-color);
|
||||
border: var(--typebot-button-border);
|
||||
box-shadow: var(--typebot-button-shadow);
|
||||
color: var(--typebot-button-color);
|
||||
background-color: var(--typebot-button-bg-color);
|
||||
}
|
||||
|
||||
.typebot-host-bubble {
|
||||
@ -342,45 +163,23 @@ textarea {
|
||||
.typebot-host-bubble > .bubble-typing {
|
||||
background-color: var(--typebot-host-bubble-bg-color);
|
||||
border: var(--typebot-host-bubble-border);
|
||||
box-shadow: var(--typebot-host-bubble-shadow);
|
||||
}
|
||||
|
||||
.typebot-guest-bubble {
|
||||
color: var(--typebot-guest-bubble-color);
|
||||
background-color: var(--typebot-guest-bubble-bg-color);
|
||||
border: var(--typebot-guest-bubble-border);
|
||||
box-shadow: var(--typebot-guest-bubble-shadow);
|
||||
}
|
||||
|
||||
.typebot-input {
|
||||
color: var(--typebot-input-color);
|
||||
background-color: var(--typebot-input-bg-color);
|
||||
border: var(--typebot-input-border);
|
||||
box-shadow: var(--typebot-input-shadow);
|
||||
box-shadow: 0 2px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.typebot-button > .send-icon {
|
||||
fill: var(--typebot-button-active-color);
|
||||
}
|
||||
|
||||
.text-on-chat {
|
||||
color: var(--typebot-chat-view-color);
|
||||
}
|
||||
|
||||
.star-icon {
|
||||
cursor: pointer;
|
||||
fill: transparent;
|
||||
stroke-width: 30px;
|
||||
stroke: var(--typebot-button-active-bg-color);
|
||||
}
|
||||
|
||||
.star-icon.active {
|
||||
fill: var(--typebot-button-active-bg-color);
|
||||
}
|
||||
|
||||
.scale-labels {
|
||||
font-size: 13px;
|
||||
color: var(--typebot-button-active-bg-color);
|
||||
font-weight: 600;
|
||||
padding-right: 0.5rem;
|
||||
.typebot-chat-view {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
@ -10,7 +10,10 @@ export const GuestBubble = ({ message }: Props): JSX.Element => {
|
||||
<CSSTransition classNames="bubble" timeout={1000}>
|
||||
<div className="flex justify-end mb-2 items-center">
|
||||
<div className="flex items-end w-11/12 lg:w-4/6 justify-end">
|
||||
<div className="inline-flex px-4 py-2 rounded-lg mr-2 whitespace-pre-wrap max-w-full typebot-guest-bubble">
|
||||
<div
|
||||
className="inline-flex px-4 py-2 rounded-lg mr-2 whitespace-pre-wrap max-w-full typebot-guest-bubble"
|
||||
data-testid="guest-bubble"
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,9 +13,14 @@ type Props = {
|
||||
|
||||
export const showAnimationDuration = 400
|
||||
|
||||
const defaultTypingEmulation = {
|
||||
enabled: true,
|
||||
speed: 300,
|
||||
maxDelay: 1.5,
|
||||
}
|
||||
|
||||
export const TextBubble = ({ step, onTransitionEnd }: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const { typingEmulation } = typebot.settings
|
||||
const { updateLastAvatarOffset } = useHostAvatars()
|
||||
const messageContainer = useRef<HTMLDivElement | null>(null)
|
||||
const [isTyping, setIsTyping] = useState(true)
|
||||
@ -30,7 +35,7 @@ export const TextBubble = ({ step, onTransitionEnd }: Props) => {
|
||||
sendAvatarOffset()
|
||||
const typingTimeout = computeTypingTimeout(
|
||||
step.content.plainText,
|
||||
typingEmulation
|
||||
typebot.settings?.typingEmulation ?? defaultTypingEmulation
|
||||
)
|
||||
setTimeout(() => {
|
||||
onTypingEnd()
|
||||
@ -61,6 +66,7 @@ export const TextBubble = ({ step, onTransitionEnd }: Props) => {
|
||||
width: isTyping ? '4rem' : '100%',
|
||||
height: isTyping ? '2rem' : '100%',
|
||||
}}
|
||||
data-testid="host-bubble"
|
||||
>
|
||||
{isTyping ? <TypingContent /> : <></>}
|
||||
</div>
|
||||
|
@ -49,6 +49,7 @@ export const ChoiceForm = ({ options, onSubmit }: ChoiceFormProps) => {
|
||||
? 'active'
|
||||
: '')
|
||||
}
|
||||
data-testid="button"
|
||||
>
|
||||
{items.byId[itemId].content}
|
||||
</button>
|
||||
|
@ -34,7 +34,7 @@ export const DateForm = ({
|
||||
<p className="font-semibold mr-2">{labels?.from ?? 'From:'}</p>
|
||||
)}
|
||||
<input
|
||||
className="focus:outline-none bg-transparent flex-1 w-full comp-input"
|
||||
className="focus:outline-none bg-transparent flex-1 w-full text-input"
|
||||
type={hasTime ? 'datetime-local' : 'date'}
|
||||
onChange={(e) =>
|
||||
setInputValues({ ...inputValues, from: e.target.value })
|
||||
@ -48,7 +48,7 @@ export const DateForm = ({
|
||||
<p className="font-semibold">{labels?.to ?? 'To:'}</p>
|
||||
)}
|
||||
<input
|
||||
className="focus:outline-none bg-transparent flex-1 w-full comp-input ml-2"
|
||||
className="focus:outline-none bg-transparent flex-1 w-full text-input ml-2"
|
||||
type={hasTime ? 'datetime-local' : 'date'}
|
||||
onChange={(e) =>
|
||||
setInputValues({ ...inputValues, to: e.target.value })
|
||||
|
@ -36,6 +36,7 @@ export const TextForm = ({ step, onSubmit }: TextFormProps) => {
|
||||
<form
|
||||
className="flex items-end justify-between rounded-lg pr-2 typebot-input"
|
||||
onSubmit={handleSubmit}
|
||||
data-testid="input"
|
||||
>
|
||||
<TextInput step={step} onChange={handleChange} />
|
||||
<SendButton
|
||||
|
@ -120,7 +120,7 @@ const ShortTextInput = React.forwardRef(
|
||||
) => (
|
||||
<input
|
||||
ref={ref}
|
||||
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full comp-input"
|
||||
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
|
||||
type="text"
|
||||
required
|
||||
{...props}
|
||||
@ -135,7 +135,7 @@ const LongTextInput = React.forwardRef(
|
||||
) => (
|
||||
<textarea
|
||||
ref={ref}
|
||||
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full comp-input"
|
||||
className="focus:outline-none bg-transparent px-4 py-4 flex-1 w-full text-input"
|
||||
rows={4}
|
||||
data-testid="textarea"
|
||||
required
|
||||
|
@ -5,6 +5,8 @@ import Frame from 'react-frame-component'
|
||||
import style from '../assets/style.css'
|
||||
//@ts-ignore
|
||||
import phoneNumberInputStyle from 'react-phone-number-input/style.css'
|
||||
//@ts-ignore
|
||||
import phoneSyle from '../assets/phone.css'
|
||||
import { ConversationContainer } from './ConversationContainer'
|
||||
import { AnswersContext } from '../contexts/AnswersContext'
|
||||
import { Answer, BackgroundType, PublicTypebot } from 'models'
|
||||
@ -23,10 +25,10 @@ export const TypebotViewer = ({
|
||||
}: TypebotViewerProps) => {
|
||||
const containerBgColor = useMemo(
|
||||
() =>
|
||||
typebot.theme.general.background.type === BackgroundType.COLOR
|
||||
typebot?.theme?.general?.background?.type === BackgroundType.COLOR
|
||||
? typebot.theme.general.background.content
|
||||
: 'transparent',
|
||||
[typebot.theme.general.background]
|
||||
[typebot?.theme?.general?.background]
|
||||
)
|
||||
const handleNewBlockVisible = (blockId: string) => {
|
||||
if (onNewBlockVisible) onNewBlockVisible(blockId)
|
||||
@ -44,6 +46,7 @@ export const TypebotViewer = ({
|
||||
head={
|
||||
<style>
|
||||
{phoneNumberInputStyle}
|
||||
{phoneSyle}
|
||||
{style}
|
||||
</style>
|
||||
}
|
||||
@ -51,7 +54,9 @@ export const TypebotViewer = ({
|
||||
>
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `@import url('https://fonts.googleapis.com/css2?family=${typebot.theme.general.font}:wght@300;400;600&display=swap');`,
|
||||
__html: `@import url('https://fonts.googleapis.com/css2?family=${
|
||||
typebot?.theme?.general?.font ?? 'Open Sans'
|
||||
}:wght@300;400;600&display=swap');`,
|
||||
}}
|
||||
/>
|
||||
<TypebotContext typebot={typebot}>
|
||||
@ -60,7 +65,7 @@ export const TypebotViewer = ({
|
||||
className="flex text-base overflow-hidden bg-cover h-screen w-screen flex-col items-center typebot-container"
|
||||
style={{
|
||||
// We set this as inline style to avoid color flash for SSR
|
||||
backgroundColor: containerBgColor,
|
||||
backgroundColor: containerBgColor ?? 'transparent',
|
||||
}}
|
||||
data-testid="container"
|
||||
>
|
||||
|
@ -1,25 +1,142 @@
|
||||
import { BackgroundType, Theme } from 'models'
|
||||
import {
|
||||
Background,
|
||||
BackgroundType,
|
||||
ChatTheme,
|
||||
ContainerColors,
|
||||
GeneralTheme,
|
||||
InputColors,
|
||||
Theme,
|
||||
} from 'models'
|
||||
|
||||
const cssVariableNames = {
|
||||
container: {
|
||||
bg: {
|
||||
image: '--typebot-container-bg-image',
|
||||
color: '--typebot-container-bg-color',
|
||||
},
|
||||
general: {
|
||||
bgImage: '--typebot-container-bg-image',
|
||||
bgColor: '--typebot-container-bg-color',
|
||||
fontFamily: '--typebot-container-font-family',
|
||||
},
|
||||
chat: {
|
||||
hostBubbles: {
|
||||
bgColor: '--typebot-host-bubble-bg-color',
|
||||
color: '--typebot-host-bubble-color',
|
||||
},
|
||||
guestBubbles: {
|
||||
bgColor: '--typebot-guest-bubble-bg-color',
|
||||
color: '--typebot-guest-bubble-color',
|
||||
},
|
||||
inputs: {
|
||||
bgColor: '--typebot-input-bg-color',
|
||||
color: '--typebot-input-color',
|
||||
placeholderColor: '--typebot-input-placeholder-color',
|
||||
},
|
||||
buttons: {
|
||||
bgColor: '--typebot-button-bg-color',
|
||||
color: '--typebot-button-color',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const setCssVariablesValue = (
|
||||
theme: Theme,
|
||||
theme: Theme | undefined,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
const { background, font } = theme.general
|
||||
documentStyle.setProperty(
|
||||
background.type === BackgroundType.IMAGE
|
||||
? cssVariableNames.container.bg.image
|
||||
: cssVariableNames.container.bg.color,
|
||||
background.type === BackgroundType.NONE ? 'transparent' : background.content
|
||||
)
|
||||
documentStyle.setProperty(cssVariableNames.container.fontFamily, font)
|
||||
if (!theme) return
|
||||
if (theme.general) setGeneralTheme(theme.general, documentStyle)
|
||||
if (theme.chat) setChatTheme(theme.chat, documentStyle)
|
||||
}
|
||||
|
||||
const setGeneralTheme = (
|
||||
generalTheme: GeneralTheme,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
const { background, font } = generalTheme
|
||||
if (background) setTypebotBackground
|
||||
if (font) documentStyle.setProperty(cssVariableNames.general.fontFamily, font)
|
||||
}
|
||||
|
||||
const setChatTheme = (
|
||||
chatTheme: ChatTheme,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
const { hostBubbles, guestBubbles, buttons, inputs } = chatTheme
|
||||
if (hostBubbles) setHostBubbles(hostBubbles, documentStyle)
|
||||
if (guestBubbles) setGuestBubbles(guestBubbles, documentStyle)
|
||||
if (buttons) setButtons(buttons, documentStyle)
|
||||
if (inputs) setInputs(inputs, documentStyle)
|
||||
}
|
||||
|
||||
const setHostBubbles = (
|
||||
hostBubbles: ContainerColors,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
if (hostBubbles.backgroundColor)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.hostBubbles.bgColor,
|
||||
hostBubbles.backgroundColor
|
||||
)
|
||||
if (hostBubbles.color)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.hostBubbles.color,
|
||||
hostBubbles.color
|
||||
)
|
||||
}
|
||||
|
||||
const setGuestBubbles = (
|
||||
guestBubbles: ContainerColors,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
if (guestBubbles.backgroundColor)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.guestBubbles.bgColor,
|
||||
guestBubbles.backgroundColor
|
||||
)
|
||||
if (guestBubbles.color)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.guestBubbles.color,
|
||||
guestBubbles.color
|
||||
)
|
||||
}
|
||||
|
||||
const setButtons = (
|
||||
buttons: ContainerColors,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
if (buttons.backgroundColor)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.buttons.bgColor,
|
||||
buttons.backgroundColor
|
||||
)
|
||||
if (buttons.color)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.buttons.color,
|
||||
buttons.color
|
||||
)
|
||||
}
|
||||
|
||||
const setInputs = (inputs: InputColors, documentStyle: CSSStyleDeclaration) => {
|
||||
if (inputs.backgroundColor)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.inputs.bgColor,
|
||||
inputs.backgroundColor
|
||||
)
|
||||
if (inputs.color)
|
||||
documentStyle.setProperty(cssVariableNames.chat.inputs.color, inputs.color)
|
||||
if (inputs.placeholderColor)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.chat.inputs.placeholderColor,
|
||||
inputs.placeholderColor
|
||||
)
|
||||
}
|
||||
|
||||
const setTypebotBackground = (
|
||||
background: Background,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
documentStyle.setProperty(
|
||||
background?.type === BackgroundType.IMAGE
|
||||
? cssVariableNames.general.bgImage
|
||||
: cssVariableNames.general.bgColor,
|
||||
background.type === BackgroundType.NONE
|
||||
? 'transparent'
|
||||
: background.content ?? '#ffffff'
|
||||
)
|
||||
}
|
||||
|
@ -18,6 +18,6 @@ export type PublicTypebot = Omit<
|
||||
choiceItems: Table<ChoiceItem>
|
||||
variables: Table<Variable>
|
||||
edges: Table<Edge>
|
||||
theme: Theme
|
||||
settings: Settings
|
||||
theme?: Theme
|
||||
settings?: Settings
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
export type Settings = {
|
||||
typingEmulation: TypingEmulationSettings
|
||||
typingEmulation?: TypingEmulationSettings
|
||||
}
|
||||
|
||||
export type TypingEmulationSettings = {
|
||||
enabled: boolean
|
||||
speed: number
|
||||
maxDelay: number
|
||||
enabled?: boolean
|
||||
speed?: number
|
||||
maxDelay?: number
|
||||
}
|
||||
|
@ -1,8 +1,27 @@
|
||||
export type Theme = {
|
||||
general: {
|
||||
font: string
|
||||
background: Background
|
||||
}
|
||||
general?: GeneralTheme
|
||||
chat?: ChatTheme
|
||||
}
|
||||
|
||||
export type GeneralTheme = {
|
||||
font?: string
|
||||
background?: Background
|
||||
}
|
||||
|
||||
export type ChatTheme = {
|
||||
hostBubbles?: ContainerColors
|
||||
guestBubbles?: ContainerColors
|
||||
buttons?: ContainerColors
|
||||
inputs?: InputColors
|
||||
}
|
||||
|
||||
export type ContainerColors = {
|
||||
backgroundColor?: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
export type InputColors = ContainerColors & {
|
||||
placeholderColor?: string
|
||||
}
|
||||
|
||||
export enum BackgroundType {
|
||||
@ -13,5 +32,5 @@ export enum BackgroundType {
|
||||
|
||||
export type Background = {
|
||||
type: BackgroundType
|
||||
content: string
|
||||
content?: string
|
||||
}
|
||||
|
@ -23,8 +23,8 @@ export type Typebot = Omit<
|
||||
variables: Table<Variable>
|
||||
edges: Table<Edge>
|
||||
webhooks: Table<Webhook>
|
||||
theme: Theme
|
||||
settings: Settings
|
||||
theme?: Theme
|
||||
settings?: Settings
|
||||
}
|
||||
|
||||
export type Block = {
|
||||
|
Reference in New Issue
Block a user