⚡ (embed) Add size and icon picker in bubble settings (#508)
This commit is contained in:
@@ -19,7 +19,7 @@ import tinyColor from 'tinycolor2'
|
|||||||
|
|
||||||
const colorsSelection: `#${string}`[] = [
|
const colorsSelection: `#${string}`[] = [
|
||||||
'#666460',
|
'#666460',
|
||||||
'#AFABA3',
|
'#FFFFFF',
|
||||||
'#A87964',
|
'#A87964',
|
||||||
'#D09C46',
|
'#D09C46',
|
||||||
'#DE8031',
|
'#DE8031',
|
||||||
@@ -27,7 +27,7 @@ const colorsSelection: `#${string}`[] = [
|
|||||||
'#4A8BB2',
|
'#4A8BB2',
|
||||||
'#9B74B7',
|
'#9B74B7',
|
||||||
'#C75F96',
|
'#C75F96',
|
||||||
'#C75F96',
|
'#0042DA',
|
||||||
]
|
]
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -78,18 +78,19 @@ export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
|
|||||||
</PopoverHeader>
|
</PopoverHeader>
|
||||||
<PopoverBody as={Stack}>
|
<PopoverBody as={Stack}>
|
||||||
<SimpleGrid columns={5} spacing={2}>
|
<SimpleGrid columns={5} spacing={2}>
|
||||||
{colorsSelection.map((c) => (
|
{colorsSelection.map((color) => (
|
||||||
<Button
|
<Button
|
||||||
key={c}
|
key={color}
|
||||||
aria-label={c}
|
aria-label={color}
|
||||||
background={c}
|
background={color}
|
||||||
height="22px"
|
height="22px"
|
||||||
width="22px"
|
width="22px"
|
||||||
padding={0}
|
padding={0}
|
||||||
minWidth="unset"
|
minWidth="unset"
|
||||||
borderRadius={3}
|
borderRadius={3}
|
||||||
_hover={{ background: c }}
|
borderWidth={color === '#FFFFFF' ? 1 : undefined}
|
||||||
onClick={handleClick(c)}
|
_hover={{ background: color }}
|
||||||
|
onClick={handleClick(color)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|||||||
@@ -57,10 +57,9 @@ export const EditableEmojiOrImageIcon = ({
|
|||||||
filePath={uploadFilePath}
|
filePath={uploadFilePath}
|
||||||
defaultUrl={icon ?? ''}
|
defaultUrl={icon ?? ''}
|
||||||
onSubmit={onChangeIcon}
|
onSubmit={onChangeIcon}
|
||||||
isGiphyEnabled={false}
|
excludedTabs={['giphy', 'unsplash']}
|
||||||
isUnsplashEnabled={false}
|
|
||||||
isEmojiEnabled={true}
|
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
initialTab="icon"
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ export const IconPicker = ({ onIconSelected }: Props) => {
|
|||||||
)
|
)
|
||||||
const searchQuery = useRef<string>('')
|
const searchQuery = useRef<string>('')
|
||||||
const [selectedColor, setSelectedColor] = useState(initialIconColor)
|
const [selectedColor, setSelectedColor] = useState(initialIconColor)
|
||||||
|
const isWhite = useMemo(
|
||||||
|
() =>
|
||||||
|
selectedColor.toLowerCase() === '#ffffff' ||
|
||||||
|
selectedColor.toLowerCase() === '#fff' ||
|
||||||
|
selectedColor === 'white',
|
||||||
|
[selectedColor]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bottomElement.current) return
|
if (!bottomElement.current) return
|
||||||
@@ -68,10 +75,9 @@ export const IconPicker = ({ onIconSelected }: Props) => {
|
|||||||
|
|
||||||
const selectIcon = async (iconName: string) => {
|
const selectIcon = async (iconName: string) => {
|
||||||
const svg = await (await fetch(`/icons/${iconName}.svg`)).text()
|
const svg = await (await fetch(`/icons/${iconName}.svg`)).text()
|
||||||
const dataUri = `data:image/svg+xml;utf8,${svg.replace(
|
const dataUri = `data:image/svg+xml;utf8,${svg
|
||||||
'<svg',
|
.replace('<svg', `<svg fill='${encodeURIComponent(selectedColor)}'`)
|
||||||
`<svg fill='${encodeURIComponent(selectedColor)}'`
|
.replace(/"/g, "'")}`
|
||||||
)}`
|
|
||||||
onIconSelected(dataUri)
|
onIconSelected(dataUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,8 +102,9 @@ export const IconPicker = ({ onIconSelected }: Props) => {
|
|||||||
>
|
>
|
||||||
{displayedIconNames.map((iconName) => (
|
{displayedIconNames.map((iconName) => (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
|
variant={isWhite ? 'solid' : 'ghost'}
|
||||||
|
colorScheme={isWhite ? 'blackAlpha' : undefined}
|
||||||
fontSize="xl"
|
fontSize="xl"
|
||||||
w="38px"
|
w="38px"
|
||||||
h="38px"
|
h="38px"
|
||||||
|
|||||||
@@ -10,30 +10,52 @@ import { IconPicker } from './IconPicker'
|
|||||||
type Tabs = 'link' | 'upload' | 'giphy' | 'emoji' | 'unsplash' | 'icon'
|
type Tabs = 'link' | 'upload' | 'giphy' | 'emoji' | 'unsplash' | 'icon'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
filePath: string
|
filePath: string | undefined
|
||||||
includeFileName?: boolean
|
includeFileName?: boolean
|
||||||
defaultUrl?: string
|
defaultUrl?: string
|
||||||
isEmojiEnabled?: boolean
|
|
||||||
isGiphyEnabled?: boolean
|
|
||||||
isUnsplashEnabled?: boolean
|
|
||||||
imageSize?: 'small' | 'regular' | 'thumb'
|
imageSize?: 'small' | 'regular' | 'thumb'
|
||||||
|
initialTab?: Tabs
|
||||||
onSubmit: (url: string) => void
|
onSubmit: (url: string) => void
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
}
|
} & (
|
||||||
|
| {
|
||||||
|
includedTabs?: Tabs[]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
excludedTabs?: Tabs[]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultDisplayedTabs: Tabs[] = [
|
||||||
|
'link',
|
||||||
|
'upload',
|
||||||
|
'giphy',
|
||||||
|
'emoji',
|
||||||
|
'unsplash',
|
||||||
|
'icon',
|
||||||
|
]
|
||||||
|
|
||||||
export const ImageUploadContent = ({
|
export const ImageUploadContent = ({
|
||||||
filePath,
|
filePath,
|
||||||
includeFileName,
|
includeFileName,
|
||||||
defaultUrl,
|
defaultUrl,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isEmojiEnabled = false,
|
|
||||||
isGiphyEnabled = true,
|
|
||||||
isUnsplashEnabled = true,
|
|
||||||
imageSize = 'regular',
|
imageSize = 'regular',
|
||||||
onClose,
|
onClose,
|
||||||
|
initialTab,
|
||||||
|
...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const includedTabs =
|
||||||
|
'includedTabs' in props
|
||||||
|
? props.includedTabs ?? defaultDisplayedTabs
|
||||||
|
: defaultDisplayedTabs
|
||||||
|
const excludedTabs = 'excludedTabs' in props ? props.excludedTabs ?? [] : []
|
||||||
|
const displayedTabs = defaultDisplayedTabs.filter(
|
||||||
|
(tab) => !excludedTabs.includes(tab) && includedTabs.includes(tab)
|
||||||
|
)
|
||||||
|
|
||||||
const [currentTab, setCurrentTab] = useState<Tabs>(
|
const [currentTab, setCurrentTab] = useState<Tabs>(
|
||||||
isEmojiEnabled ? 'emoji' : 'link'
|
initialTab ?? displayedTabs[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSubmit = (url: string) => {
|
const handleSubmit = (url: string) => {
|
||||||
@@ -44,7 +66,25 @@ export const ImageUploadContent = ({
|
|||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<HStack>
|
<HStack>
|
||||||
{isEmojiEnabled && (
|
{displayedTabs.includes('link') && (
|
||||||
|
<Button
|
||||||
|
variant={currentTab === 'link' ? 'solid' : 'ghost'}
|
||||||
|
onClick={() => setCurrentTab('link')}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Link
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{displayedTabs.includes('upload') && (
|
||||||
|
<Button
|
||||||
|
variant={currentTab === 'upload' ? 'solid' : 'ghost'}
|
||||||
|
onClick={() => setCurrentTab('upload')}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{displayedTabs.includes('emoji') && (
|
||||||
<Button
|
<Button
|
||||||
variant={currentTab === 'emoji' ? 'solid' : 'ghost'}
|
variant={currentTab === 'emoji' ? 'solid' : 'ghost'}
|
||||||
onClick={() => setCurrentTab('emoji')}
|
onClick={() => setCurrentTab('emoji')}
|
||||||
@@ -53,21 +93,7 @@ export const ImageUploadContent = ({
|
|||||||
Emoji
|
Emoji
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
{displayedTabs.includes('giphy') && (
|
||||||
variant={currentTab === 'link' ? 'solid' : 'ghost'}
|
|
||||||
onClick={() => setCurrentTab('link')}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Link
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant={currentTab === 'upload' ? 'solid' : 'ghost'}
|
|
||||||
onClick={() => setCurrentTab('upload')}
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Upload
|
|
||||||
</Button>
|
|
||||||
{isGiphyEnabled && (
|
|
||||||
<Button
|
<Button
|
||||||
variant={currentTab === 'giphy' ? 'solid' : 'ghost'}
|
variant={currentTab === 'giphy' ? 'solid' : 'ghost'}
|
||||||
onClick={() => setCurrentTab('giphy')}
|
onClick={() => setCurrentTab('giphy')}
|
||||||
@@ -76,7 +102,7 @@ export const ImageUploadContent = ({
|
|||||||
Giphy
|
Giphy
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{isUnsplashEnabled && (
|
{displayedTabs.includes('unsplash') && (
|
||||||
<Button
|
<Button
|
||||||
variant={currentTab === 'unsplash' ? 'solid' : 'ghost'}
|
variant={currentTab === 'unsplash' ? 'solid' : 'ghost'}
|
||||||
onClick={() => setCurrentTab('unsplash')}
|
onClick={() => setCurrentTab('unsplash')}
|
||||||
@@ -85,13 +111,15 @@ export const ImageUploadContent = ({
|
|||||||
Unsplash
|
Unsplash
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
{displayedTabs.includes('icon') && (
|
||||||
variant={currentTab === 'icon' ? 'solid' : 'ghost'}
|
<Button
|
||||||
onClick={() => setCurrentTab('icon')}
|
variant={currentTab === 'icon' ? 'solid' : 'ghost'}
|
||||||
size="sm"
|
onClick={() => setCurrentTab('icon')}
|
||||||
>
|
size="sm"
|
||||||
Icon
|
>
|
||||||
</Button>
|
Icon
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|
||||||
<BodyContent
|
<BodyContent
|
||||||
@@ -115,14 +143,15 @@ const BodyContent = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
}: {
|
}: {
|
||||||
includeFileName?: boolean
|
includeFileName?: boolean
|
||||||
filePath: string
|
filePath: string | undefined
|
||||||
tab: Tabs
|
tab: Tabs
|
||||||
defaultUrl?: string
|
defaultUrl?: string
|
||||||
imageSize: 'small' | 'regular' | 'thumb'
|
imageSize: 'small' | 'regular' | 'thumb'
|
||||||
onSubmit: (url: string) => void
|
onSubmit: (url: string) => void
|
||||||
}) => {
|
}) => {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
case 'upload':
|
case 'upload': {
|
||||||
|
if (!filePath) return null
|
||||||
return (
|
return (
|
||||||
<UploadFileContent
|
<UploadFileContent
|
||||||
filePath={filePath}
|
filePath={filePath}
|
||||||
@@ -130,6 +159,7 @@ const BodyContent = ({
|
|||||||
onNewUrl={onSubmit}
|
onNewUrl={onSubmit}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
case 'link':
|
case 'link':
|
||||||
return <EmbedLinkContent defaultUrl={defaultUrl} onNewUrl={onSubmit} />
|
return <EmbedLinkContent defaultUrl={defaultUrl} onNewUrl={onSubmit} />
|
||||||
case 'giphy':
|
case 'giphy':
|
||||||
|
|||||||
@@ -9,11 +9,6 @@ import { JavascriptBubbleSnippet } from '../JavascriptBubbleSnippet'
|
|||||||
export const parseDefaultBubbleTheme = (typebot?: Typebot) => ({
|
export const parseDefaultBubbleTheme = (typebot?: Typebot) => ({
|
||||||
button: {
|
button: {
|
||||||
backgroundColor: typebot?.theme.chat.buttons.backgroundColor,
|
backgroundColor: typebot?.theme.chat.buttons.backgroundColor,
|
||||||
iconColor: typebot?.theme.chat.buttons.color,
|
|
||||||
},
|
|
||||||
previewMessage: {
|
|
||||||
backgroundColor: typebot?.theme.general.background.content ?? 'white',
|
|
||||||
textColor: 'black',
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import { Stack, Heading, HStack, Flex, Text, Image } from '@chakra-ui/react'
|
import {
|
||||||
|
Stack,
|
||||||
|
Heading,
|
||||||
|
HStack,
|
||||||
|
Flex,
|
||||||
|
Text,
|
||||||
|
Image,
|
||||||
|
chakra,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
import { BubbleProps } from '@typebot.io/js'
|
import { BubbleProps } from '@typebot.io/js'
|
||||||
import { isDefined } from '@typebot.io/lib'
|
import { isDefined, isSvgSrc } from '@typebot.io/lib'
|
||||||
import { PreviewMessageSettings } from './PreviewMessageSettings'
|
import { PreviewMessageSettings } from './PreviewMessageSettings'
|
||||||
import { ThemeSettings } from './ThemeSettings'
|
import { ThemeSettings } from './ThemeSettings'
|
||||||
|
import { isLight } from '@typebot.io/lib/hexToRgb'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
defaultPreviewMessageAvatar: string
|
defaultPreviewMessageAvatar: string
|
||||||
@@ -79,24 +88,72 @@ export const BubbleSettings = ({
|
|||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
boxSize="3rem"
|
transition="all 0.2s ease-in-out"
|
||||||
|
boxSize={theme?.button?.size === 'large' ? '64px' : '48px'}
|
||||||
bgColor={theme?.button?.backgroundColor}
|
bgColor={theme?.button?.backgroundColor}
|
||||||
rounded="full"
|
rounded="full"
|
||||||
|
boxShadow="0 0 #0000,0 0 #0000,0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1)"
|
||||||
>
|
>
|
||||||
<svg
|
<BubbleIcon buttonTheme={theme?.button} />
|
||||||
viewBox="0 0 24 24"
|
|
||||||
style={{
|
|
||||||
stroke: theme?.button?.iconColor,
|
|
||||||
}}
|
|
||||||
width="30px"
|
|
||||||
strokeWidth="2px"
|
|
||||||
fill="transparent"
|
|
||||||
>
|
|
||||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
|
||||||
</svg>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BubbleIcon = ({
|
||||||
|
buttonTheme,
|
||||||
|
}: {
|
||||||
|
buttonTheme: NonNullable<BubbleProps['theme']>['button']
|
||||||
|
}) => {
|
||||||
|
if (!buttonTheme?.customIconSrc)
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
style={{
|
||||||
|
stroke: buttonTheme?.backgroundColor
|
||||||
|
? isLight(buttonTheme?.backgroundColor)
|
||||||
|
? '#000'
|
||||||
|
: '#fff'
|
||||||
|
: '#fff',
|
||||||
|
transition: 'all 0.2s ease-in-out',
|
||||||
|
}}
|
||||||
|
width={buttonTheme?.size === 'large' ? '36px' : '28px'}
|
||||||
|
strokeWidth="2px"
|
||||||
|
fill="transparent"
|
||||||
|
>
|
||||||
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
buttonTheme.customIconSrc.startsWith('http') ||
|
||||||
|
buttonTheme.customIconSrc.startsWith('data:image/svg+xml')
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src={buttonTheme.customIconSrc}
|
||||||
|
transition="all 0.2s ease-in-out"
|
||||||
|
boxSize={
|
||||||
|
isSvgSrc(buttonTheme.customIconSrc)
|
||||||
|
? buttonTheme?.size === 'large'
|
||||||
|
? '36px'
|
||||||
|
: '28px'
|
||||||
|
: '90%'
|
||||||
|
}
|
||||||
|
rounded={isSvgSrc(buttonTheme.customIconSrc) ? undefined : 'full'}
|
||||||
|
alt="Bubble button icon"
|
||||||
|
objectFit={isSvgSrc(buttonTheme.customIconSrc) ? undefined : 'cover'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<chakra.span
|
||||||
|
transition="all 0.2s ease-in-out"
|
||||||
|
fontSize={buttonTheme.size === 'large' ? '36px' : '24px'}
|
||||||
|
lineHeight={buttonTheme.size === 'large' ? '40px' : '32px'}
|
||||||
|
>
|
||||||
|
{buttonTheme.customIconSrc}
|
||||||
|
</chakra.span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
import { ColorPicker } from '@/components/ColorPicker'
|
import { ColorPicker } from '@/components/ColorPicker'
|
||||||
import { Heading, HStack, Input, Stack, Text } from '@chakra-ui/react'
|
import { ImageUploadContent } from '@/components/ImageUploadContent'
|
||||||
|
import { ChevronDownIcon } from '@/components/icons'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Heading,
|
||||||
|
HStack,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
Popover,
|
||||||
|
PopoverAnchor,
|
||||||
|
PopoverContent,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
useDisclosure,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
import { ButtonTheme } from '@typebot.io/js/dist/features/bubble/types'
|
import { ButtonTheme } from '@typebot.io/js/dist/features/bubble/types'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
@@ -9,6 +25,8 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ButtonThemeSettings = ({ buttonTheme, onChange }: Props) => {
|
export const ButtonThemeSettings = ({ buttonTheme, onChange }: Props) => {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
|
|
||||||
const updateBackgroundColor = (backgroundColor: string) => {
|
const updateBackgroundColor = (backgroundColor: string) => {
|
||||||
onChange({
|
onChange({
|
||||||
...buttonTheme,
|
...buttonTheme,
|
||||||
@@ -16,48 +34,58 @@ export const ButtonThemeSettings = ({ buttonTheme, onChange }: Props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateIconColor = (iconColor: string) => {
|
|
||||||
onChange({
|
|
||||||
...buttonTheme,
|
|
||||||
iconColor,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateCustomIconSrc = (customIconSrc: string) => {
|
const updateCustomIconSrc = (customIconSrc: string) => {
|
||||||
onChange({
|
onChange({
|
||||||
...buttonTheme,
|
...buttonTheme,
|
||||||
customIconSrc,
|
customIconSrc,
|
||||||
})
|
})
|
||||||
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateSize = (size: ButtonTheme['size']) =>
|
||||||
|
onChange({
|
||||||
|
...buttonTheme,
|
||||||
|
size,
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4} borderWidth="1px" rounded="md" p="4">
|
<Stack spacing={4} borderWidth="1px" rounded="md" p="4">
|
||||||
<Heading size="sm">Button</Heading>
|
<Heading size="sm">Button</Heading>
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<HStack justify="space-between">
|
<HStack justify="space-between">
|
||||||
<Text>Background color</Text>
|
<Text>Size</Text>
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={Button} size="sm" rightIcon={<ChevronDownIcon />}>
|
||||||
|
{buttonTheme?.size ?? 'medium'}
|
||||||
|
</MenuButton>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem onClick={() => updateSize('medium')}>medium</MenuItem>
|
||||||
|
<MenuItem onClick={() => updateSize('large')}>large</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</HStack>
|
||||||
|
<HStack justify="space-between">
|
||||||
|
<Text>Color</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
defaultValue={buttonTheme?.backgroundColor}
|
defaultValue={buttonTheme?.backgroundColor}
|
||||||
onColorChange={updateBackgroundColor}
|
onColorChange={updateBackgroundColor}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack justify="space-between">
|
|
||||||
<Text>Icon color</Text>
|
|
||||||
<ColorPicker
|
|
||||||
defaultValue={buttonTheme?.iconColor}
|
|
||||||
onColorChange={updateIconColor}
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
<HStack justify="space-between">
|
<HStack justify="space-between">
|
||||||
<Text>Custom icon</Text>
|
<Text>Custom icon</Text>
|
||||||
<Input
|
<Popover isLazy isOpen={isOpen}>
|
||||||
placeholder={'Paste image link (.png, .svg)'}
|
<PopoverAnchor>
|
||||||
value={buttonTheme?.customIconSrc}
|
<Button size="sm" onClick={onOpen}>
|
||||||
onChange={(e) => updateCustomIconSrc(e.target.value)}
|
Pick an image
|
||||||
minW="0"
|
</Button>
|
||||||
w="300px"
|
</PopoverAnchor>
|
||||||
size="sm"
|
<PopoverContent p="4" w="500px">
|
||||||
/>
|
<ImageUploadContent
|
||||||
|
onSubmit={updateCustomIconSrc}
|
||||||
|
filePath={undefined}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ const parseButtonTheme = (
|
|||||||
)
|
)
|
||||||
const iconColorLine = parseStringParam('iconColor', iconColor)
|
const iconColorLine = parseStringParam('iconColor', iconColor)
|
||||||
const customIconLine = parseStringParam('customIconSrc', customIconSrc)
|
const customIconLine = parseStringParam('customIconSrc', customIconSrc)
|
||||||
const line = `button: {${backgroundColorLine}${iconColorLine}${customIconLine}},`
|
const sizeLine = parseStringParam('size', button.size)
|
||||||
|
const line = `button: {${backgroundColorLine}${iconColorLine}${customIconLine}${sizeLine}},`
|
||||||
if (line === 'button: {},') return ''
|
if (line === 'button: {},') return ''
|
||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export const MetadataForm = ({
|
|||||||
filePath={`typebots/${typebotId}/favIcon`}
|
filePath={`typebots/${typebotId}/favIcon`}
|
||||||
defaultUrl={metadata.favIconUrl ?? ''}
|
defaultUrl={metadata.favIconUrl ?? ''}
|
||||||
onSubmit={handleFavIconSubmit}
|
onSubmit={handleFavIconSubmit}
|
||||||
isGiphyEnabled={false}
|
excludedTabs={['giphy', 'unsplash', 'emoji']}
|
||||||
imageSize="thumb"
|
imageSize="thumb"
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
@@ -90,7 +90,7 @@ export const MetadataForm = ({
|
|||||||
filePath={`typebots/${typebotId}/ogImage`}
|
filePath={`typebots/${typebotId}/ogImage`}
|
||||||
defaultUrl={metadata.imageUrl}
|
defaultUrl={metadata.imageUrl}
|
||||||
onSubmit={handleImageSubmit}
|
onSubmit={handleImageSubmit}
|
||||||
isGiphyEnabled={false}
|
excludedTabs={['giphy', 'icon', 'emoji']}
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export const BackgroundContent = ({
|
|||||||
filePath={`typebots/${typebot?.id}/background`}
|
filePath={`typebots/${typebot?.id}/background`}
|
||||||
defaultUrl={background.content}
|
defaultUrl={background.content}
|
||||||
onSubmit={handleContentChange}
|
onSubmit={handleContentChange}
|
||||||
isGiphyEnabled={false}
|
excludedTabs={['giphy', 'icon']}
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/js",
|
"name": "@typebot.io/js",
|
||||||
"version": "0.0.50",
|
"version": "0.0.51",
|
||||||
"description": "Javascript library to display typebots on your website",
|
"description": "Javascript library to display typebots on your website",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Show } from 'solid-js'
|
import { Show } from 'solid-js'
|
||||||
import { isNotDefined } from '@typebot.io/lib'
|
import { isNotDefined, isSvgSrc } from '@typebot.io/lib'
|
||||||
import { ButtonTheme } from '../types'
|
import { ButtonTheme } from '../types'
|
||||||
|
import { isLight } from '@typebot.io/lib/hexToRgb'
|
||||||
|
|
||||||
type Props = ButtonTheme & {
|
type Props = ButtonTheme & {
|
||||||
isBotOpened: boolean
|
isBotOpened: boolean
|
||||||
@@ -8,7 +9,9 @@ type Props = ButtonTheme & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultButtonColor = '#0042DA'
|
const defaultButtonColor = '#0042DA'
|
||||||
const defaultIconColor = 'white'
|
|
||||||
|
const isImageSrc = (src: string) =>
|
||||||
|
src.startsWith('http') || src.startsWith('data:image/svg+xml')
|
||||||
|
|
||||||
export const BubbleButton = (props: Props) => {
|
export const BubbleButton = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
@@ -28,7 +31,11 @@ export const BubbleButton = (props: Props) => {
|
|||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
style={{
|
style={{
|
||||||
stroke: props.iconColor ?? defaultIconColor,
|
stroke:
|
||||||
|
props.iconColor ??
|
||||||
|
isLight(props.backgroundColor ?? defaultButtonColor)
|
||||||
|
? '#27272A'
|
||||||
|
: '#fff',
|
||||||
}}
|
}}
|
||||||
class={
|
class={
|
||||||
`stroke-2 fill-transparent absolute duration-200 transition ` +
|
`stroke-2 fill-transparent absolute duration-200 transition ` +
|
||||||
@@ -41,18 +48,42 @@ export const BubbleButton = (props: Props) => {
|
|||||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
||||||
</svg>
|
</svg>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.customIconSrc}>
|
<Show when={props.customIconSrc && isImageSrc(props.customIconSrc)}>
|
||||||
<img
|
<img
|
||||||
src={props.customIconSrc}
|
src={props.customIconSrc}
|
||||||
class={
|
class={
|
||||||
'rounded-full object-cover' +
|
'duration-200 transition' +
|
||||||
(props.size === 'large' ? ' w-9 h-9' : ' w-7 h-7')
|
(props.isBotOpened
|
||||||
|
? ' scale-0 opacity-0'
|
||||||
|
: ' scale-100 opacity-100') +
|
||||||
|
(isSvgSrc(props.customIconSrc)
|
||||||
|
? props.size === 'large'
|
||||||
|
? ' w-9 h-9'
|
||||||
|
: ' w-7 h-7'
|
||||||
|
: ' w-[90%] h-[90%]') +
|
||||||
|
(isSvgSrc(props.customIconSrc) ? '' : ' object-cover rounded-full')
|
||||||
}
|
}
|
||||||
alt="Bubble button icon"
|
alt="Bubble button icon"
|
||||||
elementtiming={'Bubble button icon'}
|
elementtiming={'Bubble button icon'}
|
||||||
fetchpriority={'high'}
|
fetchpriority={'high'}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={props.customIconSrc && !isImageSrc(props.customIconSrc)}>
|
||||||
|
<span
|
||||||
|
class={
|
||||||
|
'text-4xl duration-200 transition' +
|
||||||
|
(props.isBotOpened
|
||||||
|
? ' scale-0 opacity-0'
|
||||||
|
: ' scale-100 opacity-100')
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
'font-family':
|
||||||
|
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.customIconSrc}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import {
|
|||||||
Theme,
|
Theme,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
import { BackgroundType } from '@typebot.io/schemas/features/typebot/theme/enums'
|
import { BackgroundType } from '@typebot.io/schemas/features/typebot/theme/enums'
|
||||||
import { hexToRgb } from './hexToRgb'
|
import { isLight, hexToRgb } from '@typebot.io/lib/hexToRgb'
|
||||||
import { isLight } from './hexToRgb'
|
|
||||||
|
|
||||||
const cssVariableNames = {
|
const cssVariableNames = {
|
||||||
general: {
|
general: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/react",
|
"name": "@typebot.io/react",
|
||||||
"version": "0.0.50",
|
"version": "0.0.51",
|
||||||
"description": "React library to display typebots on your website",
|
"description": "React library to display typebots on your website",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user