✨ (theme) Add theme templates
Allows you to save your themes and select a theme from Typebot's gallery Closes #275
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
ButtonProps,
|
ButtonProps,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import React, { ChangeEvent, useEffect, useState } from 'react'
|
import React, { ChangeEvent, useState } from 'react'
|
||||||
import tinyColor from 'tinycolor2'
|
import tinyColor from 'tinycolor2'
|
||||||
|
|
||||||
const colorsSelection: `#${string}`[] = [
|
const colorsSelection: `#${string}`[] = [
|
||||||
@@ -29,31 +29,37 @@ const colorsSelection: `#${string}`[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialColor?: string
|
value?: string
|
||||||
|
defaultValue?: string
|
||||||
onColorChange: (color: string) => void
|
onColorChange: (color: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
|
export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
|
||||||
const [color, setColor] = useState(initialColor ?? '')
|
const [color, setColor] = useState(defaultValue ?? '')
|
||||||
|
const displayedValue = value ?? color
|
||||||
|
|
||||||
useEffect(() => {
|
const handleColorChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
onColorChange(color)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [color])
|
|
||||||
|
|
||||||
const handleColorChange = (e: ChangeEvent<HTMLInputElement>) =>
|
|
||||||
setColor(e.target.value)
|
setColor(e.target.value)
|
||||||
|
onColorChange(e.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
const handleClick = (color: string) => () => setColor(color)
|
const handleClick = (color: string) => () => {
|
||||||
|
setColor(color)
|
||||||
|
onColorChange(color)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover variant="picker" placement="right" isLazy>
|
<Popover variant="picker" placement="right" isLazy>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button
|
<Button
|
||||||
aria-label={'Pick a color'}
|
aria-label={'Pick a color'}
|
||||||
bgColor={color}
|
bgColor={displayedValue}
|
||||||
_hover={{ bgColor: `#${tinyColor(color).darken(10).toHex()}` }}
|
_hover={{
|
||||||
_active={{ bgColor: `#${tinyColor(color).darken(30).toHex()}` }}
|
bgColor: `#${tinyColor(displayedValue).darken(10).toHex()}`,
|
||||||
|
}}
|
||||||
|
_active={{
|
||||||
|
bgColor: `#${tinyColor(displayedValue).darken(30).toHex()}`,
|
||||||
|
}}
|
||||||
height="22px"
|
height="22px"
|
||||||
width="22px"
|
width="22px"
|
||||||
padding={0}
|
padding={0}
|
||||||
@@ -62,16 +68,16 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent width="170px">
|
<PopoverContent width="170px">
|
||||||
<PopoverArrow bg={color} />
|
<PopoverArrow bg={displayedValue} />
|
||||||
<PopoverCloseButton color="white" />
|
<PopoverCloseButton color="white" />
|
||||||
<PopoverHeader
|
<PopoverHeader
|
||||||
height="100px"
|
height="100px"
|
||||||
backgroundColor={color}
|
backgroundColor={displayedValue}
|
||||||
borderTopLeftRadius={5}
|
borderTopLeftRadius={5}
|
||||||
borderTopRightRadius={5}
|
borderTopRightRadius={5}
|
||||||
color={tinyColor(color).isLight() ? 'gray.800' : 'white'}
|
color={tinyColor(displayedValue).isLight() ? 'gray.800' : 'white'}
|
||||||
>
|
>
|
||||||
<Center height="100%">{color}</Center>
|
<Center height="100%">{displayedValue}</Center>
|
||||||
</PopoverHeader>
|
</PopoverHeader>
|
||||||
<PopoverBody as={Stack}>
|
<PopoverBody as={Stack}>
|
||||||
<SimpleGrid columns={5} spacing={2}>
|
<SimpleGrid columns={5} spacing={2}>
|
||||||
@@ -96,12 +102,12 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
|
|||||||
placeholder="#2a9d8f"
|
placeholder="#2a9d8f"
|
||||||
aria-label="Color value"
|
aria-label="Color value"
|
||||||
size="sm"
|
size="sm"
|
||||||
value={color}
|
value={displayedValue}
|
||||||
onChange={handleColorChange}
|
onChange={handleColorChange}
|
||||||
/>
|
/>
|
||||||
<NativeColorPicker
|
<NativeColorPicker
|
||||||
size="sm"
|
size="sm"
|
||||||
color={color}
|
color={displayedValue}
|
||||||
onColorChange={handleColorChange}
|
onColorChange={handleColorChange}
|
||||||
>
|
>
|
||||||
Advanced picker
|
Advanced picker
|
||||||
|
|||||||
@@ -202,15 +202,6 @@ export const CodeIcon = (props: IconProps) => (
|
|||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
export const PencilIcon = (props: IconProps) => (
|
|
||||||
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
|
||||||
<path d="M12 19l7-7 3 3-7 7-3-3z"></path>
|
|
||||||
<path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path>
|
|
||||||
<path d="M2 2l7.586 7.586"></path>
|
|
||||||
<circle cx="11" cy="11" r="2"></circle>
|
|
||||||
</Icon>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const EditIcon = (props: IconProps) => (
|
export const EditIcon = (props: IconProps) => (
|
||||||
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
|
<path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
|
||||||
@@ -591,3 +582,15 @@ export const LargeRadiusIcon = (props: IconProps) => (
|
|||||||
/>
|
/>
|
||||||
</Icon>
|
</Icon>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const DropletIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"></path>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const TableIcon = (props: IconProps) => (
|
||||||
|
<Icon viewBox="0 0 24 24" {...featherIconsBaseProps} {...props}>
|
||||||
|
<path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"></path>
|
||||||
|
</Icon>
|
||||||
|
)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
items: string[]
|
items: string[]
|
||||||
|
value?: string
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
debounceTimeout?: number
|
debounceTimeout?: number
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
@@ -40,6 +41,7 @@ export const AutocompleteInput = ({
|
|||||||
debounceTimeout,
|
debounceTimeout,
|
||||||
placeholder,
|
placeholder,
|
||||||
withVariableButton = true,
|
withVariableButton = true,
|
||||||
|
value,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
label,
|
label,
|
||||||
moreInfoTooltip,
|
moreInfoTooltip,
|
||||||
@@ -178,7 +180,7 @@ export const AutocompleteInput = ({
|
|||||||
<Input
|
<Input
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
value={inputValue}
|
value={value ?? inputValue}
|
||||||
onChange={(e) => changeValue(e.target.value)}
|
onChange={(e) => changeValue(e.target.value)}
|
||||||
onFocus={onOpen}
|
onFocus={onOpen}
|
||||||
onBlur={updateCarretPosition}
|
onBlur={updateCarretPosition}
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ export const CodeEditor = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = (newValue: string) => {
|
const handleChange = (newValue: string) => {
|
||||||
if (isDefined(props.value)) return
|
|
||||||
setValue(newValue)
|
setValue(newValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,15 +11,18 @@ import { ReactNode } from 'react'
|
|||||||
|
|
||||||
type Props<T extends string> = {
|
type Props<T extends string> = {
|
||||||
options: (T | { value: T; label: ReactNode })[]
|
options: (T | { value: T; label: ReactNode })[]
|
||||||
defaultValue: T
|
value?: T
|
||||||
|
defaultValue?: T
|
||||||
onSelect: (newValue: T) => void
|
onSelect: (newValue: T) => void
|
||||||
}
|
}
|
||||||
export const RadioButtons = <T extends string>({
|
export const RadioButtons = <T extends string>({
|
||||||
options,
|
options,
|
||||||
|
value,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
onSelect,
|
onSelect,
|
||||||
}: Props<T>) => {
|
}: Props<T>) => {
|
||||||
const { getRootProps, getRadioProps } = useRadioGroup({
|
const { getRootProps, getRadioProps } = useRadioGroup({
|
||||||
|
value,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
onChange: onSelect,
|
onChange: onSelect,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
|||||||
|
|
||||||
export type TextInputProps = {
|
export type TextInputProps = {
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
onChange: (value: string) => void
|
onChange?: (value: string) => void
|
||||||
debounceTimeout?: number
|
debounceTimeout?: number
|
||||||
label?: ReactNode
|
label?: ReactNode
|
||||||
helperText?: ReactNode
|
helperText?: ReactNode
|
||||||
@@ -66,7 +66,8 @@ export const TextInput = forwardRef(function TextInput(
|
|||||||
localValue.length ?? 0
|
localValue.length ?? 0
|
||||||
)
|
)
|
||||||
const onChange = useDebouncedCallback(
|
const onChange = useDebouncedCallback(
|
||||||
_onChange,
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
_onChange ?? (() => {}),
|
||||||
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
|
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export const parseNewTypebot = ({
|
|||||||
groups: [startGroup],
|
groups: [startGroup],
|
||||||
edges: [],
|
edges: [],
|
||||||
variables: [],
|
variables: [],
|
||||||
|
selectedThemeTemplateId: null,
|
||||||
theme: {
|
theme: {
|
||||||
...defaultTheme,
|
...defaultTheme,
|
||||||
chat: {
|
chat: {
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
LogicBlockType,
|
LogicBlockType,
|
||||||
PublicTypebot,
|
PublicTypebot,
|
||||||
ResultsTablePreferences,
|
|
||||||
Settings,
|
|
||||||
Theme,
|
|
||||||
Typebot,
|
Typebot,
|
||||||
Webhook,
|
Webhook,
|
||||||
} from '@typebot.io/schemas'
|
} from '@typebot.io/schemas'
|
||||||
@@ -45,16 +42,20 @@ import { convertPublicTypebotToTypebot } from '@/features/publish/helpers/conver
|
|||||||
|
|
||||||
const autoSaveTimeout = 10000
|
const autoSaveTimeout = 10000
|
||||||
|
|
||||||
type UpdateTypebotPayload = Partial<{
|
type UpdateTypebotPayload = Partial<
|
||||||
theme: Theme
|
Pick<
|
||||||
settings: Settings
|
Typebot,
|
||||||
publicId: string
|
| 'theme'
|
||||||
name: string
|
| 'selectedThemeTemplateId'
|
||||||
icon: string
|
| 'settings'
|
||||||
customDomain: string | null
|
| 'publicId'
|
||||||
resultsTablePreferences: ResultsTablePreferences
|
| 'name'
|
||||||
isClosed: boolean
|
| 'icon'
|
||||||
}>
|
| 'customDomain'
|
||||||
|
| 'resultsTablePreferences'
|
||||||
|
| 'isClosed'
|
||||||
|
>
|
||||||
|
>
|
||||||
|
|
||||||
export type SetTypebot = (
|
export type SetTypebot = (
|
||||||
newPresent: Typebot | ((current: Typebot) => Typebot)
|
newPresent: Typebot | ((current: Typebot) => Typebot)
|
||||||
|
|||||||
@@ -37,14 +37,14 @@ export const ButtonThemeSettings = ({ buttonTheme, onChange }: Props) => {
|
|||||||
<HStack justify="space-between">
|
<HStack justify="space-between">
|
||||||
<Text>Background color</Text>
|
<Text>Background color</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={buttonTheme?.backgroundColor}
|
defaultValue={buttonTheme?.backgroundColor}
|
||||||
onColorChange={updateBackgroundColor}
|
onColorChange={updateBackgroundColor}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack justify="space-between">
|
<HStack justify="space-between">
|
||||||
<Text>Icon color</Text>
|
<Text>Icon color</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={buttonTheme?.iconColor}
|
defaultValue={buttonTheme?.iconColor}
|
||||||
onColorChange={updateIconColor}
|
onColorChange={updateIconColor}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@@ -49,28 +49,28 @@ export const PreviewMessageThemeSettings = ({
|
|||||||
<HStack justify="space-between">
|
<HStack justify="space-between">
|
||||||
<Text>Background color</Text>
|
<Text>Background color</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={previewMessageTheme?.backgroundColor}
|
defaultValue={previewMessageTheme?.backgroundColor}
|
||||||
onColorChange={updateBackgroundColor}
|
onColorChange={updateBackgroundColor}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack justify="space-between">
|
<HStack justify="space-between">
|
||||||
<Text>Text color</Text>
|
<Text>Text color</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={previewMessageTheme?.textColor}
|
defaultValue={previewMessageTheme?.textColor}
|
||||||
onColorChange={updateTextColor}
|
onColorChange={updateTextColor}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack justify="space-between">
|
<HStack justify="space-between">
|
||||||
<Text>Close button background</Text>
|
<Text>Close button background</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={previewMessageTheme?.closeButtonBackgroundColor}
|
defaultValue={previewMessageTheme?.closeButtonBackgroundColor}
|
||||||
onColorChange={updateCloseButtonBackgroundColor}
|
onColorChange={updateCloseButtonBackgroundColor}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack justify="space-between">
|
<HStack justify="space-between">
|
||||||
<Text>Close icon color</Text>
|
<Text>Close icon color</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={previewMessageTheme?.closeButtonIconColor}
|
defaultValue={previewMessageTheme?.closeButtonIconColor}
|
||||||
onColorChange={updateCloseButtonIconColor}
|
onColorChange={updateCloseButtonIconColor}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ export const convertPublicTypebotToTypebot = (
|
|||||||
isArchived: existingTypebot.isArchived,
|
isArchived: existingTypebot.isArchived,
|
||||||
isClosed: existingTypebot.isClosed,
|
isClosed: existingTypebot.isClosed,
|
||||||
resultsTablePreferences: existingTypebot.resultsTablePreferences,
|
resultsTablePreferences: existingTypebot.resultsTablePreferences,
|
||||||
|
selectedThemeTemplateId: existingTypebot.selectedThemeTemplateId,
|
||||||
})
|
})
|
||||||
|
|||||||
55
apps/builder/src/features/theme/api/deleteThemeTemplate.ts
Normal file
55
apps/builder/src/features/theme/api/deleteThemeTemplate.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { ThemeTemplate, themeTemplateSchema } from '@typebot.io/schemas'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
|
||||||
|
import { WorkspaceRole } from '@typebot.io/prisma'
|
||||||
|
|
||||||
|
export const deleteThemeTemplate = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'DELETE',
|
||||||
|
path: '/themeTemplates/{themeTemplateId}',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Delete a theme template',
|
||||||
|
tags: ['Workspace', 'Theme'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
themeTemplateId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
themeTemplate: themeTemplateSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(
|
||||||
|
async ({ input: { themeTemplateId, workspaceId }, ctx: { user } }) => {
|
||||||
|
const workspace = await prisma.workspace.findUnique({
|
||||||
|
where: { id: workspaceId },
|
||||||
|
select: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const userRole = getUserRoleInWorkspace(user.id, workspace?.members)
|
||||||
|
if (userRole === undefined || userRole === WorkspaceRole.GUEST)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'Workspace not found',
|
||||||
|
})
|
||||||
|
|
||||||
|
const themeTemplate = (await prisma.themeTemplate.delete({
|
||||||
|
where: {
|
||||||
|
id: themeTemplateId,
|
||||||
|
},
|
||||||
|
})) as ThemeTemplate
|
||||||
|
|
||||||
|
return {
|
||||||
|
themeTemplate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
56
apps/builder/src/features/theme/api/listThemeTemplates.ts
Normal file
56
apps/builder/src/features/theme/api/listThemeTemplates.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { ThemeTemplate, themeTemplateSchema } from '@typebot.io/schemas'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
|
||||||
|
import { WorkspaceRole } from '@typebot.io/prisma'
|
||||||
|
|
||||||
|
export const listThemeTemplates = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'GET',
|
||||||
|
path: '/themeTemplates',
|
||||||
|
protect: true,
|
||||||
|
summary: 'List theme templates',
|
||||||
|
tags: ['Workspace', 'Theme'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(z.object({ workspaceId: z.string() }))
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
themeTemplates: z.array(
|
||||||
|
themeTemplateSchema.pick({
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
theme: true,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input: { workspaceId }, ctx: { user } }) => {
|
||||||
|
const workspace = await prisma.workspace.findUnique({
|
||||||
|
where: { id: workspaceId },
|
||||||
|
select: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const userRole = getUserRoleInWorkspace(user.id, workspace?.members)
|
||||||
|
if (userRole === undefined || userRole === WorkspaceRole.GUEST)
|
||||||
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Workspace not found' })
|
||||||
|
const themeTemplates = (await prisma.themeTemplate.findMany({
|
||||||
|
where: {
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
theme: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
})) as Pick<ThemeTemplate, 'id' | 'name' | 'theme'>[]
|
||||||
|
|
||||||
|
return { themeTemplates }
|
||||||
|
})
|
||||||
10
apps/builder/src/features/theme/api/router.ts
Normal file
10
apps/builder/src/features/theme/api/router.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { router } from '@/helpers/server/trpc'
|
||||||
|
import { deleteThemeTemplate } from './deleteThemeTemplate'
|
||||||
|
import { listThemeTemplates } from './listThemeTemplates'
|
||||||
|
import { saveThemeTemplate } from './saveThemeTemplate'
|
||||||
|
|
||||||
|
export const themeRouter = router({
|
||||||
|
listThemeTemplates,
|
||||||
|
saveThemeTemplate,
|
||||||
|
deleteThemeTemplate,
|
||||||
|
})
|
||||||
63
apps/builder/src/features/theme/api/saveThemeTemplate.ts
Normal file
63
apps/builder/src/features/theme/api/saveThemeTemplate.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import prisma from '@/lib/prisma'
|
||||||
|
import { authenticatedProcedure } from '@/helpers/server/trpc'
|
||||||
|
import { TRPCError } from '@trpc/server'
|
||||||
|
import { ThemeTemplate, themeTemplateSchema } from '@typebot.io/schemas'
|
||||||
|
import { z } from 'zod'
|
||||||
|
import { getUserRoleInWorkspace } from '@/features/workspace/helpers/getUserRoleInWorkspace'
|
||||||
|
import { WorkspaceRole } from '@typebot.io/prisma'
|
||||||
|
|
||||||
|
export const saveThemeTemplate = authenticatedProcedure
|
||||||
|
.meta({
|
||||||
|
openapi: {
|
||||||
|
method: 'PUT',
|
||||||
|
path: '/themeTemplates/{themeTemplateId}',
|
||||||
|
protect: true,
|
||||||
|
summary: 'Save theme template',
|
||||||
|
tags: ['Workspace', 'Theme'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
workspaceId: z.string(),
|
||||||
|
themeTemplateId: z.string(),
|
||||||
|
name: themeTemplateSchema.shape.name,
|
||||||
|
theme: themeTemplateSchema.shape.theme,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
themeTemplate: themeTemplateSchema,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(
|
||||||
|
async ({
|
||||||
|
input: { themeTemplateId, workspaceId, ...data },
|
||||||
|
ctx: { user },
|
||||||
|
}) => {
|
||||||
|
const workspace = await prisma.workspace.findUnique({
|
||||||
|
where: { id: workspaceId },
|
||||||
|
select: {
|
||||||
|
members: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const userRole = getUserRoleInWorkspace(user.id, workspace?.members)
|
||||||
|
if (userRole === undefined || userRole === WorkspaceRole.GUEST)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: 'Workspace not found',
|
||||||
|
})
|
||||||
|
|
||||||
|
const themeTemplate = (await prisma.themeTemplate.upsert({
|
||||||
|
where: { id: themeTemplateId },
|
||||||
|
create: {
|
||||||
|
...data,
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
update: data,
|
||||||
|
})) as ThemeTemplate
|
||||||
|
|
||||||
|
return {
|
||||||
|
themeTemplate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -9,7 +9,7 @@ type Props = {
|
|||||||
export const CustomCssSettings = ({ customCss, onCustomCssChange }: Props) => {
|
export const CustomCssSettings = ({ customCss, onCustomCssChange }: Props) => {
|
||||||
return (
|
return (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
defaultValue={customCss ?? ''}
|
value={customCss ?? ''}
|
||||||
lang="css"
|
lang="css"
|
||||||
onChange={onCustomCssChange}
|
onChange={onCustomCssChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
69
apps/builder/src/features/theme/components/MyTemplates.tsx
Normal file
69
apps/builder/src/features/theme/components/MyTemplates.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { SaveIcon } from '@/components/icons'
|
||||||
|
import { trpc } from '@/lib/trpc'
|
||||||
|
import { Button, SimpleGrid, Stack, useDisclosure } from '@chakra-ui/react'
|
||||||
|
import { ThemeTemplate } from '@typebot.io/schemas'
|
||||||
|
import { areThemesEqual } from '../helpers/areThemesEqual'
|
||||||
|
import { SaveThemeModal } from './SaveThemeModal'
|
||||||
|
import { ThemeTemplateCard } from './ThemeTemplateCard'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedTemplateId: string | undefined
|
||||||
|
currentTheme: ThemeTemplate['theme']
|
||||||
|
workspaceId: string
|
||||||
|
onTemplateSelect: (
|
||||||
|
template: Partial<Pick<ThemeTemplate, 'id' | 'theme'>>
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MyTemplates = ({
|
||||||
|
selectedTemplateId,
|
||||||
|
currentTheme,
|
||||||
|
workspaceId,
|
||||||
|
onTemplateSelect,
|
||||||
|
}: Props) => {
|
||||||
|
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||||
|
const { data } = trpc.theme.listThemeTemplates.useQuery({
|
||||||
|
workspaceId,
|
||||||
|
})
|
||||||
|
const selectedTemplate = data?.themeTemplates.find(
|
||||||
|
(themeTemplate) => themeTemplate.id === selectedTemplateId
|
||||||
|
)
|
||||||
|
|
||||||
|
const closeModalAndSelectTemplate = (
|
||||||
|
template?: Pick<ThemeTemplate, 'id' | 'theme'>
|
||||||
|
) => {
|
||||||
|
if (template) onTemplateSelect(template)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
{(!selectedTemplate ||
|
||||||
|
!areThemesEqual(selectedTemplate?.theme, currentTheme)) && (
|
||||||
|
<Button leftIcon={<SaveIcon />} onClick={onOpen} colorScheme="blue">
|
||||||
|
Save current theme
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<SaveThemeModal
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
selectedTemplate={selectedTemplate}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={closeModalAndSelectTemplate}
|
||||||
|
theme={currentTheme}
|
||||||
|
/>
|
||||||
|
<SimpleGrid columns={2} spacing={4}>
|
||||||
|
{data?.themeTemplates.map((themeTemplate) => (
|
||||||
|
<ThemeTemplateCard
|
||||||
|
key={themeTemplate.id}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
themeTemplate={themeTemplate}
|
||||||
|
isSelected={themeTemplate.id === selectedTemplateId}
|
||||||
|
onClick={() => onTemplateSelect(themeTemplate)}
|
||||||
|
onRenameClick={onOpen}
|
||||||
|
onDeleteSuccess={() => onTemplateSelect({ id: '' })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
109
apps/builder/src/features/theme/components/SaveThemeModal.tsx
Normal file
109
apps/builder/src/features/theme/components/SaveThemeModal.tsx
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { TextInput } from '@/components/inputs'
|
||||||
|
import { useToast } from '@/hooks/useToast'
|
||||||
|
import { trpc } from '@/lib/trpc'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
HStack,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalFooter,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { createId } from '@paralleldrive/cuid2'
|
||||||
|
import { ThemeTemplate } from '@typebot.io/schemas'
|
||||||
|
import { FormEvent, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
workspaceId: string
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: (template?: Pick<ThemeTemplate, 'id' | 'theme'>) => void
|
||||||
|
selectedTemplate: Pick<ThemeTemplate, 'id' | 'name'> | undefined
|
||||||
|
theme: ThemeTemplate['theme']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SaveThemeModal = ({
|
||||||
|
workspaceId,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
selectedTemplate,
|
||||||
|
theme,
|
||||||
|
}: Props) => {
|
||||||
|
const { showToast } = useToast()
|
||||||
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const {
|
||||||
|
theme: {
|
||||||
|
listThemeTemplates: { refetch: refetchThemeTemplates },
|
||||||
|
},
|
||||||
|
} = trpc.useContext()
|
||||||
|
const { mutate } = trpc.theme.saveThemeTemplate.useMutation({
|
||||||
|
onMutate: () => setIsSaving(true),
|
||||||
|
onSettled: () => setIsSaving(false),
|
||||||
|
onSuccess: ({ themeTemplate }) => {
|
||||||
|
refetchThemeTemplates()
|
||||||
|
onClose(themeTemplate)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast({
|
||||||
|
description: error.message,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateExistingTemplate = (e: FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const newName = inputRef.current?.value
|
||||||
|
if (!newName) return
|
||||||
|
mutate({
|
||||||
|
name: newName,
|
||||||
|
theme,
|
||||||
|
workspaceId,
|
||||||
|
themeTemplateId: selectedTemplate?.id ?? createId(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveNewTemplate = () => {
|
||||||
|
const newName = inputRef.current?.value
|
||||||
|
if (!newName) return
|
||||||
|
mutate({
|
||||||
|
name: newName,
|
||||||
|
theme,
|
||||||
|
workspaceId,
|
||||||
|
themeTemplateId: createId(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} initialFocusRef={inputRef}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent as="form" onSubmit={updateExistingTemplate}>
|
||||||
|
<ModalHeader>Save theme</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody>
|
||||||
|
<TextInput
|
||||||
|
ref={inputRef}
|
||||||
|
label="Name:"
|
||||||
|
defaultValue={selectedTemplate?.name}
|
||||||
|
withVariableButton={false}
|
||||||
|
placeholder="My template"
|
||||||
|
isRequired
|
||||||
|
/>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter as={HStack}>
|
||||||
|
{selectedTemplate?.id && (
|
||||||
|
<Button isLoading={isSaving} onClick={saveNewTemplate}>
|
||||||
|
Save as new template
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button type="submit" colorScheme="blue" isLoading={isSaving}>
|
||||||
|
{selectedTemplate?.id ? 'Update' : 'Save'}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { SimpleGrid, Stack } from '@chakra-ui/react'
|
||||||
|
import { ThemeTemplate } from '@typebot.io/schemas'
|
||||||
|
import { galleryTemplates } from '../galleryTemplates'
|
||||||
|
import { ThemeTemplateCard } from './ThemeTemplateCard'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedTemplateId: string | undefined
|
||||||
|
currentTheme: ThemeTemplate['theme']
|
||||||
|
workspaceId: string
|
||||||
|
onTemplateSelect: (
|
||||||
|
template: Partial<Pick<ThemeTemplate, 'id' | 'theme'>>
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TemplatesGallery = ({
|
||||||
|
selectedTemplateId,
|
||||||
|
workspaceId,
|
||||||
|
onTemplateSelect,
|
||||||
|
}: Props) => (
|
||||||
|
<Stack spacing={4}>
|
||||||
|
<SimpleGrid columns={2} spacing={4}>
|
||||||
|
{galleryTemplates.map((themeTemplate) => (
|
||||||
|
<ThemeTemplateCard
|
||||||
|
key={themeTemplate.id}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
themeTemplate={themeTemplate}
|
||||||
|
isSelected={themeTemplate.id === selectedTemplateId}
|
||||||
|
onClick={() => onTemplateSelect(themeTemplate)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
@@ -7,28 +7,41 @@ import {
|
|||||||
Heading,
|
Heading,
|
||||||
HStack,
|
HStack,
|
||||||
Stack,
|
Stack,
|
||||||
|
Tag,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { ChatIcon, CodeIcon, PencilIcon } from '@/components/icons'
|
import { ChatIcon, CodeIcon, DropletIcon, TableIcon } from '@/components/icons'
|
||||||
import { ChatTheme, GeneralTheme } from '@typebot.io/schemas'
|
import { ChatTheme, GeneralTheme, ThemeTemplate } from '@typebot.io/schemas'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { CustomCssSettings } from './CustomCssSettings'
|
import { CustomCssSettings } from './CustomCssSettings'
|
||||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||||
import { headerHeight } from '@/features/editor/constants'
|
import { headerHeight } from '@/features/editor/constants'
|
||||||
import { ChatThemeSettings } from './chat/ChatThemeSettings'
|
import { ChatThemeSettings } from './chat/ChatThemeSettings'
|
||||||
import { GeneralSettings } from './general/GeneralSettings'
|
import { GeneralSettings } from './general/GeneralSettings'
|
||||||
|
import { ThemeTemplates } from './ThemeTemplates'
|
||||||
|
|
||||||
export const ThemeSideMenu = () => {
|
export const ThemeSideMenu = () => {
|
||||||
const { typebot, updateTypebot } = useTypebot()
|
const { typebot, updateTypebot } = useTypebot()
|
||||||
|
|
||||||
const handleChatThemeChange = (chat: ChatTheme) =>
|
const updateChatTheme = (chat: ChatTheme) =>
|
||||||
typebot && updateTypebot({ theme: { ...typebot.theme, chat } })
|
typebot && updateTypebot({ theme: { ...typebot.theme, chat } })
|
||||||
|
|
||||||
const handleGeneralThemeChange = (general: GeneralTheme) =>
|
const updateGeneralTheme = (general: GeneralTheme) =>
|
||||||
typebot && updateTypebot({ theme: { ...typebot.theme, general } })
|
typebot && updateTypebot({ theme: { ...typebot.theme, general } })
|
||||||
|
|
||||||
const handleCustomCssChange = (customCss: string) =>
|
const updateCustomCss = (customCss: string) =>
|
||||||
typebot && updateTypebot({ theme: { ...typebot.theme, customCss } })
|
typebot && updateTypebot({ theme: { ...typebot.theme, customCss } })
|
||||||
|
|
||||||
|
const selectedTemplate = (
|
||||||
|
selectedTemplate: Partial<Pick<ThemeTemplate, 'id' | 'theme'>>
|
||||||
|
) => {
|
||||||
|
if (!typebot) return
|
||||||
|
const { theme, id } = selectedTemplate
|
||||||
|
updateTypebot({
|
||||||
|
selectedThemeTemplateId: id,
|
||||||
|
theme: theme ? { ...theme } : typebot.theme,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
flex="1"
|
flex="1"
|
||||||
@@ -48,8 +61,33 @@ export const ThemeSideMenu = () => {
|
|||||||
<AccordionItem>
|
<AccordionItem>
|
||||||
<AccordionButton py={6}>
|
<AccordionButton py={6}>
|
||||||
<HStack flex="1" pl={2}>
|
<HStack flex="1" pl={2}>
|
||||||
<PencilIcon />
|
<TableIcon />
|
||||||
<Heading fontSize="lg">General</Heading>
|
<Heading fontSize="lg">
|
||||||
|
<HStack>
|
||||||
|
<span>Templates</span> <Tag colorScheme="orange">New!</Tag>
|
||||||
|
</HStack>
|
||||||
|
</Heading>
|
||||||
|
</HStack>
|
||||||
|
<AccordionIcon />
|
||||||
|
</AccordionButton>
|
||||||
|
<AccordionPanel pb={4}>
|
||||||
|
{typebot && (
|
||||||
|
<ThemeTemplates
|
||||||
|
selectedTemplateId={
|
||||||
|
typebot.selectedThemeTemplateId ?? undefined
|
||||||
|
}
|
||||||
|
currentTheme={typebot.theme}
|
||||||
|
workspaceId={typebot.workspaceId}
|
||||||
|
onTemplateSelect={selectedTemplate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem>
|
||||||
|
<AccordionButton py={6}>
|
||||||
|
<HStack flex="1" pl={2}>
|
||||||
|
<DropletIcon />
|
||||||
|
<Heading fontSize="lg">Font & Background</Heading>
|
||||||
</HStack>
|
</HStack>
|
||||||
<AccordionIcon />
|
<AccordionIcon />
|
||||||
</AccordionButton>
|
</AccordionButton>
|
||||||
@@ -57,7 +95,7 @@ export const ThemeSideMenu = () => {
|
|||||||
{typebot && (
|
{typebot && (
|
||||||
<GeneralSettings
|
<GeneralSettings
|
||||||
generalTheme={typebot.theme.general}
|
generalTheme={typebot.theme.general}
|
||||||
onGeneralThemeChange={handleGeneralThemeChange}
|
onGeneralThemeChange={updateGeneralTheme}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
@@ -75,7 +113,7 @@ export const ThemeSideMenu = () => {
|
|||||||
<ChatThemeSettings
|
<ChatThemeSettings
|
||||||
typebotId={typebot.id}
|
typebotId={typebot.id}
|
||||||
chatTheme={typebot.theme.chat}
|
chatTheme={typebot.theme.chat}
|
||||||
onChatThemeChange={handleChatThemeChange}
|
onChatThemeChange={updateChatTheme}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
@@ -92,7 +130,7 @@ export const ThemeSideMenu = () => {
|
|||||||
{typebot && (
|
{typebot && (
|
||||||
<CustomCssSettings
|
<CustomCssSettings
|
||||||
customCss={typebot.theme.customCss}
|
customCss={typebot.theme.customCss}
|
||||||
onCustomCssChange={handleCustomCssChange}
|
onCustomCssChange={updateCustomCss}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
|
|||||||
223
apps/builder/src/features/theme/components/ThemeTemplateCard.tsx
Normal file
223
apps/builder/src/features/theme/components/ThemeTemplateCard.tsx
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
import { MoreHorizontalIcon, EditIcon, TrashIcon } from '@/components/icons'
|
||||||
|
import { colors } from '@/lib/theme'
|
||||||
|
import { trpc } from '@/lib/trpc'
|
||||||
|
import {
|
||||||
|
Stack,
|
||||||
|
HStack,
|
||||||
|
Flex,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
IconButton,
|
||||||
|
MenuList,
|
||||||
|
MenuItem,
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
Image,
|
||||||
|
} from '@chakra-ui/react'
|
||||||
|
import { BackgroundType, ThemeTemplate } from '@typebot.io/schemas'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { DefaultAvatar } from './DefaultAvatar'
|
||||||
|
|
||||||
|
export const ThemeTemplateCard = ({
|
||||||
|
workspaceId,
|
||||||
|
themeTemplate,
|
||||||
|
isSelected,
|
||||||
|
onClick,
|
||||||
|
onRenameClick,
|
||||||
|
onDeleteSuccess,
|
||||||
|
}: {
|
||||||
|
workspaceId: string
|
||||||
|
themeTemplate: Pick<ThemeTemplate, 'name' | 'theme' | 'id'>
|
||||||
|
isSelected: boolean
|
||||||
|
onRenameClick?: () => void
|
||||||
|
onClick: () => void
|
||||||
|
onDeleteSuccess?: () => void
|
||||||
|
}) => {
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false)
|
||||||
|
|
||||||
|
const {
|
||||||
|
theme: {
|
||||||
|
listThemeTemplates: { refetch: refetchThemeTemplates },
|
||||||
|
},
|
||||||
|
} = trpc.useContext()
|
||||||
|
const { mutate } = trpc.theme.deleteThemeTemplate.useMutation({
|
||||||
|
onMutate: () => setIsDeleting(true),
|
||||||
|
onSettled: () => setIsDeleting(false),
|
||||||
|
onSuccess: () => {
|
||||||
|
refetchThemeTemplates()
|
||||||
|
if (onDeleteSuccess) onDeleteSuccess()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteThemeTemplate = () => {
|
||||||
|
mutate({ themeTemplateId: themeTemplate.id, workspaceId })
|
||||||
|
}
|
||||||
|
|
||||||
|
const rounded =
|
||||||
|
themeTemplate.theme.chat.roundness === 'large'
|
||||||
|
? 'md'
|
||||||
|
: themeTemplate.theme.chat.roundness === 'none'
|
||||||
|
? 'none'
|
||||||
|
: 'sm'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={onClick}
|
||||||
|
spacing={0}
|
||||||
|
opacity={isDeleting ? 0.5 : 1}
|
||||||
|
pointerEvents={isDeleting ? 'none' : undefined}
|
||||||
|
rounded="md"
|
||||||
|
boxShadow={
|
||||||
|
isSelected
|
||||||
|
? `${colors['blue']['400']} 0 0 0 4px`
|
||||||
|
: `rgba(0, 0, 0, 0.08) 0px 2px 4px`
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
willChange: 'box-shadow',
|
||||||
|
transition: 'box-shadow 0.2s ease 0s',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
borderTopRadius="md"
|
||||||
|
backgroundSize="cover"
|
||||||
|
{...parseBackground(themeTemplate.theme.general.background)}
|
||||||
|
borderColor={isSelected ? 'blue.400' : undefined}
|
||||||
|
>
|
||||||
|
<HStack mt="4" ml="4" spacing={0.5} alignItems="flex-end">
|
||||||
|
<AvatarPreview avatar={themeTemplate.theme.chat.hostAvatar} />
|
||||||
|
<Box
|
||||||
|
rounded="sm"
|
||||||
|
w="80px"
|
||||||
|
h="16px"
|
||||||
|
background={themeTemplate.theme.chat.hostBubbles.backgroundColor}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack
|
||||||
|
mt="1"
|
||||||
|
mr="4"
|
||||||
|
ml="auto"
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems="flex-end"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
rounded="sm"
|
||||||
|
w="80px"
|
||||||
|
h="16px"
|
||||||
|
background={themeTemplate.theme.chat.guestBubbles.backgroundColor}
|
||||||
|
/>
|
||||||
|
<AvatarPreview avatar={themeTemplate.theme.chat.guestAvatar} />
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack mt="1" ml="4" spacing={0.5} alignItems="flex-end">
|
||||||
|
<AvatarPreview avatar={themeTemplate.theme.chat.hostAvatar} />
|
||||||
|
<Box
|
||||||
|
rounded="sm"
|
||||||
|
w="80px"
|
||||||
|
h="16px"
|
||||||
|
background={themeTemplate.theme.chat.hostBubbles.backgroundColor}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
<Flex
|
||||||
|
mt="1"
|
||||||
|
mb="4"
|
||||||
|
pr="4"
|
||||||
|
ml="auto"
|
||||||
|
w="full"
|
||||||
|
justifyContent="flex-end"
|
||||||
|
gap="1"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
rounded={rounded}
|
||||||
|
w="20px"
|
||||||
|
h="10px"
|
||||||
|
background={themeTemplate.theme.chat.buttons.backgroundColor}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
rounded={rounded}
|
||||||
|
w="20px"
|
||||||
|
h="10px"
|
||||||
|
background={themeTemplate.theme.chat.buttons.backgroundColor}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
rounded={rounded}
|
||||||
|
w="20px"
|
||||||
|
h="10px"
|
||||||
|
background={themeTemplate.theme.chat.buttons.backgroundColor}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
<HStack p="2" justifyContent="space-between">
|
||||||
|
<Text fontSize="sm" noOfLines={1}>
|
||||||
|
{themeTemplate.name}
|
||||||
|
</Text>
|
||||||
|
{onDeleteSuccess && onRenameClick && (
|
||||||
|
<Menu isLazy>
|
||||||
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
icon={<MoreHorizontalIcon />}
|
||||||
|
aria-label="Open template menu"
|
||||||
|
variant="ghost"
|
||||||
|
size="xs"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<MenuList onClick={(e) => e.stopPropagation()}>
|
||||||
|
{isSelected && (
|
||||||
|
<MenuItem icon={<EditIcon />} onClick={onRenameClick}>
|
||||||
|
Rename
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem
|
||||||
|
icon={<TrashIcon />}
|
||||||
|
color="red.500"
|
||||||
|
onClick={deleteThemeTemplate}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseBackground = (background: {
|
||||||
|
type: BackgroundType
|
||||||
|
content?: string
|
||||||
|
}) => {
|
||||||
|
switch (background.type) {
|
||||||
|
case BackgroundType.COLOR:
|
||||||
|
return {
|
||||||
|
backgroundColor: background.content,
|
||||||
|
}
|
||||||
|
case BackgroundType.IMAGE:
|
||||||
|
return { backgroundImage: `url(${background.content})` }
|
||||||
|
case BackgroundType.NONE:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const AvatarPreview = ({
|
||||||
|
avatar,
|
||||||
|
}: {
|
||||||
|
avatar:
|
||||||
|
| {
|
||||||
|
isEnabled: boolean
|
||||||
|
url?: string | undefined
|
||||||
|
}
|
||||||
|
| undefined
|
||||||
|
}) => {
|
||||||
|
if (!avatar?.isEnabled) return null
|
||||||
|
return avatar.url ? (
|
||||||
|
<Image
|
||||||
|
src={avatar.url}
|
||||||
|
alt="Avatar preview in theme template card"
|
||||||
|
boxSize="12px"
|
||||||
|
rounded="full"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<DefaultAvatar boxSize="12px" />
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Button, HStack, Stack } from '@chakra-ui/react'
|
||||||
|
import { ThemeTemplate } from '@typebot.io/schemas'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { MyTemplates } from './MyTemplates'
|
||||||
|
import { TemplatesGallery } from './TemplatesGallery'
|
||||||
|
|
||||||
|
type Tab = 'my-templates' | 'gallery'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
workspaceId: string
|
||||||
|
selectedTemplateId: string | undefined
|
||||||
|
currentTheme: ThemeTemplate['theme']
|
||||||
|
onTemplateSelect: (
|
||||||
|
template: Partial<Pick<ThemeTemplate, 'id' | 'theme'>>
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThemeTemplates = ({
|
||||||
|
workspaceId,
|
||||||
|
selectedTemplateId,
|
||||||
|
currentTheme,
|
||||||
|
onTemplateSelect,
|
||||||
|
}: Props) => {
|
||||||
|
const [selectedTab, setSelectedTab] = useState<Tab>('my-templates')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack spacing={4} pb={12}>
|
||||||
|
<HStack>
|
||||||
|
<Button
|
||||||
|
flex="1"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme={selectedTab === 'my-templates' ? 'blue' : 'gray'}
|
||||||
|
onClick={() => setSelectedTab('my-templates')}
|
||||||
|
>
|
||||||
|
My templates
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
flex="1"
|
||||||
|
variant="outline"
|
||||||
|
colorScheme={selectedTab === 'gallery' ? 'blue' : 'gray'}
|
||||||
|
onClick={() => setSelectedTab('gallery')}
|
||||||
|
>
|
||||||
|
Gallery
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
<ThemeTemplatesBody
|
||||||
|
tab={selectedTab}
|
||||||
|
currentTheme={currentTheme}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
selectedTemplateId={selectedTemplateId}
|
||||||
|
onTemplateSelect={onTemplateSelect}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeTemplatesBody = ({
|
||||||
|
tab,
|
||||||
|
workspaceId,
|
||||||
|
selectedTemplateId,
|
||||||
|
currentTheme,
|
||||||
|
onTemplateSelect,
|
||||||
|
}: {
|
||||||
|
tab: Tab
|
||||||
|
} & Props) => {
|
||||||
|
switch (tab) {
|
||||||
|
case 'my-templates':
|
||||||
|
return (
|
||||||
|
<MyTemplates
|
||||||
|
onTemplateSelect={onTemplateSelect}
|
||||||
|
currentTheme={currentTheme}
|
||||||
|
selectedTemplateId={selectedTemplateId}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
case 'gallery':
|
||||||
|
return (
|
||||||
|
<TemplatesGallery
|
||||||
|
onTemplateSelect={onTemplateSelect}
|
||||||
|
currentTheme={currentTheme}
|
||||||
|
selectedTemplateId={selectedTemplateId}
|
||||||
|
workspaceId={workspaceId}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,16 +19,13 @@ export const ButtonsTheme = ({ buttons, onButtonsChange }: Props) => {
|
|||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<Text>Background:</Text>
|
<Text>Background:</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={buttons.backgroundColor}
|
value={buttons.backgroundColor}
|
||||||
onColorChange={handleBackgroundChange}
|
onColorChange={handleBackgroundChange}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<Text>Text:</Text>
|
<Text>Text:</Text>
|
||||||
<ColorPicker
|
<ColorPicker value={buttons.color} onColorChange={handleTextChange} />
|
||||||
initialColor={buttons.color}
|
|
||||||
onColorChange={handleTextChange}
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -65,29 +65,7 @@ export const ChatThemeSettings = ({
|
|||||||
onHostBubblesChange={handleHostBubblesChange}
|
onHostBubblesChange={handleHostBubblesChange}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
|
||||||
<Heading fontSize="lg">Corners roundness</Heading>
|
|
||||||
<RadioButtons
|
|
||||||
options={[
|
|
||||||
{
|
|
||||||
label: <NoRadiusIcon />,
|
|
||||||
value: 'none',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: <MediumRadiusIcon />,
|
|
||||||
value: 'medium',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: <LargeRadiusIcon />,
|
|
||||||
value: 'large',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
defaultValue={chatTheme.roundness ?? 'medium'}
|
|
||||||
onSelect={(roundness) =>
|
|
||||||
onChatThemeChange({ ...chatTheme, roundness })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
||||||
<Heading fontSize="lg">User bubbles</Heading>
|
<Heading fontSize="lg">User bubbles</Heading>
|
||||||
<GuestBubbles
|
<GuestBubbles
|
||||||
@@ -109,6 +87,29 @@ export const ChatThemeSettings = ({
|
|||||||
onInputsChange={handleInputsChange}
|
onInputsChange={handleInputsChange}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Stack borderWidth={1} rounded="md" p="4" spacing={4}>
|
||||||
|
<Heading fontSize="lg">Corners roundness</Heading>
|
||||||
|
<RadioButtons
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: <NoRadiusIcon />,
|
||||||
|
value: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <MediumRadiusIcon />,
|
||||||
|
value: 'medium',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <LargeRadiusIcon />,
|
||||||
|
value: 'large',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
value={chatTheme.roundness ?? 'medium'}
|
||||||
|
onSelect={(roundness) =>
|
||||||
|
onChatThemeChange({ ...chatTheme, roundness })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
|
|||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<Text>Background:</Text>
|
<Text>Background:</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={guestBubbles.backgroundColor}
|
value={guestBubbles.backgroundColor}
|
||||||
onColorChange={handleBackgroundChange}
|
onColorChange={handleBackgroundChange}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<Text>Text:</Text>
|
<Text>Text:</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={guestBubbles.color}
|
value={guestBubbles.color}
|
||||||
onColorChange={handleTextChange}
|
onColorChange={handleTextChange}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
|
|||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<Text>Background:</Text>
|
<Text>Background:</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={hostBubbles.backgroundColor}
|
value={hostBubbles.backgroundColor}
|
||||||
onColorChange={handleBackgroundChange}
|
onColorChange={handleBackgroundChange}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<Text>Text:</Text>
|
<Text>Text:</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={hostBubbles.color}
|
value={hostBubbles.color}
|
||||||
onColorChange={handleTextChange}
|
onColorChange={handleTextChange}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -21,21 +21,18 @@ export const InputsTheme = ({ inputs, onInputsChange }: Props) => {
|
|||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<Text>Background:</Text>
|
<Text>Background:</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={inputs.backgroundColor}
|
value={inputs.backgroundColor}
|
||||||
onColorChange={handleBackgroundChange}
|
onColorChange={handleBackgroundChange}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<Text>Text:</Text>
|
<Text>Text:</Text>
|
||||||
<ColorPicker
|
<ColorPicker value={inputs.color} onColorChange={handleTextChange} />
|
||||||
initialColor={inputs.color}
|
|
||||||
onColorChange={handleTextChange}
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<Text>Placeholder text:</Text>
|
<Text>Placeholder text:</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={inputs.placeholderColor}
|
value={inputs.placeholderColor}
|
||||||
onColorChange={handlePlaceholderChange}
|
onColorChange={handlePlaceholderChange}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const BackgroundContent = ({
|
|||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<Text>Background color:</Text>
|
<Text>Background color:</Text>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
initialColor={background.content ?? defaultBackgroundColor}
|
value={background.content ?? defaultBackgroundColor}
|
||||||
onColorChange={handleContentChange}
|
onColorChange={handleContentChange}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const BackgroundSelector = ({
|
|||||||
BackgroundType.IMAGE,
|
BackgroundType.IMAGE,
|
||||||
BackgroundType.NONE,
|
BackgroundType.NONE,
|
||||||
]}
|
]}
|
||||||
defaultValue={background?.type ?? defaultBackgroundType}
|
value={background?.type ?? defaultBackgroundType}
|
||||||
onSelect={handleBackgroundTypeChange}
|
onSelect={handleBackgroundTypeChange}
|
||||||
/>
|
/>
|
||||||
<BackgroundContent
|
<BackgroundContent
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export const FontSelector = ({
|
|||||||
<HStack justify="space-between" align="center">
|
<HStack justify="space-between" align="center">
|
||||||
<Text>Font</Text>
|
<Text>Font</Text>
|
||||||
<AutocompleteInput
|
<AutocompleteInput
|
||||||
defaultValue={activeFont}
|
value={activeFont}
|
||||||
items={googleFonts}
|
items={googleFonts}
|
||||||
onChange={handleFontSelected}
|
onChange={handleFontSelected}
|
||||||
withVariableButton={false}
|
withVariableButton={false}
|
||||||
|
|||||||
144
apps/builder/src/features/theme/galleryTemplates.ts
Normal file
144
apps/builder/src/features/theme/galleryTemplates.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { BackgroundType, ThemeTemplate } from '@typebot.io/schemas'
|
||||||
|
|
||||||
|
export const galleryTemplates: Pick<ThemeTemplate, 'id' | 'name' | 'theme'>[] =
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'typebot-light',
|
||||||
|
name: 'Typebot Light',
|
||||||
|
theme: {
|
||||||
|
chat: {
|
||||||
|
inputs: {
|
||||||
|
color: '#303235',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
placeholderColor: '#9095A0',
|
||||||
|
},
|
||||||
|
buttons: { color: '#FFFFFF', backgroundColor: '#0042DA' },
|
||||||
|
hostAvatar: {
|
||||||
|
isEnabled: true,
|
||||||
|
},
|
||||||
|
hostBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
|
||||||
|
guestBubbles: { color: '#FFFFFF', backgroundColor: '#FF8E21' },
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
font: 'Open Sans',
|
||||||
|
background: { type: BackgroundType.COLOR, content: '#ffffff' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'typebot-dark',
|
||||||
|
name: 'Typebot Dark',
|
||||||
|
theme: {
|
||||||
|
chat: {
|
||||||
|
inputs: {
|
||||||
|
color: '#ffffff',
|
||||||
|
backgroundColor: '#1e293b',
|
||||||
|
placeholderColor: '#9095A0',
|
||||||
|
},
|
||||||
|
buttons: { color: '#ffffff', backgroundColor: '#1a5fff' },
|
||||||
|
hostAvatar: {
|
||||||
|
isEnabled: true,
|
||||||
|
},
|
||||||
|
hostBubbles: { color: '#ffffff', backgroundColor: '#1e293b' },
|
||||||
|
guestBubbles: { color: '#FFFFFF', backgroundColor: '#FF8E21' },
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
font: 'Open Sans',
|
||||||
|
background: { type: BackgroundType.COLOR, content: '#171923' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'minimalist-black',
|
||||||
|
name: 'Minimalist Black',
|
||||||
|
theme: {
|
||||||
|
chat: {
|
||||||
|
inputs: {
|
||||||
|
color: '#303235',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
placeholderColor: '#9095A0',
|
||||||
|
},
|
||||||
|
buttons: { color: '#FFFFFF', backgroundColor: '#303235' },
|
||||||
|
hostAvatar: { isEnabled: false },
|
||||||
|
hostBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
|
||||||
|
guestBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
font: 'Inter',
|
||||||
|
background: { type: BackgroundType.COLOR, content: '#ffffff' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'minimalist-teal',
|
||||||
|
name: 'Minimalist Teal',
|
||||||
|
theme: {
|
||||||
|
chat: {
|
||||||
|
inputs: {
|
||||||
|
color: '#303235',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
placeholderColor: '#9095A0',
|
||||||
|
},
|
||||||
|
buttons: { color: '#FFFFFF', backgroundColor: '#0d9488' },
|
||||||
|
hostAvatar: { isEnabled: false },
|
||||||
|
hostBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
|
||||||
|
guestBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
font: 'Inter',
|
||||||
|
background: { type: BackgroundType.COLOR, content: '#ffffff' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'bright-rain',
|
||||||
|
name: 'Bright Rain',
|
||||||
|
theme: {
|
||||||
|
chat: {
|
||||||
|
inputs: {
|
||||||
|
color: '#303235',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
placeholderColor: '#9095A0',
|
||||||
|
},
|
||||||
|
buttons: { color: '#fff', backgroundColor: '#D27A7D' },
|
||||||
|
hostAvatar: { isEnabled: true },
|
||||||
|
hostBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
|
||||||
|
guestBubbles: { color: '#303235', backgroundColor: '#FDDDBF' },
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
font: 'Montserrat',
|
||||||
|
background: {
|
||||||
|
type: BackgroundType.IMAGE,
|
||||||
|
content:
|
||||||
|
'https://s3.fr-par.scw.cloud/typebot/public/typebots/hlmywyje0sbz1lfogu86pyks/blocks/ssmyt084oosa17cggqd8kfg9',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ray-of-lights',
|
||||||
|
name: 'Ray of Lights',
|
||||||
|
theme: {
|
||||||
|
chat: {
|
||||||
|
inputs: {
|
||||||
|
color: '#303235',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
placeholderColor: '#9095A0',
|
||||||
|
},
|
||||||
|
buttons: { color: '#fff', backgroundColor: '#1A2249' },
|
||||||
|
hostAvatar: { isEnabled: true },
|
||||||
|
hostBubbles: { color: '#303235', backgroundColor: '#F7F8FF' },
|
||||||
|
guestBubbles: { color: '#fff', backgroundColor: '#1A2249' },
|
||||||
|
},
|
||||||
|
general: {
|
||||||
|
font: 'Raleway',
|
||||||
|
background: {
|
||||||
|
type: BackgroundType.IMAGE,
|
||||||
|
content:
|
||||||
|
'https://s3.fr-par.scw.cloud/typebot/public/typebots/hlmywyje0sbz1lfogu86pyks/blocks/uc2dyf63eeogaivqzm4z2hdb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
11
apps/builder/src/features/theme/helpers/areThemesEqual.ts
Normal file
11
apps/builder/src/features/theme/helpers/areThemesEqual.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { ThemeTemplate } from '@typebot.io/schemas'
|
||||||
|
import { dequal } from 'dequal'
|
||||||
|
|
||||||
|
export const areThemesEqual = (
|
||||||
|
selectedTemplate: ThemeTemplate['theme'],
|
||||||
|
currentTheme: ThemeTemplate['theme']
|
||||||
|
) =>
|
||||||
|
dequal(
|
||||||
|
JSON.parse(JSON.stringify(selectedTemplate)),
|
||||||
|
JSON.parse(JSON.stringify(currentTheme))
|
||||||
|
)
|
||||||
@@ -21,6 +21,7 @@ test.describe.parallel('Theme page', () => {
|
|||||||
await expect(page.locator('button >> text="Go"')).toBeVisible()
|
await expect(page.locator('button >> text="Go"')).toBeVisible()
|
||||||
|
|
||||||
// Font
|
// Font
|
||||||
|
await page.getByRole('button', { name: 'Font & Background' }).click()
|
||||||
await page.getByRole('textbox').fill('Roboto Slab')
|
await page.getByRole('textbox').fill('Roboto Slab')
|
||||||
await expect(page.locator('.typebot-container')).toHaveCSS(
|
await expect(page.locator('.typebot-container')).toHaveCSS(
|
||||||
'font-family',
|
'font-family',
|
||||||
@@ -213,4 +214,40 @@ test.describe.parallel('Theme page', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Theme templates', () => {
|
||||||
|
test('should reflect change in real-time', async ({ page }) => {
|
||||||
|
const typebotId = createId()
|
||||||
|
await importTypebotInDatabase(getTestAsset('typebots/theme.json'), {
|
||||||
|
id: typebotId,
|
||||||
|
})
|
||||||
|
await page.goto(`/typebots/${typebotId}/theme`)
|
||||||
|
await expect(page.locator('button >> text="Go"')).toBeVisible()
|
||||||
|
await page.getByRole('button', { name: 'Save current theme' }).click()
|
||||||
|
await page.getByPlaceholder('My template').fill('My awesome theme')
|
||||||
|
await page.getByRole('button', { name: 'Save' }).click()
|
||||||
|
await page.getByRole('button', { name: 'Open template menu' }).click()
|
||||||
|
await page.getByRole('menuitem', { name: 'Rename' }).click()
|
||||||
|
await expect(page.getByPlaceholder('My template')).toHaveValue(
|
||||||
|
'My awesome theme'
|
||||||
|
)
|
||||||
|
await page.getByPlaceholder('My template').fill('My awesome theme 2')
|
||||||
|
await page.getByRole('button', { name: 'Save as new template' }).click()
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Open template menu' })
|
||||||
|
).toHaveCount(2)
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'Open template menu' })
|
||||||
|
.first()
|
||||||
|
.click()
|
||||||
|
await page.getByRole('menuitem', { name: 'Delete' }).click()
|
||||||
|
await expect(page.getByText('My awesome theme 2')).toBeHidden()
|
||||||
|
await page.getByRole('button', { name: 'Gallery' }).click()
|
||||||
|
await page.getByText('Typebot Dark').click()
|
||||||
|
await expect(page.getByTestId('host-bubble')).toHaveCSS(
|
||||||
|
'background-color',
|
||||||
|
'rgb(30, 41, 59)'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { MemberInWorkspace } from '@typebot.io/prisma'
|
||||||
|
|
||||||
|
export const getUserRoleInWorkspace = (
|
||||||
|
userId: string,
|
||||||
|
workspaceMembers: MemberInWorkspace[] | undefined
|
||||||
|
) => workspaceMembers?.find((member) => member.userId === userId)?.role
|
||||||
@@ -4,6 +4,7 @@ import { credentialsRouter } from '@/features/credentials/api/router'
|
|||||||
import { getAppVersionProcedure } from '@/features/dashboard/api/getAppVersionProcedure'
|
import { getAppVersionProcedure } from '@/features/dashboard/api/getAppVersionProcedure'
|
||||||
import { resultsRouter } from '@/features/results/api/router'
|
import { resultsRouter } from '@/features/results/api/router'
|
||||||
import { processTelemetryEvent } from '@/features/telemetry/api/processTelemetryEvent'
|
import { processTelemetryEvent } from '@/features/telemetry/api/processTelemetryEvent'
|
||||||
|
import { themeRouter } from '@/features/theme/api/router'
|
||||||
import { typebotRouter } from '@/features/typebot/api/router'
|
import { typebotRouter } from '@/features/typebot/api/router'
|
||||||
import { workspaceRouter } from '@/features/workspace/api/router'
|
import { workspaceRouter } from '@/features/workspace/api/router'
|
||||||
import { router } from '../../trpc'
|
import { router } from '../../trpc'
|
||||||
@@ -17,6 +18,7 @@ export const trpcRouter = router({
|
|||||||
results: resultsRouter,
|
results: resultsRouter,
|
||||||
billing: billingRouter,
|
billing: billingRouter,
|
||||||
credentials: credentialsRouter,
|
credentials: credentialsRouter,
|
||||||
|
theme: themeRouter,
|
||||||
})
|
})
|
||||||
|
|
||||||
export type AppRouter = typeof trpcRouter
|
export type AppRouter = typeof trpcRouter
|
||||||
|
|||||||
@@ -2716,6 +2716,912 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/themeTemplates": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "query.theme.listThemeTemplates",
|
||||||
|
"summary": "List theme templates",
|
||||||
|
"tags": [
|
||||||
|
"Workspace",
|
||||||
|
"Theme"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Authorization": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "workspaceId",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"themeTemplates": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"general": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"font": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Color",
|
||||||
|
"Image",
|
||||||
|
"None"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"font",
|
||||||
|
"background"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hostAvatar": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"isEnabled"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"guestAvatar": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"isEnabled"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"hostBubbles": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"guestBubbles": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"placeholderColor": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color",
|
||||||
|
"placeholderColor"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"roundness": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"none",
|
||||||
|
"medium",
|
||||||
|
"large"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"hostBubbles",
|
||||||
|
"guestBubbles",
|
||||||
|
"buttons",
|
||||||
|
"inputs"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"customCss": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"general",
|
||||||
|
"chat"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"theme"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"themeTemplates"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$ref": "#/components/responses/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/themeTemplates/{themeTemplateId}": {
|
||||||
|
"put": {
|
||||||
|
"operationId": "mutation.theme.saveThemeTemplate",
|
||||||
|
"summary": "Save theme template",
|
||||||
|
"tags": [
|
||||||
|
"Workspace",
|
||||||
|
"Theme"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Authorization": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"general": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"font": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Color",
|
||||||
|
"Image",
|
||||||
|
"None"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"font",
|
||||||
|
"background"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hostAvatar": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"isEnabled"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"guestAvatar": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"isEnabled"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"hostBubbles": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"guestBubbles": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"placeholderColor": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color",
|
||||||
|
"placeholderColor"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"roundness": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"none",
|
||||||
|
"medium",
|
||||||
|
"large"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"hostBubbles",
|
||||||
|
"guestBubbles",
|
||||||
|
"buttons",
|
||||||
|
"inputs"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"customCss": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"general",
|
||||||
|
"chat"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"workspaceId",
|
||||||
|
"name",
|
||||||
|
"theme"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "themeTemplateId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"themeTemplate": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"general": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"font": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Color",
|
||||||
|
"Image",
|
||||||
|
"None"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"font",
|
||||||
|
"background"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hostAvatar": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"isEnabled"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"guestAvatar": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"isEnabled"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"hostBubbles": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"guestBubbles": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"placeholderColor": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color",
|
||||||
|
"placeholderColor"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"roundness": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"none",
|
||||||
|
"medium",
|
||||||
|
"large"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"hostBubbles",
|
||||||
|
"guestBubbles",
|
||||||
|
"buttons",
|
||||||
|
"inputs"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"customCss": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"general",
|
||||||
|
"chat"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"theme",
|
||||||
|
"workspaceId",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"themeTemplate"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$ref": "#/components/responses/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"operationId": "mutation.theme.deleteThemeTemplate",
|
||||||
|
"summary": "Delete a theme template",
|
||||||
|
"tags": [
|
||||||
|
"Workspace",
|
||||||
|
"Theme"
|
||||||
|
],
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"Authorization": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "workspaceId",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "themeTemplateId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"themeTemplate": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"general": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"font": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"Color",
|
||||||
|
"Image",
|
||||||
|
"None"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"font",
|
||||||
|
"background"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"chat": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hostAvatar": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"isEnabled"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"guestAvatar": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"isEnabled"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"hostBubbles": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"guestBubbles": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"inputs": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"backgroundColor": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"color": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"placeholderColor": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"backgroundColor",
|
||||||
|
"color",
|
||||||
|
"placeholderColor"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"roundness": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"none",
|
||||||
|
"medium",
|
||||||
|
"large"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"hostBubbles",
|
||||||
|
"guestBubbles",
|
||||||
|
"buttons",
|
||||||
|
"inputs"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"customCss": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"general",
|
||||||
|
"chat"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"updatedAt": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"theme",
|
||||||
|
"workspaceId",
|
||||||
|
"createdAt",
|
||||||
|
"updatedAt"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"themeTemplate"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"$ref": "#/components/responses/error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
|
|||||||
@@ -2765,6 +2765,14 @@
|
|||||||
"placeholderColor"
|
"placeholderColor"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"roundness": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"none",
|
||||||
|
"medium",
|
||||||
|
"large"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -4145,6 +4153,14 @@
|
|||||||
"placeholderColor"
|
"placeholderColor"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"roundness": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"none",
|
||||||
|
"medium",
|
||||||
|
"large"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/js",
|
"name": "@typebot.io/js",
|
||||||
"version": "0.0.30",
|
"version": "0.0.31",
|
||||||
"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",
|
||||||
|
|||||||
@@ -159,21 +159,31 @@ const BotContent = (props: BotContentProps) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const injectCustomFont = () => {
|
const injectCustomFont = () => {
|
||||||
|
const existingFont = document.getElementById('bot-font')
|
||||||
|
if (
|
||||||
|
existingFont
|
||||||
|
?.getAttribute('href')
|
||||||
|
?.includes(
|
||||||
|
props.initialChatReply.typebot?.theme?.general?.font ?? 'Open Sans'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
const font = document.createElement('link')
|
const font = document.createElement('link')
|
||||||
font.href = `https://fonts.googleapis.com/css2?family=${
|
font.href = `https://fonts.googleapis.com/css2?family=${
|
||||||
props.initialChatReply.typebot?.theme?.general?.font ?? 'Open Sans'
|
props.initialChatReply.typebot?.theme?.general?.font ?? 'Open Sans'
|
||||||
}:ital,wght@0,300;0,400;0,600;1,300;1,400;1,600&display=swap');')`
|
}:ital,wght@0,300;0,400;0,600;1,300;1,400;1,600&display=swap');')`
|
||||||
font.rel = 'stylesheet'
|
font.rel = 'stylesheet'
|
||||||
|
font.id = 'bot-font'
|
||||||
document.head.appendChild(font)
|
document.head.appendChild(font)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
injectCustomFont()
|
|
||||||
if (!botContainer) return
|
if (!botContainer) return
|
||||||
resizeObserver.observe(botContainer)
|
resizeObserver.observe(botContainer)
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
injectCustomFont()
|
||||||
if (!botContainer) return
|
if (!botContainer) return
|
||||||
setCssVariablesValue(props.initialChatReply.typebot.theme, botContainer)
|
setCssVariablesValue(props.initialChatReply.typebot.theme, botContainer)
|
||||||
})
|
})
|
||||||
@@ -187,7 +197,7 @@ const BotContent = (props: BotContentProps) => {
|
|||||||
<div
|
<div
|
||||||
ref={botContainer}
|
ref={botContainer}
|
||||||
class={
|
class={
|
||||||
'relative flex w-full h-full text-base overflow-hidden bg-cover flex-col items-center typebot-container ' +
|
'relative flex w-full h-full text-base overflow-hidden bg-cover bg-center flex-col items-center typebot-container ' +
|
||||||
props.class
|
props.class
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ export const ChoiceForm = (props: Props) => {
|
|||||||
</button>
|
</button>
|
||||||
{props.inputIndex === 0 && props.block.items.length === 1 && (
|
{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="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-225 opacity-75" />
|
<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-200" />
|
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -134,6 +134,8 @@ const setTypebotBackground = (
|
|||||||
background: Background,
|
background: Background,
|
||||||
documentStyle: CSSStyleDeclaration
|
documentStyle: CSSStyleDeclaration
|
||||||
) => {
|
) => {
|
||||||
|
documentStyle.setProperty(cssVariableNames.general.bgImage, null)
|
||||||
|
documentStyle.setProperty(cssVariableNames.general.bgColor, null)
|
||||||
documentStyle.setProperty(
|
documentStyle.setProperty(
|
||||||
background?.type === BackgroundType.IMAGE
|
background?.type === BackgroundType.IMAGE
|
||||||
? cssVariableNames.general.bgImage
|
? cssVariableNames.general.bgImage
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@typebot.io/react",
|
"name": "@typebot.io/react",
|
||||||
"version": "0.0.30",
|
"version": "0.0.31",
|
||||||
"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",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const parseTestTypebot = (
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
customDomain: null,
|
customDomain: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
|
selectedThemeTemplateId: null,
|
||||||
isArchived: false,
|
isArchived: false,
|
||||||
isClosed: false,
|
isClosed: false,
|
||||||
resultsTablePreferences: null,
|
resultsTablePreferences: null,
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ model Workspace {
|
|||||||
customChatsLimit Int?
|
customChatsLimit Int?
|
||||||
customStorageLimit Int?
|
customStorageLimit Int?
|
||||||
customSeatsLimit Int?
|
customSeatsLimit Int?
|
||||||
|
themeTemplates ThemeTemplate[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model MemberInWorkspace {
|
model MemberInWorkspace {
|
||||||
@@ -180,6 +181,7 @@ model Typebot {
|
|||||||
variables Json
|
variables Json
|
||||||
edges Json
|
edges Json
|
||||||
theme Json
|
theme Json
|
||||||
|
selectedThemeTemplateId String?
|
||||||
settings Json
|
settings Json
|
||||||
publicId String? @unique
|
publicId String? @unique
|
||||||
customDomain String? @unique
|
customDomain String? @unique
|
||||||
@@ -325,6 +327,18 @@ model ChatSession {
|
|||||||
state Json
|
state Json
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ThemeTemplate {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
name String
|
||||||
|
theme Json
|
||||||
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
workspaceId String
|
||||||
|
|
||||||
|
@@index([workspaceId])
|
||||||
|
}
|
||||||
|
|
||||||
enum WorkspaceRole {
|
enum WorkspaceRole {
|
||||||
ADMIN
|
ADMIN
|
||||||
MEMBER
|
MEMBER
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Typebot" ADD COLUMN "selectedThemeTemplateId" TEXT;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ThemeTemplate" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"theme" JSONB NOT NULL,
|
||||||
|
"workspaceId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "ThemeTemplate_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ThemeTemplate" ADD CONSTRAINT "ThemeTemplate_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -89,6 +89,7 @@ model Workspace {
|
|||||||
customChatsLimit Int?
|
customChatsLimit Int?
|
||||||
customStorageLimit Int?
|
customStorageLimit Int?
|
||||||
customSeatsLimit Int?
|
customSeatsLimit Int?
|
||||||
|
themeTemplates ThemeTemplate[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model MemberInWorkspace {
|
model MemberInWorkspace {
|
||||||
@@ -164,6 +165,7 @@ model Typebot {
|
|||||||
variables Json
|
variables Json
|
||||||
edges Json
|
edges Json
|
||||||
theme Json
|
theme Json
|
||||||
|
selectedThemeTemplateId String?
|
||||||
settings Json
|
settings Json
|
||||||
publicId String? @unique
|
publicId String? @unique
|
||||||
customDomain String? @unique
|
customDomain String? @unique
|
||||||
@@ -304,6 +306,16 @@ model ChatSession {
|
|||||||
state Json
|
state Json
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ThemeTemplate {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
name String
|
||||||
|
theme Json
|
||||||
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
workspaceId String
|
||||||
|
}
|
||||||
|
|
||||||
enum WorkspaceRole {
|
enum WorkspaceRole {
|
||||||
ADMIN
|
ADMIN
|
||||||
MEMBER
|
MEMBER
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ThemeTemplate as ThemeTemplatePrisma } from '@typebot.io/prisma'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { BackgroundType } from './enums'
|
import { BackgroundType } from './enums'
|
||||||
|
|
||||||
@@ -43,6 +44,15 @@ export const themeSchema = z.object({
|
|||||||
customCss: z.string().optional(),
|
customCss: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const themeTemplateSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
theme: themeSchema,
|
||||||
|
workspaceId: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
}) satisfies z.ZodType<ThemeTemplatePrisma>
|
||||||
|
|
||||||
export const defaultTheme: Theme = {
|
export const defaultTheme: Theme = {
|
||||||
chat: {
|
chat: {
|
||||||
hostBubbles: { backgroundColor: '#F7F8FF', color: '#303235' },
|
hostBubbles: { backgroundColor: '#F7F8FF', color: '#303235' },
|
||||||
@@ -67,3 +77,4 @@ export type GeneralTheme = z.infer<typeof generalThemeSchema>
|
|||||||
export type Background = z.infer<typeof backgroundSchema>
|
export type Background = z.infer<typeof backgroundSchema>
|
||||||
export type ContainerColors = z.infer<typeof containerColorsSchema>
|
export type ContainerColors = z.infer<typeof containerColorsSchema>
|
||||||
export type InputColors = z.infer<typeof inputColorsSchema>
|
export type InputColors = z.infer<typeof inputColorsSchema>
|
||||||
|
export type ThemeTemplate = z.infer<typeof themeTemplateSchema>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const typebotSchema = z.object({
|
|||||||
edges: z.array(edgeSchema),
|
edges: z.array(edgeSchema),
|
||||||
variables: z.array(variableSchema),
|
variables: z.array(variableSchema),
|
||||||
theme: themeSchema,
|
theme: themeSchema,
|
||||||
|
selectedThemeTemplateId: z.string().nullable(),
|
||||||
settings: settingsSchema,
|
settings: settingsSchema,
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
|
|||||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -660,10 +660,10 @@ importers:
|
|||||||
packages/embeds/wordpress:
|
packages/embeds/wordpress:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@prettier/plugin-php': 0.19.4
|
'@prettier/plugin-php': 0.19.4
|
||||||
prettier: 2.8.4
|
prettier: 2.8.7
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@prettier/plugin-php': 0.19.4_prettier@2.8.4
|
'@prettier/plugin-php': 0.19.4_prettier@2.8.7
|
||||||
prettier: 2.8.4
|
prettier: 2.8.7
|
||||||
|
|
||||||
packages/eslint-config-custom:
|
packages/eslint-config-custom:
|
||||||
specifiers:
|
specifiers:
|
||||||
@@ -5641,7 +5641,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
|
resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@prettier/plugin-php/0.19.4_prettier@2.8.4:
|
/@prettier/plugin-php/0.19.4_prettier@2.8.7:
|
||||||
resolution: {integrity: sha512-FiSnSfP+Vo0/HVRXg7ZnEYJEM1eWS+MmsozYtzEdIf8Vg9v/+fwvyXMNayI5SgZ1Y9F5LGhl/EOMWIPzr9c2Xg==}
|
resolution: {integrity: sha512-FiSnSfP+Vo0/HVRXg7ZnEYJEM1eWS+MmsozYtzEdIf8Vg9v/+fwvyXMNayI5SgZ1Y9F5LGhl/EOMWIPzr9c2Xg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
prettier: ^1.15.0 || ^2.0.0
|
prettier: ^1.15.0 || ^2.0.0
|
||||||
@@ -5649,7 +5649,7 @@ packages:
|
|||||||
linguist-languages: 7.21.0
|
linguist-languages: 7.21.0
|
||||||
mem: 8.1.1
|
mem: 8.1.1
|
||||||
php-parser: 3.1.3
|
php-parser: 3.1.3
|
||||||
prettier: 2.8.4
|
prettier: 2.8.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@prisma/client/4.11.0_prisma@4.11.0:
|
/@prisma/client/4.11.0_prisma@4.11.0:
|
||||||
@@ -16728,6 +16728,12 @@ packages:
|
|||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
/prettier/2.8.7:
|
||||||
|
resolution: {integrity: sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==}
|
||||||
|
engines: {node: '>=10.13.0'}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/pretty-error/4.0.0:
|
/pretty-error/4.0.0:
|
||||||
resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==}
|
resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user