2
0

(embed) Add size and icon picker in bubble settings (#508)

This commit is contained in:
Baptiste Arnaud
2023-05-15 15:22:04 +02:00
committed by GitHub
parent 123926f273
commit 0f91b34497
15 changed files with 256 additions and 108 deletions

View File

@@ -19,7 +19,7 @@ import tinyColor from 'tinycolor2'
const colorsSelection: `#${string}`[] = [
'#666460',
'#AFABA3',
'#FFFFFF',
'#A87964',
'#D09C46',
'#DE8031',
@@ -27,7 +27,7 @@ const colorsSelection: `#${string}`[] = [
'#4A8BB2',
'#9B74B7',
'#C75F96',
'#C75F96',
'#0042DA',
]
type Props = {
@@ -78,18 +78,19 @@ export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
</PopoverHeader>
<PopoverBody as={Stack}>
<SimpleGrid columns={5} spacing={2}>
{colorsSelection.map((c) => (
{colorsSelection.map((color) => (
<Button
key={c}
aria-label={c}
background={c}
key={color}
aria-label={color}
background={color}
height="22px"
width="22px"
padding={0}
minWidth="unset"
borderRadius={3}
_hover={{ background: c }}
onClick={handleClick(c)}
borderWidth={color === '#FFFFFF' ? 1 : undefined}
_hover={{ background: color }}
onClick={handleClick(color)}
/>
))}
</SimpleGrid>

View File

@@ -57,10 +57,9 @@ export const EditableEmojiOrImageIcon = ({
filePath={uploadFilePath}
defaultUrl={icon ?? ''}
onSubmit={onChangeIcon}
isGiphyEnabled={false}
isUnsplashEnabled={false}
isEmojiEnabled={true}
excludedTabs={['giphy', 'unsplash']}
onClose={onClose}
initialTab="icon"
/>
</PopoverContent>
</>

View File

@@ -26,6 +26,13 @@ export const IconPicker = ({ onIconSelected }: Props) => {
)
const searchQuery = useRef<string>('')
const [selectedColor, setSelectedColor] = useState(initialIconColor)
const isWhite = useMemo(
() =>
selectedColor.toLowerCase() === '#ffffff' ||
selectedColor.toLowerCase() === '#fff' ||
selectedColor === 'white',
[selectedColor]
)
useEffect(() => {
if (!bottomElement.current) return
@@ -68,10 +75,9 @@ export const IconPicker = ({ onIconSelected }: Props) => {
const selectIcon = async (iconName: string) => {
const svg = await (await fetch(`/icons/${iconName}.svg`)).text()
const dataUri = `data:image/svg+xml;utf8,${svg.replace(
'<svg',
`<svg fill='${encodeURIComponent(selectedColor)}'`
)}`
const dataUri = `data:image/svg+xml;utf8,${svg
.replace('<svg', `<svg fill='${encodeURIComponent(selectedColor)}'`)
.replace(/"/g, "'")}`
onIconSelected(dataUri)
}
@@ -96,8 +102,9 @@ export const IconPicker = ({ onIconSelected }: Props) => {
>
{displayedIconNames.map((iconName) => (
<Button
variant="ghost"
size="sm"
variant={isWhite ? 'solid' : 'ghost'}
colorScheme={isWhite ? 'blackAlpha' : undefined}
fontSize="xl"
w="38px"
h="38px"

View File

@@ -10,30 +10,52 @@ import { IconPicker } from './IconPicker'
type Tabs = 'link' | 'upload' | 'giphy' | 'emoji' | 'unsplash' | 'icon'
type Props = {
filePath: string
filePath: string | undefined
includeFileName?: boolean
defaultUrl?: string
isEmojiEnabled?: boolean
isGiphyEnabled?: boolean
isUnsplashEnabled?: boolean
imageSize?: 'small' | 'regular' | 'thumb'
initialTab?: Tabs
onSubmit: (url: string) => void
onClose?: () => void
}
} & (
| {
includedTabs?: Tabs[]
}
| {
excludedTabs?: Tabs[]
}
)
const defaultDisplayedTabs: Tabs[] = [
'link',
'upload',
'giphy',
'emoji',
'unsplash',
'icon',
]
export const ImageUploadContent = ({
filePath,
includeFileName,
defaultUrl,
onSubmit,
isEmojiEnabled = false,
isGiphyEnabled = true,
isUnsplashEnabled = true,
imageSize = 'regular',
onClose,
initialTab,
...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>(
isEmojiEnabled ? 'emoji' : 'link'
initialTab ?? displayedTabs[0]
)
const handleSubmit = (url: string) => {
@@ -44,7 +66,25 @@ export const ImageUploadContent = ({
return (
<Stack>
<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
variant={currentTab === 'emoji' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('emoji')}
@@ -53,21 +93,7 @@ export const ImageUploadContent = ({
Emoji
</Button>
)}
<Button
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 && (
{displayedTabs.includes('giphy') && (
<Button
variant={currentTab === 'giphy' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('giphy')}
@@ -76,7 +102,7 @@ export const ImageUploadContent = ({
Giphy
</Button>
)}
{isUnsplashEnabled && (
{displayedTabs.includes('unsplash') && (
<Button
variant={currentTab === 'unsplash' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('unsplash')}
@@ -85,13 +111,15 @@ export const ImageUploadContent = ({
Unsplash
</Button>
)}
<Button
variant={currentTab === 'icon' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('icon')}
size="sm"
>
Icon
</Button>
{displayedTabs.includes('icon') && (
<Button
variant={currentTab === 'icon' ? 'solid' : 'ghost'}
onClick={() => setCurrentTab('icon')}
size="sm"
>
Icon
</Button>
)}
</HStack>
<BodyContent
@@ -115,14 +143,15 @@ const BodyContent = ({
onSubmit,
}: {
includeFileName?: boolean
filePath: string
filePath: string | undefined
tab: Tabs
defaultUrl?: string
imageSize: 'small' | 'regular' | 'thumb'
onSubmit: (url: string) => void
}) => {
switch (tab) {
case 'upload':
case 'upload': {
if (!filePath) return null
return (
<UploadFileContent
filePath={filePath}
@@ -130,6 +159,7 @@ const BodyContent = ({
onNewUrl={onSubmit}
/>
)
}
case 'link':
return <EmbedLinkContent defaultUrl={defaultUrl} onNewUrl={onSubmit} />
case 'giphy':

View File

@@ -9,11 +9,6 @@ import { JavascriptBubbleSnippet } from '../JavascriptBubbleSnippet'
export const parseDefaultBubbleTheme = (typebot?: Typebot) => ({
button: {
backgroundColor: typebot?.theme.chat.buttons.backgroundColor,
iconColor: typebot?.theme.chat.buttons.color,
},
previewMessage: {
backgroundColor: typebot?.theme.general.background.content ?? 'white',
textColor: 'black',
},
})

View File

@@ -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 { isDefined } from '@typebot.io/lib'
import { isDefined, isSvgSrc } from '@typebot.io/lib'
import { PreviewMessageSettings } from './PreviewMessageSettings'
import { ThemeSettings } from './ThemeSettings'
import { isLight } from '@typebot.io/lib/hexToRgb'
type Props = {
defaultPreviewMessageAvatar: string
@@ -79,24 +88,72 @@ export const BubbleSettings = ({
<Flex
align="center"
justifyContent="center"
boxSize="3rem"
transition="all 0.2s ease-in-out"
boxSize={theme?.button?.size === 'large' ? '64px' : '48px'}
bgColor={theme?.button?.backgroundColor}
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
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>
<BubbleIcon buttonTheme={theme?.button} />
</Flex>
</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>
)
}

View File

@@ -1,5 +1,21 @@
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 React from 'react'
@@ -9,6 +25,8 @@ type Props = {
}
export const ButtonThemeSettings = ({ buttonTheme, onChange }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure()
const updateBackgroundColor = (backgroundColor: string) => {
onChange({
...buttonTheme,
@@ -16,48 +34,58 @@ export const ButtonThemeSettings = ({ buttonTheme, onChange }: Props) => {
})
}
const updateIconColor = (iconColor: string) => {
onChange({
...buttonTheme,
iconColor,
})
}
const updateCustomIconSrc = (customIconSrc: string) => {
onChange({
...buttonTheme,
customIconSrc,
})
onClose()
}
const updateSize = (size: ButtonTheme['size']) =>
onChange({
...buttonTheme,
size,
})
return (
<Stack spacing={4} borderWidth="1px" rounded="md" p="4">
<Heading size="sm">Button</Heading>
<Stack spacing={4}>
<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
defaultValue={buttonTheme?.backgroundColor}
onColorChange={updateBackgroundColor}
/>
</HStack>
<HStack justify="space-between">
<Text>Icon color</Text>
<ColorPicker
defaultValue={buttonTheme?.iconColor}
onColorChange={updateIconColor}
/>
</HStack>
<HStack justify="space-between">
<Text>Custom icon</Text>
<Input
placeholder={'Paste image link (.png, .svg)'}
value={buttonTheme?.customIconSrc}
onChange={(e) => updateCustomIconSrc(e.target.value)}
minW="0"
w="300px"
size="sm"
/>
<Popover isLazy isOpen={isOpen}>
<PopoverAnchor>
<Button size="sm" onClick={onOpen}>
Pick an image
</Button>
</PopoverAnchor>
<PopoverContent p="4" w="500px">
<ImageUploadContent
onSubmit={updateCustomIconSrc}
filePath={undefined}
/>
</PopoverContent>
</Popover>
</HStack>
</Stack>
</Stack>

View File

@@ -19,7 +19,8 @@ const parseButtonTheme = (
)
const iconColorLine = parseStringParam('iconColor', iconColor)
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 ''
return line
}

View File

@@ -64,7 +64,7 @@ export const MetadataForm = ({
filePath={`typebots/${typebotId}/favIcon`}
defaultUrl={metadata.favIconUrl ?? ''}
onSubmit={handleFavIconSubmit}
isGiphyEnabled={false}
excludedTabs={['giphy', 'unsplash', 'emoji']}
imageSize="thumb"
/>
</PopoverContent>
@@ -90,7 +90,7 @@ export const MetadataForm = ({
filePath={`typebots/${typebotId}/ogImage`}
defaultUrl={metadata.imageUrl}
onSubmit={handleImageSubmit}
isGiphyEnabled={false}
excludedTabs={['giphy', 'icon', 'emoji']}
/>
</PopoverContent>
</Popover>

View File

@@ -62,7 +62,7 @@ export const BackgroundContent = ({
filePath={`typebots/${typebot?.id}/background`}
defaultUrl={background.content}
onSubmit={handleContentChange}
isGiphyEnabled={false}
excludedTabs={['giphy', 'icon']}
/>
</PopoverContent>
</Popover>