✨ (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,
|
||||
ButtonProps,
|
||||
} from '@chakra-ui/react'
|
||||
import React, { ChangeEvent, useEffect, useState } from 'react'
|
||||
import React, { ChangeEvent, useState } from 'react'
|
||||
import tinyColor from 'tinycolor2'
|
||||
|
||||
const colorsSelection: `#${string}`[] = [
|
||||
@@ -29,31 +29,37 @@ const colorsSelection: `#${string}`[] = [
|
||||
]
|
||||
|
||||
type Props = {
|
||||
initialColor?: string
|
||||
value?: string
|
||||
defaultValue?: string
|
||||
onColorChange: (color: string) => void
|
||||
}
|
||||
|
||||
export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
|
||||
const [color, setColor] = useState(initialColor ?? '')
|
||||
export const ColorPicker = ({ value, defaultValue, onColorChange }: Props) => {
|
||||
const [color, setColor] = useState(defaultValue ?? '')
|
||||
const displayedValue = value ?? color
|
||||
|
||||
useEffect(() => {
|
||||
onColorChange(color)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [color])
|
||||
|
||||
const handleColorChange = (e: ChangeEvent<HTMLInputElement>) =>
|
||||
const handleColorChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setColor(e.target.value)
|
||||
onColorChange(e.target.value)
|
||||
}
|
||||
|
||||
const handleClick = (color: string) => () => setColor(color)
|
||||
const handleClick = (color: string) => () => {
|
||||
setColor(color)
|
||||
onColorChange(color)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover variant="picker" placement="right" isLazy>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
aria-label={'Pick a color'}
|
||||
bgColor={color}
|
||||
_hover={{ bgColor: `#${tinyColor(color).darken(10).toHex()}` }}
|
||||
_active={{ bgColor: `#${tinyColor(color).darken(30).toHex()}` }}
|
||||
bgColor={displayedValue}
|
||||
_hover={{
|
||||
bgColor: `#${tinyColor(displayedValue).darken(10).toHex()}`,
|
||||
}}
|
||||
_active={{
|
||||
bgColor: `#${tinyColor(displayedValue).darken(30).toHex()}`,
|
||||
}}
|
||||
height="22px"
|
||||
width="22px"
|
||||
padding={0}
|
||||
@@ -62,16 +68,16 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent width="170px">
|
||||
<PopoverArrow bg={color} />
|
||||
<PopoverArrow bg={displayedValue} />
|
||||
<PopoverCloseButton color="white" />
|
||||
<PopoverHeader
|
||||
height="100px"
|
||||
backgroundColor={color}
|
||||
backgroundColor={displayedValue}
|
||||
borderTopLeftRadius={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>
|
||||
<PopoverBody as={Stack}>
|
||||
<SimpleGrid columns={5} spacing={2}>
|
||||
@@ -96,12 +102,12 @@ export const ColorPicker = ({ initialColor, onColorChange }: Props) => {
|
||||
placeholder="#2a9d8f"
|
||||
aria-label="Color value"
|
||||
size="sm"
|
||||
value={color}
|
||||
value={displayedValue}
|
||||
onChange={handleColorChange}
|
||||
/>
|
||||
<NativeColorPicker
|
||||
size="sm"
|
||||
color={color}
|
||||
color={displayedValue}
|
||||
onColorChange={handleColorChange}
|
||||
>
|
||||
Advanced picker
|
||||
|
||||
@@ -202,15 +202,6 @@ export const CodeIcon = (props: IconProps) => (
|
||||
</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) => (
|
||||
<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>
|
||||
@@ -591,3 +582,15 @@ export const LargeRadiusIcon = (props: IconProps) => (
|
||||
/>
|
||||
</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 = {
|
||||
items: string[]
|
||||
value?: string
|
||||
defaultValue?: string
|
||||
debounceTimeout?: number
|
||||
placeholder?: string
|
||||
@@ -40,6 +41,7 @@ export const AutocompleteInput = ({
|
||||
debounceTimeout,
|
||||
placeholder,
|
||||
withVariableButton = true,
|
||||
value,
|
||||
defaultValue,
|
||||
label,
|
||||
moreInfoTooltip,
|
||||
@@ -178,7 +180,7 @@ export const AutocompleteInput = ({
|
||||
<Input
|
||||
autoComplete="off"
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
value={value ?? inputValue}
|
||||
onChange={(e) => changeValue(e.target.value)}
|
||||
onFocus={onOpen}
|
||||
onBlur={updateCarretPosition}
|
||||
|
||||
@@ -65,7 +65,6 @@ export const CodeEditor = ({
|
||||
}
|
||||
|
||||
const handleChange = (newValue: string) => {
|
||||
if (isDefined(props.value)) return
|
||||
setValue(newValue)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,15 +11,18 @@ import { ReactNode } from 'react'
|
||||
|
||||
type Props<T extends string> = {
|
||||
options: (T | { value: T; label: ReactNode })[]
|
||||
defaultValue: T
|
||||
value?: T
|
||||
defaultValue?: T
|
||||
onSelect: (newValue: T) => void
|
||||
}
|
||||
export const RadioButtons = <T extends string>({
|
||||
options,
|
||||
value,
|
||||
defaultValue,
|
||||
onSelect,
|
||||
}: Props<T>) => {
|
||||
const { getRootProps, getRadioProps } = useRadioGroup({
|
||||
value,
|
||||
defaultValue,
|
||||
onChange: onSelect,
|
||||
})
|
||||
|
||||
@@ -24,7 +24,7 @@ import { MoreInfoTooltip } from '../MoreInfoTooltip'
|
||||
|
||||
export type TextInputProps = {
|
||||
defaultValue?: string
|
||||
onChange: (value: string) => void
|
||||
onChange?: (value: string) => void
|
||||
debounceTimeout?: number
|
||||
label?: ReactNode
|
||||
helperText?: ReactNode
|
||||
@@ -66,7 +66,8 @@ export const TextInput = forwardRef(function TextInput(
|
||||
localValue.length ?? 0
|
||||
)
|
||||
const onChange = useDebouncedCallback(
|
||||
_onChange,
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
_onChange ?? (() => {}),
|
||||
env('E2E_TEST') === 'true' ? 0 : debounceTimeout
|
||||
)
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ export const parseNewTypebot = ({
|
||||
groups: [startGroup],
|
||||
edges: [],
|
||||
variables: [],
|
||||
selectedThemeTemplateId: null,
|
||||
theme: {
|
||||
...defaultTheme,
|
||||
chat: {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import {
|
||||
LogicBlockType,
|
||||
PublicTypebot,
|
||||
ResultsTablePreferences,
|
||||
Settings,
|
||||
Theme,
|
||||
Typebot,
|
||||
Webhook,
|
||||
} from '@typebot.io/schemas'
|
||||
@@ -45,16 +42,20 @@ import { convertPublicTypebotToTypebot } from '@/features/publish/helpers/conver
|
||||
|
||||
const autoSaveTimeout = 10000
|
||||
|
||||
type UpdateTypebotPayload = Partial<{
|
||||
theme: Theme
|
||||
settings: Settings
|
||||
publicId: string
|
||||
name: string
|
||||
icon: string
|
||||
customDomain: string | null
|
||||
resultsTablePreferences: ResultsTablePreferences
|
||||
isClosed: boolean
|
||||
}>
|
||||
type UpdateTypebotPayload = Partial<
|
||||
Pick<
|
||||
Typebot,
|
||||
| 'theme'
|
||||
| 'selectedThemeTemplateId'
|
||||
| 'settings'
|
||||
| 'publicId'
|
||||
| 'name'
|
||||
| 'icon'
|
||||
| 'customDomain'
|
||||
| 'resultsTablePreferences'
|
||||
| 'isClosed'
|
||||
>
|
||||
>
|
||||
|
||||
export type SetTypebot = (
|
||||
newPresent: Typebot | ((current: Typebot) => Typebot)
|
||||
|
||||
@@ -37,14 +37,14 @@ export const ButtonThemeSettings = ({ buttonTheme, onChange }: Props) => {
|
||||
<HStack justify="space-between">
|
||||
<Text>Background color</Text>
|
||||
<ColorPicker
|
||||
initialColor={buttonTheme?.backgroundColor}
|
||||
defaultValue={buttonTheme?.backgroundColor}
|
||||
onColorChange={updateBackgroundColor}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack justify="space-between">
|
||||
<Text>Icon color</Text>
|
||||
<ColorPicker
|
||||
initialColor={buttonTheme?.iconColor}
|
||||
defaultValue={buttonTheme?.iconColor}
|
||||
onColorChange={updateIconColor}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
@@ -49,28 +49,28 @@ export const PreviewMessageThemeSettings = ({
|
||||
<HStack justify="space-between">
|
||||
<Text>Background color</Text>
|
||||
<ColorPicker
|
||||
initialColor={previewMessageTheme?.backgroundColor}
|
||||
defaultValue={previewMessageTheme?.backgroundColor}
|
||||
onColorChange={updateBackgroundColor}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack justify="space-between">
|
||||
<Text>Text color</Text>
|
||||
<ColorPicker
|
||||
initialColor={previewMessageTheme?.textColor}
|
||||
defaultValue={previewMessageTheme?.textColor}
|
||||
onColorChange={updateTextColor}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack justify="space-between">
|
||||
<Text>Close button background</Text>
|
||||
<ColorPicker
|
||||
initialColor={previewMessageTheme?.closeButtonBackgroundColor}
|
||||
defaultValue={previewMessageTheme?.closeButtonBackgroundColor}
|
||||
onColorChange={updateCloseButtonBackgroundColor}
|
||||
/>
|
||||
</HStack>
|
||||
<HStack justify="space-between">
|
||||
<Text>Close icon color</Text>
|
||||
<ColorPicker
|
||||
initialColor={previewMessageTheme?.closeButtonIconColor}
|
||||
defaultValue={previewMessageTheme?.closeButtonIconColor}
|
||||
onColorChange={updateCloseButtonIconColor}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
@@ -22,4 +22,5 @@ export const convertPublicTypebotToTypebot = (
|
||||
isArchived: existingTypebot.isArchived,
|
||||
isClosed: existingTypebot.isClosed,
|
||||
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) => {
|
||||
return (
|
||||
<CodeEditor
|
||||
defaultValue={customCss ?? ''}
|
||||
value={customCss ?? ''}
|
||||
lang="css"
|
||||
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,
|
||||
HStack,
|
||||
Stack,
|
||||
Tag,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChatIcon, CodeIcon, PencilIcon } from '@/components/icons'
|
||||
import { ChatTheme, GeneralTheme } from '@typebot.io/schemas'
|
||||
import { ChatIcon, CodeIcon, DropletIcon, TableIcon } from '@/components/icons'
|
||||
import { ChatTheme, GeneralTheme, ThemeTemplate } from '@typebot.io/schemas'
|
||||
import React from 'react'
|
||||
import { CustomCssSettings } from './CustomCssSettings'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { headerHeight } from '@/features/editor/constants'
|
||||
import { ChatThemeSettings } from './chat/ChatThemeSettings'
|
||||
import { GeneralSettings } from './general/GeneralSettings'
|
||||
import { ThemeTemplates } from './ThemeTemplates'
|
||||
|
||||
export const ThemeSideMenu = () => {
|
||||
const { typebot, updateTypebot } = useTypebot()
|
||||
|
||||
const handleChatThemeChange = (chat: ChatTheme) =>
|
||||
const updateChatTheme = (chat: ChatTheme) =>
|
||||
typebot && updateTypebot({ theme: { ...typebot.theme, chat } })
|
||||
|
||||
const handleGeneralThemeChange = (general: GeneralTheme) =>
|
||||
const updateGeneralTheme = (general: GeneralTheme) =>
|
||||
typebot && updateTypebot({ theme: { ...typebot.theme, general } })
|
||||
|
||||
const handleCustomCssChange = (customCss: string) =>
|
||||
const updateCustomCss = (customCss: string) =>
|
||||
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 (
|
||||
<Stack
|
||||
flex="1"
|
||||
@@ -48,8 +61,33 @@ export const ThemeSideMenu = () => {
|
||||
<AccordionItem>
|
||||
<AccordionButton py={6}>
|
||||
<HStack flex="1" pl={2}>
|
||||
<PencilIcon />
|
||||
<Heading fontSize="lg">General</Heading>
|
||||
<TableIcon />
|
||||
<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>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
@@ -57,7 +95,7 @@ export const ThemeSideMenu = () => {
|
||||
{typebot && (
|
||||
<GeneralSettings
|
||||
generalTheme={typebot.theme.general}
|
||||
onGeneralThemeChange={handleGeneralThemeChange}
|
||||
onGeneralThemeChange={updateGeneralTheme}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
@@ -75,7 +113,7 @@ export const ThemeSideMenu = () => {
|
||||
<ChatThemeSettings
|
||||
typebotId={typebot.id}
|
||||
chatTheme={typebot.theme.chat}
|
||||
onChatThemeChange={handleChatThemeChange}
|
||||
onChatThemeChange={updateChatTheme}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
@@ -92,7 +130,7 @@ export const ThemeSideMenu = () => {
|
||||
{typebot && (
|
||||
<CustomCssSettings
|
||||
customCss={typebot.theme.customCss}
|
||||
onCustomCssChange={handleCustomCssChange}
|
||||
onCustomCssChange={updateCustomCss}
|
||||
/>
|
||||
)}
|
||||
</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">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
initialColor={buttons.backgroundColor}
|
||||
value={buttons.backgroundColor}
|
||||
onColorChange={handleBackgroundChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={buttons.color}
|
||||
onColorChange={handleTextChange}
|
||||
/>
|
||||
<ColorPicker value={buttons.color} onColorChange={handleTextChange} />
|
||||
</Flex>
|
||||
</Stack>
|
||||
)
|
||||
|
||||
@@ -65,29 +65,7 @@ export const ChatThemeSettings = ({
|
||||
onHostBubblesChange={handleHostBubblesChange}
|
||||
/>
|
||||
</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}>
|
||||
<Heading fontSize="lg">User bubbles</Heading>
|
||||
<GuestBubbles
|
||||
@@ -109,6 +87,29 @@ export const ChatThemeSettings = ({
|
||||
onInputsChange={handleInputsChange}
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,14 +19,14 @@ export const GuestBubbles = ({ guestBubbles, onGuestBubblesChange }: Props) => {
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
initialColor={guestBubbles.backgroundColor}
|
||||
value={guestBubbles.backgroundColor}
|
||||
onColorChange={handleBackgroundChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={guestBubbles.color}
|
||||
value={guestBubbles.color}
|
||||
onColorChange={handleTextChange}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
@@ -19,14 +19,14 @@ export const HostBubbles = ({ hostBubbles, onHostBubblesChange }: Props) => {
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
initialColor={hostBubbles.backgroundColor}
|
||||
value={hostBubbles.backgroundColor}
|
||||
onColorChange={handleBackgroundChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={hostBubbles.color}
|
||||
value={hostBubbles.color}
|
||||
onColorChange={handleTextChange}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
@@ -21,21 +21,18 @@ export const InputsTheme = ({ inputs, onInputsChange }: Props) => {
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background:</Text>
|
||||
<ColorPicker
|
||||
initialColor={inputs.backgroundColor}
|
||||
value={inputs.backgroundColor}
|
||||
onColorChange={handleBackgroundChange}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={inputs.color}
|
||||
onColorChange={handleTextChange}
|
||||
/>
|
||||
<ColorPicker value={inputs.color} onColorChange={handleTextChange} />
|
||||
</Flex>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Placeholder text:</Text>
|
||||
<ColorPicker
|
||||
initialColor={inputs.placeholderColor}
|
||||
value={inputs.placeholderColor}
|
||||
onColorChange={handlePlaceholderChange}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
@@ -35,7 +35,7 @@ export const BackgroundContent = ({
|
||||
<Flex justify="space-between" align="center">
|
||||
<Text>Background color:</Text>
|
||||
<ColorPicker
|
||||
initialColor={background.content ?? defaultBackgroundColor}
|
||||
value={background.content ?? defaultBackgroundColor}
|
||||
onColorChange={handleContentChange}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
@@ -31,7 +31,7 @@ export const BackgroundSelector = ({
|
||||
BackgroundType.IMAGE,
|
||||
BackgroundType.NONE,
|
||||
]}
|
||||
defaultValue={background?.type ?? defaultBackgroundType}
|
||||
value={background?.type ?? defaultBackgroundType}
|
||||
onSelect={handleBackgroundTypeChange}
|
||||
/>
|
||||
<BackgroundContent
|
||||
|
||||
@@ -41,7 +41,7 @@ export const FontSelector = ({
|
||||
<HStack justify="space-between" align="center">
|
||||
<Text>Font</Text>
|
||||
<AutocompleteInput
|
||||
defaultValue={activeFont}
|
||||
value={activeFont}
|
||||
items={googleFonts}
|
||||
onChange={handleFontSelected}
|
||||
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()
|
||||
|
||||
// Font
|
||||
await page.getByRole('button', { name: 'Font & Background' }).click()
|
||||
await page.getByRole('textbox').fill('Roboto Slab')
|
||||
await expect(page.locator('.typebot-container')).toHaveCSS(
|
||||
'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 { resultsRouter } from '@/features/results/api/router'
|
||||
import { processTelemetryEvent } from '@/features/telemetry/api/processTelemetryEvent'
|
||||
import { themeRouter } from '@/features/theme/api/router'
|
||||
import { typebotRouter } from '@/features/typebot/api/router'
|
||||
import { workspaceRouter } from '@/features/workspace/api/router'
|
||||
import { router } from '../../trpc'
|
||||
@@ -17,6 +18,7 @@ export const trpcRouter = router({
|
||||
results: resultsRouter,
|
||||
billing: billingRouter,
|
||||
credentials: credentialsRouter,
|
||||
theme: themeRouter,
|
||||
})
|
||||
|
||||
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": {
|
||||
|
||||
@@ -2765,6 +2765,14 @@
|
||||
"placeholderColor"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"roundness": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"none",
|
||||
"medium",
|
||||
"large"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -4145,6 +4153,14 @@
|
||||
"placeholderColor"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"roundness": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"none",
|
||||
"medium",
|
||||
"large"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/js",
|
||||
"version": "0.0.30",
|
||||
"version": "0.0.31",
|
||||
"description": "Javascript library to display typebots on your website",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -159,21 +159,31 @@ const BotContent = (props: BotContentProps) => {
|
||||
})
|
||||
|
||||
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')
|
||||
font.href = `https://fonts.googleapis.com/css2?family=${
|
||||
props.initialChatReply.typebot?.theme?.general?.font ?? 'Open Sans'
|
||||
}:ital,wght@0,300;0,400;0,600;1,300;1,400;1,600&display=swap');')`
|
||||
font.rel = 'stylesheet'
|
||||
font.id = 'bot-font'
|
||||
document.head.appendChild(font)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
injectCustomFont()
|
||||
if (!botContainer) return
|
||||
resizeObserver.observe(botContainer)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
injectCustomFont()
|
||||
if (!botContainer) return
|
||||
setCssVariablesValue(props.initialChatReply.typebot.theme, botContainer)
|
||||
})
|
||||
@@ -187,7 +197,7 @@ const BotContent = (props: BotContentProps) => {
|
||||
<div
|
||||
ref={botContainer}
|
||||
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
|
||||
}
|
||||
>
|
||||
|
||||
@@ -62,8 +62,8 @@ export const ChoiceForm = (props: Props) => {
|
||||
</button>
|
||||
{props.inputIndex === 0 && props.block.items.length === 1 && (
|
||||
<span class="flex h-3 w-3 absolute top-0 right-0 -mt-1 -mr-1 ping">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-225 opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 brightness-200" />
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full brightness-200 opacity-75" />
|
||||
<span class="relative inline-flex rounded-full h-3 w-3 brightness-150" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -134,6 +134,8 @@ const setTypebotBackground = (
|
||||
background: Background,
|
||||
documentStyle: CSSStyleDeclaration
|
||||
) => {
|
||||
documentStyle.setProperty(cssVariableNames.general.bgImage, null)
|
||||
documentStyle.setProperty(cssVariableNames.general.bgColor, null)
|
||||
documentStyle.setProperty(
|
||||
background?.type === BackgroundType.IMAGE
|
||||
? cssVariableNames.general.bgImage
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@typebot.io/react",
|
||||
"version": "0.0.30",
|
||||
"version": "0.0.31",
|
||||
"description": "React library to display typebots on your website",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -27,6 +27,7 @@ export const parseTestTypebot = (
|
||||
createdAt: new Date(),
|
||||
customDomain: null,
|
||||
icon: null,
|
||||
selectedThemeTemplateId: null,
|
||||
isArchived: false,
|
||||
isClosed: false,
|
||||
resultsTablePreferences: null,
|
||||
|
||||
@@ -95,6 +95,7 @@ model Workspace {
|
||||
customChatsLimit Int?
|
||||
customStorageLimit Int?
|
||||
customSeatsLimit Int?
|
||||
themeTemplates ThemeTemplate[]
|
||||
}
|
||||
|
||||
model MemberInWorkspace {
|
||||
@@ -180,6 +181,7 @@ model Typebot {
|
||||
variables Json
|
||||
edges Json
|
||||
theme Json
|
||||
selectedThemeTemplateId String?
|
||||
settings Json
|
||||
publicId String? @unique
|
||||
customDomain String? @unique
|
||||
@@ -325,6 +327,18 @@ model ChatSession {
|
||||
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 {
|
||||
ADMIN
|
||||
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?
|
||||
customStorageLimit Int?
|
||||
customSeatsLimit Int?
|
||||
themeTemplates ThemeTemplate[]
|
||||
}
|
||||
|
||||
model MemberInWorkspace {
|
||||
@@ -164,6 +165,7 @@ model Typebot {
|
||||
variables Json
|
||||
edges Json
|
||||
theme Json
|
||||
selectedThemeTemplateId String?
|
||||
settings Json
|
||||
publicId String? @unique
|
||||
customDomain String? @unique
|
||||
@@ -304,6 +306,16 @@ model ChatSession {
|
||||
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 {
|
||||
ADMIN
|
||||
MEMBER
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ThemeTemplate as ThemeTemplatePrisma } from '@typebot.io/prisma'
|
||||
import { z } from 'zod'
|
||||
import { BackgroundType } from './enums'
|
||||
|
||||
@@ -43,6 +44,15 @@ export const themeSchema = z.object({
|
||||
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 = {
|
||||
chat: {
|
||||
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 ContainerColors = z.infer<typeof containerColorsSchema>
|
||||
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),
|
||||
variables: z.array(variableSchema),
|
||||
theme: themeSchema,
|
||||
selectedThemeTemplateId: z.string().nullable(),
|
||||
settings: settingsSchema,
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -660,10 +660,10 @@ importers:
|
||||
packages/embeds/wordpress:
|
||||
specifiers:
|
||||
'@prettier/plugin-php': 0.19.4
|
||||
prettier: 2.8.4
|
||||
prettier: 2.8.7
|
||||
devDependencies:
|
||||
'@prettier/plugin-php': 0.19.4_prettier@2.8.4
|
||||
prettier: 2.8.4
|
||||
'@prettier/plugin-php': 0.19.4_prettier@2.8.7
|
||||
prettier: 2.8.7
|
||||
|
||||
packages/eslint-config-custom:
|
||||
specifiers:
|
||||
@@ -5641,7 +5641,7 @@ packages:
|
||||
resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
|
||||
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==}
|
||||
peerDependencies:
|
||||
prettier: ^1.15.0 || ^2.0.0
|
||||
@@ -5649,7 +5649,7 @@ packages:
|
||||
linguist-languages: 7.21.0
|
||||
mem: 8.1.1
|
||||
php-parser: 3.1.3
|
||||
prettier: 2.8.4
|
||||
prettier: 2.8.7
|
||||
dev: true
|
||||
|
||||
/@prisma/client/4.11.0_prisma@4.11.0:
|
||||
@@ -16728,6 +16728,12 @@ packages:
|
||||
engines: {node: '>=10.13.0'}
|
||||
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:
|
||||
resolution: {integrity: sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==}
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user