✨ (theme) Custom font option (#1268)
Closes #1249 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced components for customizing fonts, including Google and custom font options. - Enhanced theme customization by simplifying theme objects and adding new font customization options. - Implemented dynamic font injection for web pages based on user-selected font configurations. - **Bug Fixes** - Fixed a condition in theme template card rendering to correctly check avatar enablement. - Corrected font property handling across various components to support both string and object types. - **Refactor** - Updated option properties in RadioButtons component to be readonly. - Simplified logic for setting CSS variables for fonts, including checks for font types and existence. - **Documentation** - Added constants and schemas for supporting new font types in theme customization. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -13,7 +13,6 @@ import {
|
||||
} from '@typebot.io/schemas'
|
||||
import { Log } from '@typebot.io/prisma'
|
||||
import { LiteBadge } from './LiteBadge'
|
||||
import { isNotEmpty } from '@typebot.io/lib'
|
||||
import { BackgroundType } from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||
import { env } from '@typebot.io/env'
|
||||
|
||||
@@ -74,7 +73,7 @@ export const TypebotViewer = ({
|
||||
</style>
|
||||
<style>{typebot.theme?.customCss}</style>
|
||||
<style>{importantStyles}</style>
|
||||
{isNotEmpty(typebot?.theme?.general?.font) && (
|
||||
{typebot?.theme?.general?.font && (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `@import url('https://fonts.googleapis.com/css2?family=${
|
||||
|
||||
@@ -50,7 +50,8 @@ const setGeneralTheme = (
|
||||
) => {
|
||||
const { background, font } = generalTheme
|
||||
if (background) setTypebotBackground
|
||||
if (font) documentStyle.setProperty(cssVariableNames.general.fontFamily, font)
|
||||
if (font && typeof font === 'string')
|
||||
documentStyle.setProperty(cssVariableNames.general.fontFamily, font)
|
||||
}
|
||||
|
||||
const setChatTheme = (
|
||||
|
||||
@@ -12,11 +12,12 @@ import {
|
||||
} from '@/utils/storage'
|
||||
import { setCssVariablesValue } from '@/utils/setCssVariablesValue'
|
||||
import immutableCss from '../assets/immutable.css'
|
||||
import { InputBlock } from '@typebot.io/schemas'
|
||||
import { Font, InputBlock } from '@typebot.io/schemas'
|
||||
import { StartFrom } from '@typebot.io/schemas'
|
||||
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||
import { clsx } from 'clsx'
|
||||
import { HTTPError } from 'ky'
|
||||
import { injectFont } from '@/utils/injectFont'
|
||||
|
||||
export type BotProps = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -25,6 +26,7 @@ export type BotProps = {
|
||||
resultId?: string
|
||||
prefilledVariables?: Record<string, unknown>
|
||||
apiHost?: string
|
||||
font?: Font
|
||||
onNewInputBlock?: (inputBlock: InputBlock) => void
|
||||
onAnswer?: (answer: { message: string; blockId: string }) => void
|
||||
onInit?: () => void
|
||||
@@ -42,6 +44,7 @@ export const Bot = (props: BotProps & { class?: string }) => {
|
||||
const [error, setError] = createSignal<Error | undefined>()
|
||||
|
||||
const initializeBot = async () => {
|
||||
if (props.font) injectFont(props.font)
|
||||
setIsInitialized(true)
|
||||
const urlParams = new URLSearchParams(location.search)
|
||||
props.onInit?.()
|
||||
@@ -193,34 +196,16 @@ const BotContent = (props: BotContentProps) => {
|
||||
return setIsMobile(entries[0].target.clientWidth < 400)
|
||||
})
|
||||
|
||||
const injectCustomFont = () => {
|
||||
const existingFont = document.getElementById('bot-font')
|
||||
if (
|
||||
existingFont
|
||||
?.getAttribute('href')
|
||||
?.includes(
|
||||
props.initialChatReply.typebot?.theme?.general?.font ??
|
||||
defaultTheme.general.font
|
||||
)
|
||||
)
|
||||
return
|
||||
const font = document.createElement('link')
|
||||
font.href = `https://fonts.bunny.net/css2?family=${
|
||||
props.initialChatReply.typebot?.theme?.general?.font ??
|
||||
defaultTheme.general.font
|
||||
}: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(() => {
|
||||
if (!botContainer) return
|
||||
resizeObserver.observe(botContainer)
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
injectCustomFont()
|
||||
injectFont(
|
||||
props.initialChatReply.typebot.theme.general?.font ??
|
||||
defaultTheme.general.font
|
||||
)
|
||||
if (!botContainer) return
|
||||
setCssVariablesValue(props.initialChatReply.typebot.theme, botContainer)
|
||||
})
|
||||
|
||||
32
packages/embeds/js/src/utils/injectFont.ts
Normal file
32
packages/embeds/js/src/utils/injectFont.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { isEmpty } from '@typebot.io/lib'
|
||||
import { Font } from '@typebot.io/schemas'
|
||||
import { defaultTheme } from '@typebot.io/schemas/features/typebot/theme/constants'
|
||||
|
||||
const googleFontCdnBaseUrl = 'https://fonts.bunny.net/css2'
|
||||
|
||||
export const injectFont = (font: Font) => {
|
||||
const existingFont = document.getElementById('bot-font')
|
||||
|
||||
if (typeof font === 'string' || font.type === 'Google') {
|
||||
const fontFamily =
|
||||
(typeof font === 'string' ? font : font.family) ??
|
||||
defaultTheme.general.font.family
|
||||
if (existingFont?.getAttribute('href')?.includes(fontFamily)) return
|
||||
const fontElement = document.createElement('link')
|
||||
fontElement.href = `${googleFontCdnBaseUrl}?family=${fontFamily}:ital,wght@0,300;0,400;0,600;1,300;1,400;1,600&display=swap`
|
||||
fontElement.rel = 'stylesheet'
|
||||
fontElement.id = 'bot-font'
|
||||
document.head.appendChild(fontElement)
|
||||
return
|
||||
}
|
||||
|
||||
if (font.type === 'Custom') {
|
||||
if (existingFont?.getAttribute('href') === font.url || isEmpty(font.url))
|
||||
return
|
||||
const fontElement = document.createElement('link')
|
||||
fontElement.href = font.url
|
||||
fontElement.rel = 'stylesheet'
|
||||
fontElement.id = 'bot-font'
|
||||
document.head.appendChild(fontElement)
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,9 @@ const setGeneralTheme = (
|
||||
)
|
||||
documentStyle.setProperty(
|
||||
cssVariableNames.general.fontFamily,
|
||||
generalTheme.font ?? defaultTheme.general.font
|
||||
(typeof generalTheme.font === 'string'
|
||||
? generalTheme.font
|
||||
: generalTheme.font?.family) ?? defaultTheme.general.font.family
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ export enum BackgroundType {
|
||||
NONE = 'None',
|
||||
}
|
||||
|
||||
export const fontTypes = ['Google', 'Custom'] as const
|
||||
|
||||
export const defaultTheme = {
|
||||
chat: {
|
||||
roundness: 'medium',
|
||||
@@ -25,7 +27,10 @@ export const defaultTheme = {
|
||||
},
|
||||
},
|
||||
general: {
|
||||
font: 'Open Sans',
|
||||
font: {
|
||||
type: 'Google',
|
||||
family: 'Open Sans',
|
||||
},
|
||||
background: { type: BackgroundType.COLOR, content: '#ffffff' },
|
||||
},
|
||||
} as const satisfies Theme
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ThemeTemplate as ThemeTemplatePrisma } from '@typebot.io/prisma'
|
||||
import { z } from '../../../zod'
|
||||
import { BackgroundType } from './constants'
|
||||
import { BackgroundType, fontTypes } from './constants'
|
||||
|
||||
const avatarPropsSchema = z.object({
|
||||
isEnabled: z.boolean().optional(),
|
||||
@@ -33,8 +33,26 @@ const backgroundSchema = z.object({
|
||||
content: z.string().optional().optional(),
|
||||
})
|
||||
|
||||
const googleFontSchema = z.object({
|
||||
type: z.literal(fontTypes[0]),
|
||||
family: z.string().optional(),
|
||||
})
|
||||
export type GoogleFont = z.infer<typeof googleFontSchema>
|
||||
|
||||
const customFontSchema = z.object({
|
||||
type: z.literal(fontTypes[1]),
|
||||
family: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
})
|
||||
export type CustomFont = z.infer<typeof customFontSchema>
|
||||
|
||||
export const fontSchema = z
|
||||
.string()
|
||||
.or(z.discriminatedUnion('type', [googleFontSchema, customFontSchema]))
|
||||
export type Font = z.infer<typeof fontSchema>
|
||||
|
||||
const generalThemeSchema = z.object({
|
||||
font: z.string().optional(),
|
||||
font: fontSchema.optional(),
|
||||
background: backgroundSchema.optional(),
|
||||
})
|
||||
|
||||
@@ -56,7 +74,7 @@ export const themeTemplateSchema = z.object({
|
||||
workspaceId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
}) satisfies z.ZodType<ThemeTemplatePrisma>
|
||||
}) satisfies z.ZodType<Omit<ThemeTemplatePrisma, 'theme'>>
|
||||
|
||||
export type Theme = z.infer<typeof themeSchema>
|
||||
export type ChatTheme = z.infer<typeof chatThemeSchema>
|
||||
|
||||
Reference in New Issue
Block a user