💄 (buttons) Improve multiple choice form UI
This commit is contained in:
@ -45,9 +45,8 @@ test.describe.parallel('Buttons input block', () => {
|
|||||||
await expect(page.locator('text=Item 2')).toBeHidden()
|
await expect(page.locator('text=Item 2')).toBeHidden()
|
||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Preview')
|
||||||
const item3Button = page.locator('button >> text=Item 3')
|
await page.getByRole('button', { name: 'Item 3' }).click()
|
||||||
await item3Button.click()
|
await expect(page.getByRole('button', { name: 'Item 3' })).toBeHidden()
|
||||||
await expect(item3Button).toBeHidden()
|
|
||||||
await expect(page.getByTestId('guest-bubble')).toHaveText('Item 3')
|
await expect(page.getByTestId('guest-bubble')).toHaveText('Item 3')
|
||||||
await page.click('button[aria-label="Close"]')
|
await page.click('button[aria-label="Close"]')
|
||||||
|
|
||||||
@ -67,8 +66,8 @@ test.describe.parallel('Buttons input block', () => {
|
|||||||
|
|
||||||
await page.click('text=Preview')
|
await page.click('text=Preview')
|
||||||
|
|
||||||
await page.locator('button >> text="Item 3"').click()
|
await page.getByRole('checkbox', { name: 'Item 3' }).click()
|
||||||
await page.locator('button >> text="Item 1"').click()
|
await page.getByRole('checkbox', { name: 'Item 1' }).click()
|
||||||
await page.locator('text=Go').click()
|
await page.locator('text=Go').click()
|
||||||
|
|
||||||
await expect(page.locator('text="Item 3, Item 1"')).toBeVisible()
|
await expect(page.locator('text="Item 3, Item 1"')).toBeVisible()
|
||||||
|
@ -18,7 +18,7 @@ test.describe.parallel('Theme page', () => {
|
|||||||
id: typebotId,
|
id: typebotId,
|
||||||
})
|
})
|
||||||
await page.goto(`/typebots/${typebotId}/theme`)
|
await page.goto(`/typebots/${typebotId}/theme`)
|
||||||
await expect(page.locator('button >> text="Go"')).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
|
||||||
|
|
||||||
// Font
|
// Font
|
||||||
await page.getByRole('button', { name: 'Font & Background' }).click()
|
await page.getByRole('button', { name: 'Font & Background' }).click()
|
||||||
@ -70,7 +70,7 @@ test.describe.parallel('Theme page', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await page.goto(`/typebots/${typebotId}/theme`)
|
await page.goto(`/typebots/${typebotId}/theme`)
|
||||||
await expect(page.locator('button >> text="Go"')).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
|
||||||
await page.click('button:has-text("Chat")')
|
await page.click('button:has-text("Chat")')
|
||||||
|
|
||||||
// Host avatar
|
// Host avatar
|
||||||
@ -81,7 +81,7 @@ test.describe.parallel('Theme page', () => {
|
|||||||
'input[placeholder="Paste the image link..."]',
|
'input[placeholder="Paste the image link..."]',
|
||||||
hostAvatarUrl
|
hostAvatarUrl
|
||||||
)
|
)
|
||||||
await page.locator('button >> text="Go"').click()
|
await page.getByRole('button', { name: 'Go' }).click()
|
||||||
|
|
||||||
await expect(page.locator('.typebot-container img')).toHaveAttribute(
|
await expect(page.locator('.typebot-container img')).toHaveAttribute(
|
||||||
'src',
|
'src',
|
||||||
@ -155,7 +155,7 @@ test.describe.parallel('Theme page', () => {
|
|||||||
'[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1'
|
'[data-testid="guest-bubbles-theme"] >> [aria-label="Pick a color"] >> nth=1'
|
||||||
)
|
)
|
||||||
await page.fill('input[value="#FFFFFF"]', '#264653')
|
await page.fill('input[value="#FFFFFF"]', '#264653')
|
||||||
await page.locator('button >> text="Go"').click()
|
await page.getByRole('button', { name: 'Go' }).click()
|
||||||
const guestBubble = page.locator('[data-testid="guest-bubble"] >> nth=-1')
|
const guestBubble = page.locator('[data-testid="guest-bubble"] >> nth=-1')
|
||||||
await expect(guestBubble).toHaveCSS(
|
await expect(guestBubble).toHaveCSS(
|
||||||
'background-color',
|
'background-color',
|
||||||
@ -173,7 +173,7 @@ test.describe.parallel('Theme page', () => {
|
|||||||
await page
|
await page
|
||||||
.locator('input[placeholder="Paste the image link..."]')
|
.locator('input[placeholder="Paste the image link..."]')
|
||||||
.fill(guestAvatarUrl)
|
.fill(guestAvatarUrl)
|
||||||
await page.locator('button >> text="Go"').click()
|
await page.getByRole('button', { name: 'Go' }).click()
|
||||||
await expect(page.locator('.typebot-container img')).toHaveAttribute(
|
await expect(page.locator('.typebot-container img')).toHaveAttribute(
|
||||||
'src',
|
'src',
|
||||||
guestAvatarUrl
|
guestAvatarUrl
|
||||||
@ -202,7 +202,7 @@ test.describe.parallel('Theme page', () => {
|
|||||||
id: typebotId,
|
id: typebotId,
|
||||||
})
|
})
|
||||||
await page.goto(`/typebots/${typebotId}/theme`)
|
await page.goto(`/typebots/${typebotId}/theme`)
|
||||||
await expect(page.locator('button >> text="Go"')).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
|
||||||
await page.click('button:has-text("Custom CSS")')
|
await page.click('button:has-text("Custom CSS")')
|
||||||
await page.fill(
|
await page.fill(
|
||||||
'div[role="textbox"]',
|
'div[role="textbox"]',
|
||||||
@ -222,7 +222,7 @@ test.describe.parallel('Theme page', () => {
|
|||||||
id: typebotId,
|
id: typebotId,
|
||||||
})
|
})
|
||||||
await page.goto(`/typebots/${typebotId}/theme`)
|
await page.goto(`/typebots/${typebotId}/theme`)
|
||||||
await expect(page.locator('button >> text="Go"')).toBeVisible()
|
await expect(page.getByRole('button', { name: 'Go' })).toBeVisible()
|
||||||
await page.getByRole('button', { name: 'Templates New!' }).click()
|
await page.getByRole('button', { name: 'Templates New!' }).click()
|
||||||
await page.getByRole('button', { name: 'Save current theme' }).click()
|
await page.getByRole('button', { name: 'Save current theme' }).click()
|
||||||
await page.getByPlaceholder('My template').fill('My awesome theme')
|
await page.getByPlaceholder('My template').fill('My awesome theme')
|
||||||
|
@ -14,7 +14,7 @@ test('Big groups should work as expected', async ({ page }) => {
|
|||||||
await page.locator('input').press('Enter')
|
await page.locator('input').press('Enter')
|
||||||
await page.locator('input').fill('26')
|
await page.locator('input').fill('26')
|
||||||
await page.locator('input').press('Enter')
|
await page.locator('input').press('Enter')
|
||||||
await page.locator('button >> text=Yes').click()
|
await page.getByRole('button', { name: 'Yes' }).click()
|
||||||
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
|
await page.goto(`${process.env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
|
||||||
await expect(page.locator('text="Baptiste"')).toBeVisible()
|
await expect(page.locator('text="Baptiste"')).toBeVisible()
|
||||||
await expect(page.locator('text="26"')).toBeVisible()
|
await expect(page.locator('text="26"')).toBeVisible()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/js",
|
"name": "@typebot.io/js",
|
||||||
"version": "0.0.39",
|
"version": "0.0.40",
|
||||||
"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",
|
||||||
|
@ -6,10 +6,14 @@
|
|||||||
--typebot-container-bg-image: none;
|
--typebot-container-bg-image: none;
|
||||||
--typebot-container-bg-color: transparent;
|
--typebot-container-bg-color: transparent;
|
||||||
--typebot-container-font-family: 'Open Sans';
|
--typebot-container-font-family: 'Open Sans';
|
||||||
|
--typebot-container-color: #303235;
|
||||||
|
|
||||||
--typebot-button-bg-color: #0042da;
|
--typebot-button-bg-color: #0042da;
|
||||||
|
--typebot-button-bg-color-rgb: 0, 66, 218;
|
||||||
--typebot-button-color: #ffffff;
|
--typebot-button-color: #ffffff;
|
||||||
|
|
||||||
|
--typebot-checkbox-bg-color: #ffffff;
|
||||||
|
|
||||||
--typebot-host-bubble-bg-color: #f7f8ff;
|
--typebot-host-bubble-bg-color: #f7f8ff;
|
||||||
--typebot-host-bubble-color: #303235;
|
--typebot-host-bubble-color: #303235;
|
||||||
|
|
||||||
@ -134,6 +138,7 @@ textarea {
|
|||||||
background-color: var(--typebot-button-bg-color);
|
background-color: var(--typebot-button-bg-color);
|
||||||
border: 1px solid var(--typebot-button-bg-color);
|
border: 1px solid var(--typebot-button-bg-color);
|
||||||
border-radius: var(--typebot-border-radius);
|
border-radius: var(--typebot-border-radius);
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.typebot-button.selectable {
|
.typebot-button.selectable {
|
||||||
@ -142,6 +147,38 @@ textarea {
|
|||||||
border: 1px solid var(--typebot-button-bg-color);
|
border: 1px solid var(--typebot-button-bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.typebot-selectable {
|
||||||
|
border: 1px solid rgba(var(--typebot-button-bg-color-rgb), 0.25);
|
||||||
|
border-radius: var(--typebot-border-radius);
|
||||||
|
color: var(--typebot-container-color);
|
||||||
|
background-color: rgba(var(--typebot-button-bg-color-rgb), 0.08);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typebot-selectable:hover {
|
||||||
|
background-color: rgba(var(--typebot-button-bg-color-rgb), 0.12);
|
||||||
|
border-color: rgba(var(--typebot-button-bg-color-rgb), 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typebot-selectable.selected {
|
||||||
|
background-color: rgba(var(--typebot-button-bg-color-rgb), 0.18);
|
||||||
|
border-color: rgba(var(--typebot-button-bg-color-rgb), 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.typebot-checkbox {
|
||||||
|
border: 1px solid var(--typebot-button-bg-color);
|
||||||
|
border-radius: var(--typebot-border-radius);
|
||||||
|
background-color: var(--typebot-checkbox-bg-color);
|
||||||
|
color: var(--typebot-button-color);
|
||||||
|
padding: 1px;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.typebot-checkbox.checked {
|
||||||
|
background-color: var(--typebot-button-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
.typebot-host-bubble {
|
.typebot-host-bubble {
|
||||||
color: var(--typebot-host-bubble-color);
|
color: var(--typebot-host-bubble-color);
|
||||||
}
|
}
|
||||||
@ -220,6 +257,7 @@ textarea {
|
|||||||
|
|
||||||
.typebot-upload-input {
|
.typebot-upload-input {
|
||||||
transition: border-color 100ms ease-out;
|
transition: border-color 100ms ease-out;
|
||||||
|
border-radius: var(--typebot-border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
.typebot-upload-input.dragging-over {
|
.typebot-upload-input.dragging-over {
|
||||||
|
33
packages/embeds/js/src/components/Button.tsx
Normal file
33
packages/embeds/js/src/components/Button.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { children, JSX, Show, splitProps } from 'solid-js'
|
||||||
|
import { Spinner } from './Spinner'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
variant?: 'primary' | 'secondary'
|
||||||
|
children: JSX.Element
|
||||||
|
isDisabled?: boolean
|
||||||
|
isLoading?: boolean
|
||||||
|
} & JSX.ButtonHTMLAttributes<HTMLButtonElement>
|
||||||
|
|
||||||
|
export const Button = (props: Props) => {
|
||||||
|
const childrenReturn = children(() => props.children)
|
||||||
|
const [local, buttonProps] = splitProps(props, ['disabled', 'class'])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...buttonProps}
|
||||||
|
disabled={props.isDisabled || props.isLoading}
|
||||||
|
class={
|
||||||
|
'py-2 px-4 font-semibold focus:outline-none filter hover:brightness-90 active:brightness-75 disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100' +
|
||||||
|
(props.variant === 'secondary'
|
||||||
|
? ' secondary-button'
|
||||||
|
: ' typebot-button') +
|
||||||
|
' ' +
|
||||||
|
local.class
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Show when={!props.isLoading} fallback={<Spinner />}>
|
||||||
|
{childrenReturn()}
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
@ -28,13 +28,13 @@ export const AvatarSideContainer = (props: Props) => {
|
|||||||
<div
|
<div
|
||||||
ref={avatarContainer}
|
ref={avatarContainer}
|
||||||
class={
|
class={
|
||||||
'flex mr-2 mb-2 flex-shrink-0 items-center relative typebot-avatar-container ' +
|
'flex flex-shrink-0 items-center relative typebot-avatar-container ' +
|
||||||
(isMobile() ? 'w-6' : 'w-10')
|
(isMobile() ? 'w-6' : 'w-10')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class={
|
class={
|
||||||
'absolute mb-2 flex items-center top-0' +
|
'absolute flex items-center top-0' +
|
||||||
(isMobile() ? ' w-6 h-6' : ' w-10 h-10') +
|
(isMobile() ? ' w-6 h-6' : ' w-10 h-10') +
|
||||||
(props.hideAvatar ? ' opacity-0' : ' opacity-100')
|
(props.hideAvatar ? ' opacity-0' : ' opacity-100')
|
||||||
}
|
}
|
||||||
|
@ -46,13 +46,11 @@ export const ChatChunk = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex w-full">
|
<div class="flex flex-col w-full min-w-0 gap-2">
|
||||||
<div class="flex flex-col w-full min-w-0">
|
<div class={'flex' + (isMobile() ? ' gap-1' : ' gap-2')}>
|
||||||
<div class="flex">
|
|
||||||
<Show
|
<Show
|
||||||
when={
|
when={
|
||||||
props.theme.chat.hostAvatar?.isEnabled &&
|
props.theme.chat.hostAvatar?.isEnabled && props.messages.length > 0
|
||||||
props.messages.length > 0
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<AvatarSideContainer
|
<AvatarSideContainer
|
||||||
@ -61,7 +59,7 @@ export const ChatChunk = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<div
|
<div
|
||||||
class="flex-1"
|
class="flex flex-col flex-1 gap-2"
|
||||||
style={{
|
style={{
|
||||||
'margin-right': props.theme.chat.guestAvatar?.isEnabled
|
'margin-right': props.theme.chat.guestAvatar?.isEnabled
|
||||||
? isMobile()
|
? isMobile()
|
||||||
@ -97,6 +95,5 @@ export const ChatChunk = (props: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -184,7 +184,7 @@ export const ConversationContainer = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={chatContainer}
|
ref={chatContainer}
|
||||||
class="overflow-y-scroll w-full min-h-full px-3 pt-10 relative scrollable-container typebot-chat-view scroll-smooth"
|
class="flex flex-col overflow-y-scroll w-full min-h-full px-3 pt-10 relative scrollable-container typebot-chat-view scroll-smooth gap-2"
|
||||||
>
|
>
|
||||||
<For each={chatChunks()}>
|
<For each={chatChunks()}>
|
||||||
{(chatChunk, index) => (
|
{(chatChunk, index) => (
|
||||||
@ -228,5 +228,5 @@ type BottomSpacerProps = {
|
|||||||
ref: HTMLDivElement | undefined
|
ref: HTMLDivElement | undefined
|
||||||
}
|
}
|
||||||
const BottomSpacer = (props: BottomSpacerProps) => {
|
const BottomSpacer = (props: BottomSpacerProps) => {
|
||||||
return <div ref={props.ref} class="w-full h-32" />
|
return <div ref={props.ref} class="w-full h-32 flex-shrink-0" />
|
||||||
}
|
}
|
||||||
|
@ -9,17 +9,18 @@ export const PopupBlockedToast = (props: Props) => {
|
|||||||
class="w-full max-w-xs p-4 text-gray-500 bg-white shadow flex flex-col gap-2 typebot-popup-blocked-toast"
|
class="w-full max-w-xs p-4 text-gray-500 bg-white shadow flex flex-col gap-2 typebot-popup-blocked-toast"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<span class="mb-1 text-sm font-semibold text-gray-900">
|
<div class="flex flex-col gap-1">
|
||||||
Popup blocked
|
<span class=" text-sm font-semibold text-gray-900">Popup blocked</span>
|
||||||
</span>
|
<div class="text-sm font-normal">
|
||||||
<div class="mb-2 text-sm font-normal">
|
|
||||||
The bot wants to open a new tab but it was blocked by your broswer. It
|
The bot wants to open a new tab but it was blocked by your broswer. It
|
||||||
needs a manual approval.
|
needs a manual approval.
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={props.url}
|
href={props.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="py-1 px-4 justify-center text-sm font-semibold text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button"
|
class="py-1 px-4 justify-center text-sm font-semibold text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 filter hover:brightness-90 active:brightness-75 typebot-button"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
onClick={() => props.onLinkClick()}
|
onClick={() => props.onLinkClick()}
|
||||||
>
|
>
|
||||||
|
@ -22,13 +22,14 @@ import { EmailInput } from '@/features/blocks/inputs/email'
|
|||||||
import { UrlInput } from '@/features/blocks/inputs/url'
|
import { UrlInput } from '@/features/blocks/inputs/url'
|
||||||
import { PhoneInput } from '@/features/blocks/inputs/phone'
|
import { PhoneInput } from '@/features/blocks/inputs/phone'
|
||||||
import { DateForm } from '@/features/blocks/inputs/date'
|
import { DateForm } from '@/features/blocks/inputs/date'
|
||||||
import { ChoiceForm } from '@/features/blocks/inputs/buttons'
|
|
||||||
import { RatingForm } from '@/features/blocks/inputs/rating'
|
import { RatingForm } from '@/features/blocks/inputs/rating'
|
||||||
import { FileUploadForm } from '@/features/blocks/inputs/fileUpload'
|
import { FileUploadForm } from '@/features/blocks/inputs/fileUpload'
|
||||||
import { createSignal, Switch, Match } from 'solid-js'
|
import { createSignal, Switch, Match } from 'solid-js'
|
||||||
import { isNotDefined } from '@typebot.io/lib'
|
import { isNotDefined } from '@typebot.io/lib'
|
||||||
import { isMobile } from '@/utils/isMobileSignal'
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
import { PaymentForm } from '@/features/blocks/inputs/payment'
|
import { PaymentForm } from '@/features/blocks/inputs/payment'
|
||||||
|
import { MultipleChoicesForm } from '@/features/blocks/inputs/buttons/components/MultipleChoicesForm'
|
||||||
|
import { Buttons } from '@/features/blocks/inputs/buttons/components/Buttons'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: NonNullable<ChatReply['input']>
|
block: NonNullable<ChatReply['input']>
|
||||||
@ -66,13 +67,13 @@ export const InputChatBlock = (props: Props) => {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={isNotDefined(answer()) || props.hasError}>
|
<Match when={isNotDefined(answer()) || props.hasError}>
|
||||||
<div
|
<div
|
||||||
class="flex justify-end animate-fade-in"
|
class="flex justify-end animate-fade-in gap-2"
|
||||||
data-blockid={props.block.id}
|
data-blockid={props.block.id}
|
||||||
>
|
>
|
||||||
{props.hasHostAvatar && (
|
{props.hasHostAvatar && (
|
||||||
<div
|
<div
|
||||||
class={
|
class={
|
||||||
'flex mr-2 mb-2 mt-1 flex-shrink-0 items-center ' +
|
'flex flex-shrink-0 items-center ' +
|
||||||
(isMobile() ? 'w-6 h-6' : 'w-10 h-10')
|
(isMobile() ? 'w-6 h-6' : 'w-10 h-10')
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -158,10 +159,28 @@ const Input = (props: {
|
|||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.block.type === InputBlockType.CHOICE}>
|
<Match
|
||||||
<ChoiceForm
|
when={
|
||||||
|
props.block.type === InputBlockType.CHOICE &&
|
||||||
|
props.block.options.isMultipleChoice
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MultipleChoicesForm
|
||||||
inputIndex={props.inputIndex}
|
inputIndex={props.inputIndex}
|
||||||
block={props.block as ChoiceInputBlock}
|
items={(props.block as ChoiceInputBlock).items}
|
||||||
|
options={props.block.options as ChoiceInputBlock['options']}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match
|
||||||
|
when={
|
||||||
|
props.block.type === InputBlockType.CHOICE &&
|
||||||
|
!props.block.options.isMultipleChoice
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Buttons
|
||||||
|
inputIndex={props.inputIndex}
|
||||||
|
items={(props.block as ChoiceInputBlock).items}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { isMobile } from '@/utils/isMobileSignal'
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
import { Show } from 'solid-js'
|
import { splitProps } from 'solid-js'
|
||||||
import { JSX } from 'solid-js/jsx-runtime'
|
import { JSX } from 'solid-js/jsx-runtime'
|
||||||
import { SendIcon } from './icons'
|
import { SendIcon } from './icons'
|
||||||
|
import { Button } from './Button'
|
||||||
|
|
||||||
type SendButtonProps = {
|
type SendButtonProps = {
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
@ -10,50 +11,16 @@ type SendButtonProps = {
|
|||||||
} & JSX.ButtonHTMLAttributes<HTMLButtonElement>
|
} & JSX.ButtonHTMLAttributes<HTMLButtonElement>
|
||||||
|
|
||||||
export const SendButton = (props: SendButtonProps) => {
|
export const SendButton = (props: SendButtonProps) => {
|
||||||
|
const [local, others] = splitProps(props, ['disableIcon'])
|
||||||
return (
|
return (
|
||||||
<button
|
<Button type="submit" {...others}>
|
||||||
type="submit"
|
{isMobile() && !local.disableIcon ? (
|
||||||
disabled={props.isDisabled || props.isLoading}
|
|
||||||
{...props}
|
|
||||||
class={
|
|
||||||
'py-2 px-4 justify-center font-semibold text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button ' +
|
|
||||||
props.class
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Show when={!props.isLoading} fallback={<Spinner class="text-white" />}>
|
|
||||||
{isMobile() && !props.disableIcon ? (
|
|
||||||
<SendIcon
|
<SendIcon
|
||||||
class={'send-icon flex ' + (props.disableIcon ? 'hidden' : '')}
|
class={'send-icon flex ' + (local.disableIcon ? 'hidden' : '')}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
props.children
|
props.children
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Button>
|
||||||
</button>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Spinner = (props: JSX.SvgSVGAttributes<SVGSVGElement>) => (
|
|
||||||
<svg
|
|
||||||
{...props}
|
|
||||||
class={'animate-spin -ml-1 mr-3 h-5 w-5 ' + props.class}
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
data-testid="loading-spinner"
|
|
||||||
>
|
|
||||||
<circle
|
|
||||||
class="opacity-25"
|
|
||||||
cx="12"
|
|
||||||
cy="12"
|
|
||||||
r="10"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="4"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
class="opacity-75"
|
|
||||||
fill="currentColor"
|
|
||||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
26
packages/embeds/js/src/components/Spinner.tsx
Normal file
26
packages/embeds/js/src/components/Spinner.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { JSX } from 'solid-js'
|
||||||
|
|
||||||
|
export const Spinner = (props: JSX.SvgSVGAttributes<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
{...props}
|
||||||
|
class={'animate-spin h-5 w-5 ' + props.class}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
data-testid="loading-spinner"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
@ -1,7 +1,7 @@
|
|||||||
export const TypingBubble = () => (
|
export const TypingBubble = () => (
|
||||||
<div class="flex items-center">
|
<div class="flex items-center gap-1">
|
||||||
<div class="w-2 h-2 mr-1 rounded-full bubble1" />
|
<div class="w-2 h-2 rounded-full bubble1" />
|
||||||
<div class="w-2 h-2 mr-1 rounded-full bubble2" />
|
<div class="w-2 h-2 rounded-full bubble2" />
|
||||||
<div class="w-2 h-2 rounded-full bubble3" />
|
<div class="w-2 h-2 rounded-full bubble3" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -9,11 +9,11 @@ type Props = {
|
|||||||
|
|
||||||
export const GuestBubble = (props: Props) => (
|
export const GuestBubble = (props: Props) => (
|
||||||
<div
|
<div
|
||||||
class="flex justify-end mb-2 items-end animate-fade-in guest-container"
|
class="flex justify-end items-end animate-fade-in gap-2 guest-container"
|
||||||
style={{ 'margin-left': '50px' }}
|
style={{ 'margin-left': '50px' }}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="px-4 py-2 mr-2 whitespace-pre-wrap max-w-full typebot-guest-bubble"
|
class="px-4 py-2 whitespace-pre-wrap max-w-full typebot-guest-bubble"
|
||||||
data-testid="guest-bubble"
|
data-testid="guest-bubble"
|
||||||
>
|
>
|
||||||
{props.message}
|
{props.message}
|
||||||
|
@ -2,7 +2,7 @@ import { TypingBubble } from '@/components'
|
|||||||
|
|
||||||
export const LoadingBubble = () => (
|
export const LoadingBubble = () => (
|
||||||
<div class="flex flex-col animate-fade-in">
|
<div class="flex flex-col animate-fade-in">
|
||||||
<div class="flex mb-2 w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div class={'flex relative items-start typebot-host-bubble'}>
|
<div class={'flex relative items-start typebot-host-bubble'}>
|
||||||
<div
|
<div
|
||||||
class="flex items-center absolute px-4 py-2 bubble-typing "
|
class="flex items-center absolute px-4 py-2 bubble-typing "
|
||||||
|
16
packages/embeds/js/src/components/icons/CheckIcon.tsx
Normal file
16
packages/embeds/js/src/components/icons/CheckIcon.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { JSX } from 'solid-js/jsx-runtime'
|
||||||
|
|
||||||
|
export const CheckIcon = (props: JSX.SvgSVGAttributes<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="3px"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<polyline points="20 6 9 17 4 12" />
|
||||||
|
</svg>
|
||||||
|
)
|
@ -30,7 +30,7 @@ export const AudioBubble = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col animate-fade-in">
|
<div class="flex flex-col animate-fade-in">
|
||||||
<div class="flex mb-2 w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div class={'flex relative z-10 items-start typebot-host-bubble'}>
|
<div class={'flex relative z-10 items-start typebot-host-bubble'}>
|
||||||
<div
|
<div
|
||||||
class="flex items-center absolute px-4 py-2 bubble-typing z-10 "
|
class="flex items-center absolute px-4 py-2 bubble-typing z-10 "
|
||||||
|
@ -29,7 +29,7 @@ export const EmbedBubble = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col w-full animate-fade-in">
|
<div class="flex flex-col w-full animate-fade-in">
|
||||||
<div class="flex mb-2 w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div
|
<div
|
||||||
class={'flex relative z-10 items-start typebot-host-bubble w-full'}
|
class={'flex relative z-10 items-start typebot-host-bubble w-full'}
|
||||||
>
|
>
|
||||||
|
@ -55,7 +55,7 @@ export const ImageBubble = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col animate-fade-in">
|
<div class="flex flex-col animate-fade-in">
|
||||||
<div class="flex mb-2 w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div class={'flex relative z-10 items-start typebot-host-bubble'}>
|
<div class={'flex relative z-10 items-start typebot-host-bubble'}>
|
||||||
<div
|
<div
|
||||||
class="flex items-center absolute px-4 py-2 bubble-typing z-10 "
|
class="flex items-center absolute px-4 py-2 bubble-typing z-10 "
|
||||||
|
@ -51,7 +51,7 @@ export const TextBubble = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col animate-fade-in">
|
<div class="flex flex-col animate-fade-in">
|
||||||
<div class="flex mb-2 w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div class="flex relative items-start typebot-host-bubble">
|
<div class="flex relative items-start typebot-host-bubble">
|
||||||
<div
|
<div
|
||||||
class="flex items-center absolute px-4 py-2 bubble-typing "
|
class="flex items-center absolute px-4 py-2 bubble-typing "
|
||||||
|
@ -33,7 +33,7 @@ export const VideoBubble = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col animate-fade-in">
|
<div class="flex flex-col animate-fade-in">
|
||||||
<div class="flex mb-2 w-full items-center">
|
<div class="flex w-full items-center">
|
||||||
<div class={'flex relative z-10 items-start typebot-host-bubble'}>
|
<div class={'flex relative z-10 items-start typebot-host-bubble'}>
|
||||||
<div
|
<div
|
||||||
class="flex items-center absolute px-4 py-2 bubble-typing z-10 "
|
class="flex items-center absolute px-4 py-2 bubble-typing z-10 "
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
import { Button } from '@/components/Button'
|
||||||
|
import { InputSubmitContent } from '@/types'
|
||||||
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
|
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
||||||
|
import { For } from 'solid-js'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
inputIndex: number
|
||||||
|
items: ChoiceInputBlock['items']
|
||||||
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Buttons = (props: Props) => {
|
||||||
|
// eslint-disable-next-line solid/reactivity
|
||||||
|
const handleClick = (itemIndex: number) => () =>
|
||||||
|
props.onSubmit({ value: props.items[itemIndex].content ?? '' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="flex flex-wrap justify-end gap-2">
|
||||||
|
<For each={props.items}>
|
||||||
|
{(item, index) => (
|
||||||
|
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||||
|
<Button
|
||||||
|
on:click={handleClick(index())}
|
||||||
|
data-itemid={item.id}
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
{item.content}
|
||||||
|
</Button>
|
||||||
|
{props.inputIndex === 0 && props.items.length === 1 && (
|
||||||
|
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
||||||
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
||||||
|
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import { CheckIcon } from '@/components/icons/CheckIcon'
|
||||||
|
import { Show } from 'solid-js'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isChecked: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Checkbox = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={'w-4 h-4 typebot-checkbox' + (props.isChecked ? ' checked' : '')}
|
||||||
|
>
|
||||||
|
<Show when={props.isChecked}>
|
||||||
|
<CheckIcon />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,82 +0,0 @@
|
|||||||
import { SendButton } from '@/components/SendButton'
|
|
||||||
import { InputSubmitContent } from '@/types'
|
|
||||||
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
|
||||||
import { createSignal, For } from 'solid-js'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
inputIndex: number
|
|
||||||
block: ChoiceInputBlock
|
|
||||||
onSubmit: (value: InputSubmitContent) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChoiceForm = (props: Props) => {
|
|
||||||
const [selectedIndices, setSelectedIndices] = createSignal<number[]>([])
|
|
||||||
|
|
||||||
const handleClick = (itemIndex: number) => {
|
|
||||||
if (props.block.options?.isMultipleChoice)
|
|
||||||
toggleSelectedItemIndex(itemIndex)
|
|
||||||
else props.onSubmit({ value: props.block.items[itemIndex].content ?? '' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleSelectedItemIndex = (itemIndex: number) => {
|
|
||||||
const existingIndex = selectedIndices().indexOf(itemIndex)
|
|
||||||
if (existingIndex !== -1) {
|
|
||||||
setSelectedIndices((selectedIndices) =>
|
|
||||||
selectedIndices.filter((index) => index !== itemIndex)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
setSelectedIndices((selectedIndices) => [...selectedIndices, itemIndex])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = () =>
|
|
||||||
props.onSubmit({
|
|
||||||
value: selectedIndices()
|
|
||||||
.map((itemIndex) => props.block.items[itemIndex].content)
|
|
||||||
.join(', '),
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form class="flex flex-col items-end" onSubmit={handleSubmit}>
|
|
||||||
<div class="flex flex-wrap justify-end">
|
|
||||||
<For each={props.block.items}>
|
|
||||||
{(item, index) => (
|
|
||||||
<span class="relative inline-flex ml-2 mb-2">
|
|
||||||
<button
|
|
||||||
role={
|
|
||||||
props.block.options?.isMultipleChoice ? 'checkbox' : 'button'
|
|
||||||
}
|
|
||||||
type="button"
|
|
||||||
on:click={() => handleClick(index())}
|
|
||||||
class={
|
|
||||||
'py-2 px-4 text-left font-semibold transition-all filter hover:brightness-90 active:brightness-75 duration-100 focus:outline-none typebot-button ' +
|
|
||||||
(selectedIndices().some(
|
|
||||||
(selectedIndex) => selectedIndex === index()
|
|
||||||
) || !props.block.options?.isMultipleChoice
|
|
||||||
? ''
|
|
||||||
: 'selectable')
|
|
||||||
}
|
|
||||||
data-itemid={item.id}
|
|
||||||
>
|
|
||||||
{item.content}
|
|
||||||
</button>
|
|
||||||
{props.inputIndex === 0 && props.block.items.length === 1 && (
|
|
||||||
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
|
||||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
|
||||||
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
<div class="flex">
|
|
||||||
{selectedIndices().length > 0 && (
|
|
||||||
<SendButton disableIcon>
|
|
||||||
{props.block.options?.buttonLabel ?? 'Send'}
|
|
||||||
</SendButton>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
@ -0,0 +1,84 @@
|
|||||||
|
import { SendButton } from '@/components/SendButton'
|
||||||
|
import { InputSubmitContent } from '@/types'
|
||||||
|
import { isMobile } from '@/utils/isMobileSignal'
|
||||||
|
import type { ChoiceInputBlock } from '@typebot.io/schemas'
|
||||||
|
import { createSignal, For } from 'solid-js'
|
||||||
|
import { Checkbox } from './Checkbox'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
inputIndex: number
|
||||||
|
items: ChoiceInputBlock['items']
|
||||||
|
options: ChoiceInputBlock['options']
|
||||||
|
onSubmit: (value: InputSubmitContent) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultipleChoicesForm = (props: Props) => {
|
||||||
|
const [selectedIndices, setSelectedIndices] = createSignal<number[]>([])
|
||||||
|
|
||||||
|
const handleClick = (itemIndex: number) => {
|
||||||
|
toggleSelectedItemIndex(itemIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelectedItemIndex = (itemIndex: number) => {
|
||||||
|
const existingIndex = selectedIndices().indexOf(itemIndex)
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
setSelectedIndices((selectedIndices) =>
|
||||||
|
selectedIndices.filter((index) => index !== itemIndex)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setSelectedIndices((selectedIndices) => [...selectedIndices, itemIndex])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = () =>
|
||||||
|
props.onSubmit({
|
||||||
|
value: selectedIndices()
|
||||||
|
.map((itemIndex) => props.items[itemIndex].content)
|
||||||
|
.join(', '),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form class="flex flex-col items-end gap-2" onSubmit={handleSubmit}>
|
||||||
|
<div class="flex flex-wrap justify-end gap-2">
|
||||||
|
<For each={props.items}>
|
||||||
|
{(item, index) => (
|
||||||
|
<span class={'relative' + (isMobile() ? ' w-full' : '')}>
|
||||||
|
<div
|
||||||
|
role="checkbox"
|
||||||
|
aria-checked={selectedIndices().some(
|
||||||
|
(selectedIndex) => selectedIndex === index()
|
||||||
|
)}
|
||||||
|
on:click={() => handleClick(index())}
|
||||||
|
class={
|
||||||
|
'w-full py-2 px-4 font-semibold focus:outline-none cursor-pointer select-none typebot-selectable' +
|
||||||
|
(selectedIndices().some(
|
||||||
|
(selectedIndex) => selectedIndex === index()
|
||||||
|
)
|
||||||
|
? ' selected'
|
||||||
|
: '')
|
||||||
|
}
|
||||||
|
data-itemid={item.id}
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
isChecked={selectedIndices().some(
|
||||||
|
(selectedIndex) => selectedIndex === index()
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span>{item.content}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
{selectedIndices().length > 0 && (
|
||||||
|
<SendButton disableIcon>
|
||||||
|
{props.options?.buttonLabel ?? 'Send'}
|
||||||
|
</SendButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
@ -1 +0,0 @@
|
|||||||
export { ChoiceForm } from './components/ChoiceForm'
|
|
@ -36,11 +36,11 @@ export const DateForm = (props: Props) => {
|
|||||||
<div
|
<div
|
||||||
class={
|
class={
|
||||||
'flex items-center p-4 ' +
|
'flex items-center p-4 ' +
|
||||||
(props.options?.isRange ? 'pb-0' : '')
|
(props.options?.isRange ? 'pb-0 gap-2' : '')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{props.options?.isRange && (
|
{props.options?.isRange && (
|
||||||
<p class="font-semibold mr-2">
|
<p class="font-semibold">
|
||||||
{props.options.labels?.from ?? 'From:'}
|
{props.options.labels?.from ?? 'From:'}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { SendButton, Spinner } from '@/components/SendButton'
|
import { SendButton } from '@/components/SendButton'
|
||||||
import { BotContext, InputSubmitContent } from '@/types'
|
import { BotContext, InputSubmitContent } from '@/types'
|
||||||
import { guessApiHost } from '@/utils/guessApiHost'
|
import { guessApiHost } from '@/utils/guessApiHost'
|
||||||
import { FileInputBlock } from '@typebot.io/schemas'
|
import { FileInputBlock } from '@typebot.io/schemas'
|
||||||
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file'
|
import { defaultFileInputOptions } from '@typebot.io/schemas/features/blocks/inputs/file'
|
||||||
import { createSignal, Match, Show, Switch } from 'solid-js'
|
import { createSignal, Match, Show, Switch } from 'solid-js'
|
||||||
import { uploadFiles } from '@typebot.io/lib'
|
import { uploadFiles } from '@typebot.io/lib'
|
||||||
|
import { Button } from '@/components/Button'
|
||||||
|
import { Spinner } from '@/components/Spinner'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
context: BotContext
|
context: BotContext
|
||||||
@ -111,12 +113,17 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
|
|
||||||
const clearFiles = () => setSelectedFiles([])
|
const clearFiles = () => setSelectedFiles([])
|
||||||
|
|
||||||
|
const skip = () =>
|
||||||
|
props.onSkip(
|
||||||
|
props.block.options.labels.skip ?? defaultFileInputOptions.labels.skip
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form class="flex flex-col w-full" onSubmit={handleSubmit}>
|
<form class="flex flex-col w-full gap-2" onSubmit={handleSubmit}>
|
||||||
<label
|
<label
|
||||||
for="dropzone-file"
|
for="dropzone-file"
|
||||||
class={
|
class={
|
||||||
'typebot-upload-input py-6 flex flex-col justify-center items-center w-full bg-gray-50 border-2 border-gray-300 border-dashed cursor-pointer hover:bg-gray-100 px-8 mb-2 ' +
|
'typebot-upload-input py-6 flex flex-col justify-center items-center w-full bg-gray-50 border-2 border-gray-300 border-dashed cursor-pointer hover:bg-gray-100 px-8 ' +
|
||||||
(isDraggingOver() ? 'dragging-over' : '')
|
(isDraggingOver() ? 'dragging-over' : '')
|
||||||
}
|
}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
@ -179,20 +186,10 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<button
|
<Button on:click={skip}>
|
||||||
class={
|
|
||||||
'py-2 px-4 justify-center font-semibold text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 typebot-button '
|
|
||||||
}
|
|
||||||
on:click={() =>
|
|
||||||
props.onSkip(
|
|
||||||
props.block.options.labels.skip ??
|
|
||||||
defaultFileInputOptions.labels.skip
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.block.options.labels.skip ??
|
{props.block.options.labels.skip ??
|
||||||
defaultFileInputOptions.labels.skip}
|
defaultFileInputOptions.labels.skip}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show
|
<Show
|
||||||
@ -203,17 +200,12 @@ export const FileUploadForm = (props: Props) => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<div class="flex">
|
<div class="flex gap-2">
|
||||||
<Show when={selectedFiles().length}>
|
<Show when={selectedFiles().length}>
|
||||||
<button
|
<Button variant="secondary" on:click={clearFiles}>
|
||||||
class={
|
|
||||||
'secondary-button py-2 px-4 justify-center font-semibold text-white focus:outline-none flex items-center disabled:opacity-50 disabled:cursor-not-allowed disabled:brightness-100 transition-all filter hover:brightness-90 active:brightness-75 mr-2'
|
|
||||||
}
|
|
||||||
on:click={clearFiles}
|
|
||||||
>
|
|
||||||
{props.block.options.labels.clear ??
|
{props.block.options.labels.clear ??
|
||||||
defaultFileInputOptions.labels.clear}
|
defaultFileInputOptions.labels.clear}
|
||||||
</button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
<SendButton type="submit" disableIcon>
|
<SendButton type="submit" disableIcon>
|
||||||
{props.block.options.labels.button ===
|
{props.block.options.labels.button ===
|
||||||
|
@ -3,6 +3,7 @@ import { InputSubmitContent } from '@/types'
|
|||||||
import type { RatingInputBlock, RatingInputOptions } from '@typebot.io/schemas'
|
import type { RatingInputBlock, RatingInputOptions } from '@typebot.io/schemas'
|
||||||
import { createSignal, For, Match, Switch } from 'solid-js'
|
import { createSignal, For, Match, Switch } from 'solid-js'
|
||||||
import { isDefined, isEmpty, isNotDefined } from '@typebot.io/lib'
|
import { isDefined, isEmpty, isNotDefined } from '@typebot.io/lib'
|
||||||
|
import { Button } from '@/components/Button'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: RatingInputBlock
|
block: RatingInputBlock
|
||||||
@ -29,13 +30,13 @@ export const RatingForm = (props: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form class="flex flex-col" onSubmit={handleSubmit}>
|
<form class="flex flex-col gap-2" onSubmit={handleSubmit}>
|
||||||
{props.block.options.labels.left && (
|
{props.block.options.labels.left && (
|
||||||
<span class="text-sm w-full mb-2 rating-label">
|
<span class="text-sm w-full rating-label">
|
||||||
{props.block.options.labels.left}
|
{props.block.options.labels.left}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div class="flex flex-wrap justify-center">
|
<div class="flex flex-wrap justify-center gap-2">
|
||||||
<For
|
<For
|
||||||
each={Array.from(
|
each={Array.from(
|
||||||
Array(
|
Array(
|
||||||
@ -57,12 +58,12 @@ export const RatingForm = (props: Props) => {
|
|||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
{props.block.options.labels.right && (
|
{props.block.options.labels.right && (
|
||||||
<span class="text-sm w-full text-right mb-2 pr-2 rating-label">
|
<span class="text-sm w-full text-right pr-2 rating-label">
|
||||||
{props.block.options.labels.right}
|
{props.block.options.labels.right}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div class="flex justify-end mr-2">
|
<div class="flex justify-end">
|
||||||
{isDefined(rating()) && (
|
{isDefined(rating()) && (
|
||||||
<SendButton disableIcon>
|
<SendButton disableIcon>
|
||||||
{props.block.options?.labels?.button ?? 'Send'}
|
{props.block.options?.labels?.button ?? 'Send'}
|
||||||
@ -80,29 +81,29 @@ type RatingButtonProps = {
|
|||||||
} & RatingInputOptions
|
} & RatingInputOptions
|
||||||
|
|
||||||
const RatingButton = (props: RatingButtonProps) => {
|
const RatingButton = (props: RatingButtonProps) => {
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
props.onClick(props.idx)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={props.buttonType === 'Numbers'}>
|
<Match when={props.buttonType === 'Numbers'}>
|
||||||
<button
|
<Button
|
||||||
on:click={(e) => {
|
on:click={handleClick}
|
||||||
e.preventDefault()
|
|
||||||
props.onClick(props.idx)
|
|
||||||
}}
|
|
||||||
class={
|
class={
|
||||||
'py-2 px-4 mr-2 mb-2 text-left font-semibold transition-all filter hover:brightness-90 active:brightness-75 duration-100 focus:outline-none typebot-button ' +
|
props.isOneClickSubmitEnabled ||
|
||||||
(props.isOneClickSubmitEnabled ||
|
|
||||||
(isDefined(props.rating) && props.idx <= props.rating)
|
(isDefined(props.rating) && props.idx <= props.rating)
|
||||||
? ''
|
? ''
|
||||||
: 'selectable')
|
: 'selectable'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{props.idx}
|
{props.idx}
|
||||||
</button>
|
</Button>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={props.buttonType !== 'Numbers'}>
|
<Match when={props.buttonType !== 'Numbers'}>
|
||||||
<div
|
<div
|
||||||
class={
|
class={
|
||||||
'flex justify-center items-center rating-icon-container cursor-pointer mr-2 mb-2 ' +
|
'flex justify-center items-center rating-icon-container cursor-pointer ' +
|
||||||
(isDefined(props.rating) && props.idx <= props.rating
|
(isDefined(props.rating) && props.idx <= props.rating
|
||||||
? 'selected'
|
? 'selected'
|
||||||
: '')
|
: '')
|
||||||
|
20
packages/embeds/js/src/utils/hexToRgb.ts
Normal file
20
packages/embeds/js/src/utils/hexToRgb.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
export const hexToRgb = (hex: string): [r: number, g: number, b: number] => {
|
||||||
|
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
|
||||||
|
hex = hex.replace(shorthandRegex, (_m, r, g, b) => {
|
||||||
|
return r + r + g + g + b + b
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||||
|
return result
|
||||||
|
? [
|
||||||
|
parseInt(result[1], 16),
|
||||||
|
parseInt(result[2], 16),
|
||||||
|
parseInt(result[3], 16),
|
||||||
|
]
|
||||||
|
: [0, 0, 0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isLight = (hexColor: string) =>
|
||||||
|
(([r, g, b]) => (r * 299 + g * 587 + b * 114) / 1000 > 155)(
|
||||||
|
hexToRgb(hexColor)
|
||||||
|
)
|
@ -7,12 +7,15 @@ 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 } from './hexToRgb'
|
||||||
|
|
||||||
const cssVariableNames = {
|
const cssVariableNames = {
|
||||||
general: {
|
general: {
|
||||||
bgImage: '--typebot-container-bg-image',
|
bgImage: '--typebot-container-bg-image',
|
||||||
bgColor: '--typebot-container-bg-color',
|
bgColor: '--typebot-container-bg-color',
|
||||||
fontFamily: '--typebot-container-font-family',
|
fontFamily: '--typebot-container-font-family',
|
||||||
|
color: '--typebot-container-color',
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
hostBubbles: {
|
hostBubbles: {
|
||||||
@ -30,10 +33,14 @@ const cssVariableNames = {
|
|||||||
},
|
},
|
||||||
buttons: {
|
buttons: {
|
||||||
bgColor: '--typebot-button-bg-color',
|
bgColor: '--typebot-button-bg-color',
|
||||||
|
bgColorRgb: '--typebot-button-bg-color-rgb',
|
||||||
color: '--typebot-button-color',
|
color: '--typebot-button-color',
|
||||||
},
|
},
|
||||||
|
checkbox: {
|
||||||
|
bgColor: '--typebot-checkbox-bg-color',
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
export const setCssVariablesValue = (
|
export const setCssVariablesValue = (
|
||||||
theme: Theme | undefined,
|
theme: Theme | undefined,
|
||||||
@ -103,11 +110,17 @@ const setButtons = (
|
|||||||
buttons: ContainerColors,
|
buttons: ContainerColors,
|
||||||
documentStyle: CSSStyleDeclaration
|
documentStyle: CSSStyleDeclaration
|
||||||
) => {
|
) => {
|
||||||
if (buttons.backgroundColor)
|
if (buttons.backgroundColor) {
|
||||||
documentStyle.setProperty(
|
documentStyle.setProperty(
|
||||||
cssVariableNames.chat.buttons.bgColor,
|
cssVariableNames.chat.buttons.bgColor,
|
||||||
buttons.backgroundColor
|
buttons.backgroundColor
|
||||||
)
|
)
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.chat.buttons.bgColorRgb,
|
||||||
|
hexToRgb(buttons.backgroundColor).join(', ')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (buttons.color)
|
if (buttons.color)
|
||||||
documentStyle.setProperty(
|
documentStyle.setProperty(
|
||||||
cssVariableNames.chat.buttons.color,
|
cssVariableNames.chat.buttons.color,
|
||||||
@ -142,6 +155,16 @@ const setTypebotBackground = (
|
|||||||
: cssVariableNames.general.bgColor,
|
: cssVariableNames.general.bgColor,
|
||||||
parseBackgroundValue(background)
|
parseBackgroundValue(background)
|
||||||
)
|
)
|
||||||
|
const backgroundColor =
|
||||||
|
(BackgroundType.COLOR ? background.content : '#ffffff') ?? '#ffffff'
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.chat.checkbox.bgColor,
|
||||||
|
(BackgroundType.COLOR ? background.content : '#ffffff') ?? '#ffffff'
|
||||||
|
)
|
||||||
|
documentStyle.setProperty(
|
||||||
|
cssVariableNames.general.color,
|
||||||
|
isLight(backgroundColor) ? '#303235' : '#ffffff'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseBackgroundValue = ({ type, content }: Background) => {
|
const parseBackgroundValue = ({ type, content }: Background) => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/react",
|
"name": "@typebot.io/react",
|
||||||
"version": "0.0.39",
|
"version": "0.0.40",
|
||||||
"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