2
0

feat(theme): Add chat theme settings

This commit is contained in:
Baptiste Arnaud
2022-01-24 15:07:09 +01:00
parent 619d10ae4e
commit b0abe5b8fa
37 changed files with 771 additions and 375 deletions

View File

@ -0,0 +1,13 @@
.PhoneInputInput {
padding: 1rem 0.5rem;
outline: none !important;
}
.PhoneInputCountry {
padding-left: 0.5rem;
}
.PhoneInputCountryIcon,
.PhoneInputCountryIconImg {
border-radius: 3px;
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -49,6 +49,7 @@ export const ChoiceForm = ({ options, onSubmit }: ChoiceFormProps) => {
? 'active'
: '')
}
data-testid="button"
>
{items.byId[itemId].content}
</button>

View File

@ -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 })

View File

@ -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

View File

@ -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

View File

@ -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"
>

View File

@ -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'
)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 = {