✨(front) handle virtual background in custom processor
BackgroundBlurCustomProcessor is renamed to BackgroundCustomProcessor in order to reflect the fact that is now handles virtual backgrounds too. BackgroundBlurFactory is also renamed to BackgroundProcessorFactory. The processor serialization handling has also been updated in order to support various options, also if persisted in local storage.
This commit is contained in:
@@ -16,7 +16,7 @@ import { VideoConference } from '../livekit/prefabs/VideoConference'
|
|||||||
import posthog from 'posthog-js'
|
import posthog from 'posthog-js'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
import { LocalUserChoices } from '../routes/Room'
|
import { LocalUserChoices } from '../routes/Room'
|
||||||
import { BackgroundBlurFactory } from '../livekit/components/blur'
|
import { BackgroundProcessorFactory } from '../livekit/components/blur'
|
||||||
|
|
||||||
export const Conference = ({
|
export const Conference = ({
|
||||||
roomId,
|
roomId,
|
||||||
@@ -114,7 +114,7 @@ export const Conference = ({
|
|||||||
audio={userConfig.audioEnabled}
|
audio={userConfig.audioEnabled}
|
||||||
video={
|
video={
|
||||||
userConfig.videoEnabled && {
|
userConfig.videoEnabled && {
|
||||||
processor: BackgroundBlurFactory.deserializeProcessor(
|
processor: BackgroundProcessorFactory.deserializeProcessor(
|
||||||
userConfig.processorSerialized
|
userConfig.processorSerialized
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ import { LocalVideoTrack, Track } from 'livekit-client'
|
|||||||
import { H } from '@/primitives/H'
|
import { H } from '@/primitives/H'
|
||||||
import { SelectToggleDevice } from '../livekit/components/controls/SelectToggleDevice'
|
import { SelectToggleDevice } from '../livekit/components/controls/SelectToggleDevice'
|
||||||
import { Field } from '@/primitives/Field'
|
import { Field } from '@/primitives/Field'
|
||||||
import { Form } from '@/primitives'
|
import { Button, Dialog, Text, Form } from '@/primitives'
|
||||||
import { HStack, VStack } from '@/styled-system/jsx'
|
import { HStack, VStack } from '@/styled-system/jsx'
|
||||||
import { Button, Dialog } from '@/primitives'
|
|
||||||
import { LocalUserChoices } from '../routes/Room'
|
import { LocalUserChoices } from '../routes/Room'
|
||||||
import { Heading } from 'react-aria-components'
|
import { Heading } from 'react-aria-components'
|
||||||
import { RiImageCircleAiFill } from '@remixicon/react'
|
import { RiImageCircleAiFill } from '@remixicon/react'
|
||||||
@@ -18,7 +17,7 @@ import {
|
|||||||
EffectsConfigurationProps,
|
EffectsConfigurationProps,
|
||||||
} from '../livekit/components/effects/EffectsConfiguration'
|
} from '../livekit/components/effects/EffectsConfiguration'
|
||||||
import { usePersistentUserChoices } from '../livekit/hooks/usePersistentUserChoices'
|
import { usePersistentUserChoices } from '../livekit/hooks/usePersistentUserChoices'
|
||||||
import { BackgroundBlurFactory } from '../livekit/components/blur'
|
import { BackgroundProcessorFactory } from '../livekit/components/blur'
|
||||||
import { isMobileBrowser } from '@livekit/components-core'
|
import { isMobileBrowser } from '@livekit/components-core'
|
||||||
|
|
||||||
const onError = (e: Error) => console.error('ERROR', e)
|
const onError = (e: Error) => console.error('ERROR', e)
|
||||||
@@ -31,7 +30,7 @@ const Effects = ({
|
|||||||
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
const [isDialogOpen, setIsDialogOpen] = useState(false)
|
||||||
const openDialog = () => setIsDialogOpen(true)
|
const openDialog = () => setIsDialogOpen(true)
|
||||||
|
|
||||||
if (!BackgroundBlurFactory.isSupported() || isMobileBrowser()) {
|
if (!BackgroundProcessorFactory.isSupported() || isMobileBrowser()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,11 +48,19 @@ const Effects = ({
|
|||||||
level={1}
|
level={1}
|
||||||
className={css({
|
className={css({
|
||||||
textStyle: 'h1',
|
textStyle: 'h1',
|
||||||
marginBottom: '1.5rem',
|
marginBottom: '0.25rem',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{t('title')}
|
{t('title')}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
<Text
|
||||||
|
variant="subTitle"
|
||||||
|
className={css({
|
||||||
|
marginBottom: '1.5rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t('subTitle')}
|
||||||
|
</Text>
|
||||||
<EffectsConfiguration videoTrack={videoTrack} onSubmit={onSubmit} />
|
<EffectsConfiguration videoTrack={videoTrack} onSubmit={onSubmit} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<div
|
<div
|
||||||
@@ -105,6 +112,8 @@ export const Join = ({
|
|||||||
saveProcessorSerialized,
|
saveProcessorSerialized,
|
||||||
} = usePersistentUserChoices({})
|
} = usePersistentUserChoices({})
|
||||||
|
|
||||||
|
const [audioEnabled, setAudioEnabled] = useState(true)
|
||||||
|
const [videoEnabled, setVideoEnabled] = useState(true)
|
||||||
const [audioDeviceId, setAudioDeviceId] = useState<string>(
|
const [audioDeviceId, setAudioDeviceId] = useState<string>(
|
||||||
initialUserChoices.audioDeviceId
|
initialUserChoices.audioDeviceId
|
||||||
)
|
)
|
||||||
@@ -113,7 +122,7 @@ export const Join = ({
|
|||||||
)
|
)
|
||||||
const [username, setUsername] = useState<string>(initialUserChoices.username)
|
const [username, setUsername] = useState<string>(initialUserChoices.username)
|
||||||
const [processor, setProcessor] = useState(
|
const [processor, setProcessor] = useState(
|
||||||
BackgroundBlurFactory.deserializeProcessor(
|
BackgroundProcessorFactory.deserializeProcessor(
|
||||||
initialUserChoices.processorSerialized
|
initialUserChoices.processorSerialized
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -129,12 +138,15 @@ export const Join = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
saveUsername(username)
|
saveUsername(username)
|
||||||
}, [username, saveUsername])
|
}, [username, saveUsername])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
saveProcessorSerialized(processor?.serialize())
|
saveProcessorSerialized(processor?.serialize())
|
||||||
}, [processor, saveProcessorSerialized])
|
}, [
|
||||||
|
processor,
|
||||||
const [audioEnabled, setAudioEnabled] = useState(true)
|
saveProcessorSerialized,
|
||||||
const [videoEnabled, setVideoEnabled] = useState(true)
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
JSON.stringify(processor?.serialize()),
|
||||||
|
])
|
||||||
|
|
||||||
const tracks = usePreviewTracks(
|
const tracks = usePreviewTracks(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
} from '@livekit/track-processors'
|
} from '@livekit/track-processors'
|
||||||
import { ProcessorOptions, Track } from 'livekit-client'
|
import { ProcessorOptions, Track } from 'livekit-client'
|
||||||
import {
|
import {
|
||||||
BackgroundBlurProcessorInterface,
|
BackgroundProcessorInterface,
|
||||||
BackgroundOptions,
|
BackgroundOptions,
|
||||||
ProcessorType,
|
ProcessorType,
|
||||||
} from '.'
|
} from '.'
|
||||||
@@ -16,9 +16,9 @@ import {
|
|||||||
* used accross the project.
|
* used accross the project.
|
||||||
*/
|
*/
|
||||||
export class BackgroundBlurTrackProcessorJsWrapper
|
export class BackgroundBlurTrackProcessorJsWrapper
|
||||||
implements BackgroundBlurProcessorInterface
|
implements BackgroundProcessorInterface
|
||||||
{
|
{
|
||||||
name: string = 'Blur'
|
name: string = 'blur'
|
||||||
|
|
||||||
processor: ProcessorWrapper<BackgroundOptions>
|
processor: ProcessorWrapper<BackgroundOptions>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
timerWorkerScript,
|
timerWorkerScript,
|
||||||
} from './TimerWorker'
|
} from './TimerWorker'
|
||||||
import {
|
import {
|
||||||
BackgroundBlurProcessorInterface,
|
BackgroundProcessorInterface,
|
||||||
BackgroundOptions,
|
BackgroundOptions,
|
||||||
ProcessorType,
|
ProcessorType,
|
||||||
} from '.'
|
} from '.'
|
||||||
@@ -32,9 +32,7 @@ const DEFAULT_BLUR = '10'
|
|||||||
* It also make possible to run blurring on browser that does not implement MediaStreamTrackGenerator and
|
* It also make possible to run blurring on browser that does not implement MediaStreamTrackGenerator and
|
||||||
* MediaStreamTrackProcessor.
|
* MediaStreamTrackProcessor.
|
||||||
*/
|
*/
|
||||||
export class BackgroundBlurCustomProcessor
|
export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
|
||||||
implements BackgroundBlurProcessorInterface
|
|
||||||
{
|
|
||||||
options: BackgroundOptions
|
options: BackgroundOptions
|
||||||
name: string
|
name: string
|
||||||
processedTrack?: MediaStreamTrack | undefined
|
processedTrack?: MediaStreamTrack | undefined
|
||||||
@@ -63,9 +61,18 @@ export class BackgroundBlurCustomProcessor
|
|||||||
|
|
||||||
timerWorker?: Worker
|
timerWorker?: Worker
|
||||||
|
|
||||||
|
type: ProcessorType
|
||||||
|
virtualBackgroundImage?: HTMLImageElement
|
||||||
|
|
||||||
constructor(opts: BackgroundOptions) {
|
constructor(opts: BackgroundOptions) {
|
||||||
this.name = 'blur'
|
this.name = 'blur'
|
||||||
this.options = opts
|
this.options = opts
|
||||||
|
|
||||||
|
if (this.options.blurRadius) {
|
||||||
|
this.type = ProcessorType.BLUR
|
||||||
|
} else {
|
||||||
|
this.type = ProcessorType.VIRTUAL
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static get isSupported() {
|
static get isSupported() {
|
||||||
@@ -81,6 +88,7 @@ export class BackgroundBlurCustomProcessor
|
|||||||
this.sourceSettings = this.source!.getSettings()
|
this.sourceSettings = this.source!.getSettings()
|
||||||
this.videoElement = opts.element as HTMLVideoElement
|
this.videoElement = opts.element as HTMLVideoElement
|
||||||
|
|
||||||
|
this._initVirtualBackgroundImage()
|
||||||
this._createMainCanvas()
|
this._createMainCanvas()
|
||||||
this._createMaskCanvas()
|
this._createMaskCanvas()
|
||||||
|
|
||||||
@@ -98,8 +106,21 @@ export class BackgroundBlurCustomProcessor
|
|||||||
posthog.capture('firefox-blurring-init')
|
posthog.capture('firefox-blurring-init')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_initVirtualBackgroundImage() {
|
||||||
|
const needsUpdate =
|
||||||
|
this.options.imagePath &&
|
||||||
|
this.virtualBackgroundImage &&
|
||||||
|
this.virtualBackgroundImage.src !== this.options.imagePath
|
||||||
|
if (this.options.imagePath || needsUpdate) {
|
||||||
|
this.virtualBackgroundImage = document.createElement('img')
|
||||||
|
this.virtualBackgroundImage.crossOrigin = 'anonymous'
|
||||||
|
this.virtualBackgroundImage.src = this.options.imagePath!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
update(opts: BackgroundOptions): void {
|
update(opts: BackgroundOptions): void {
|
||||||
this.options = opts
|
this.options = opts
|
||||||
|
this._initVirtualBackgroundImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
_initWorker() {
|
_initWorker() {
|
||||||
@@ -201,6 +222,7 @@ export class BackgroundBlurCustomProcessor
|
|||||||
this.outputCanvasCtx!.globalCompositeOperation = 'copy'
|
this.outputCanvasCtx!.globalCompositeOperation = 'copy'
|
||||||
this.outputCanvasCtx!.filter = 'blur(8px)'
|
this.outputCanvasCtx!.filter = 'blur(8px)'
|
||||||
|
|
||||||
|
// Put opacity mask.
|
||||||
this.outputCanvasCtx!.drawImage(
|
this.outputCanvasCtx!.drawImage(
|
||||||
this.segmentationMaskCanvas!,
|
this.segmentationMaskCanvas!,
|
||||||
0,
|
0,
|
||||||
@@ -213,19 +235,69 @@ export class BackgroundBlurCustomProcessor
|
|||||||
this.videoElement!.videoHeight
|
this.videoElement!.videoHeight
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Draw clear body.
|
||||||
this.outputCanvasCtx!.globalCompositeOperation = 'source-in'
|
this.outputCanvasCtx!.globalCompositeOperation = 'source-in'
|
||||||
this.outputCanvasCtx!.filter = 'none'
|
this.outputCanvasCtx!.filter = 'none'
|
||||||
this.outputCanvasCtx!.drawImage(this.videoElement!, 0, 0)
|
this.outputCanvasCtx!.drawImage(this.videoElement!, 0, 0)
|
||||||
|
|
||||||
|
// Draw blurry background.
|
||||||
this.outputCanvasCtx!.globalCompositeOperation = 'destination-over'
|
this.outputCanvasCtx!.globalCompositeOperation = 'destination-over'
|
||||||
this.outputCanvasCtx!.filter = `blur(${this.options.blurRadius ?? DEFAULT_BLUR}px)`
|
this.outputCanvasCtx!.filter = `blur(${this.options.blurRadius ?? DEFAULT_BLUR}px)`
|
||||||
this.outputCanvasCtx!.drawImage(this.videoElement!, 0, 0)
|
this.outputCanvasCtx!.drawImage(this.videoElement!, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: future improvement with WebGL.
|
||||||
|
*/
|
||||||
|
async drawVirtualBackground() {
|
||||||
|
const mask = this.imageSegmenterResult!.categoryMask!.getAsUint8Array()
|
||||||
|
for (let i = 0; i < mask.length; ++i) {
|
||||||
|
this.segmentationMask!.data[i * 4 + 3] = 255 - mask[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.segmentationMaskCanvasCtx!.putImageData(this.segmentationMask!, 0, 0)
|
||||||
|
|
||||||
|
this.outputCanvasCtx!.globalCompositeOperation = 'copy'
|
||||||
|
this.outputCanvasCtx!.filter = 'blur(8px)'
|
||||||
|
|
||||||
|
// Put opacity mask.
|
||||||
|
this.outputCanvasCtx!.drawImage(
|
||||||
|
this.segmentationMaskCanvas!,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
PROCESSING_WIDTH,
|
||||||
|
PROCESSING_HEIGHT,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
this.videoElement!.videoWidth,
|
||||||
|
this.videoElement!.videoHeight
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw clear body.
|
||||||
|
this.outputCanvasCtx!.globalCompositeOperation = 'source-in'
|
||||||
|
this.outputCanvasCtx!.filter = 'none'
|
||||||
|
this.outputCanvasCtx!.drawImage(this.videoElement!, 0, 0)
|
||||||
|
|
||||||
|
// Draw virtual background.
|
||||||
|
this.outputCanvasCtx!.globalCompositeOperation = 'destination-over'
|
||||||
|
this.outputCanvasCtx!.drawImage(
|
||||||
|
this.virtualBackgroundImage!,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
this.outputCanvas!.width,
|
||||||
|
this.outputCanvas!.height
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async process() {
|
async process() {
|
||||||
await this.sizeSource()
|
await this.sizeSource()
|
||||||
await this.segment()
|
await this.segment()
|
||||||
await this.blur()
|
|
||||||
|
if (this.options.blurRadius) {
|
||||||
|
await this.blur()
|
||||||
|
} else {
|
||||||
|
await this.drawVirtualBackground()
|
||||||
|
}
|
||||||
this.timerWorker!.postMessage({
|
this.timerWorker!.postMessage({
|
||||||
id: SET_TIMEOUT,
|
id: SET_TIMEOUT,
|
||||||
timeMs: 1000 / 30,
|
timeMs: 1000 / 30,
|
||||||
@@ -284,12 +356,12 @@ export class BackgroundBlurCustomProcessor
|
|||||||
}
|
}
|
||||||
|
|
||||||
clone() {
|
clone() {
|
||||||
return new BackgroundBlurCustomProcessor(this.options)
|
return new BackgroundCustomProcessor(this.options)
|
||||||
}
|
}
|
||||||
|
|
||||||
serialize() {
|
serialize() {
|
||||||
return {
|
return {
|
||||||
type: ProcessorType.BLUR,
|
type: this.type,
|
||||||
options: this.options,
|
options: this.options,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { ProcessorWrapper } from '@livekit/track-processors'
|
import { ProcessorWrapper } from '@livekit/track-processors'
|
||||||
import { Track, TrackProcessor } from 'livekit-client'
|
import { Track, TrackProcessor } from 'livekit-client'
|
||||||
import { BackgroundBlurTrackProcessorJsWrapper } from './BackgroundBlurTrackProcessorJsWrapper'
|
import { BackgroundBlurTrackProcessorJsWrapper } from './BackgroundBlurTrackProcessorJsWrapper'
|
||||||
import { BackgroundBlurCustomProcessor } from './BackgroundBlurCustomProcessor'
|
import { BackgroundCustomProcessor } from './BackgroundCustomProcessor'
|
||||||
|
import { BackgroundVirtualTrackProcessorJsWrapper } from './BackgroundVirtualTrackProcessorJsWrapper'
|
||||||
|
|
||||||
export type BackgroundOptions = {
|
export type BackgroundOptions = {
|
||||||
blurRadius?: number
|
blurRadius?: number
|
||||||
|
imagePath?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProcessorSerialized {
|
export interface ProcessorSerialized {
|
||||||
@@ -12,40 +14,49 @@ export interface ProcessorSerialized {
|
|||||||
options: BackgroundOptions
|
options: BackgroundOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BackgroundBlurProcessorInterface
|
export interface BackgroundProcessorInterface
|
||||||
extends TrackProcessor<Track.Kind> {
|
extends TrackProcessor<Track.Kind> {
|
||||||
update(opts: BackgroundOptions): void
|
update(opts: BackgroundOptions): void
|
||||||
options: BackgroundOptions
|
options: BackgroundOptions
|
||||||
clone(): BackgroundBlurProcessorInterface
|
clone(): BackgroundProcessorInterface
|
||||||
serialize(): ProcessorSerialized
|
serialize(): ProcessorSerialized
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ProcessorType {
|
export enum ProcessorType {
|
||||||
BLUR = 'blur',
|
BLUR = 'blur',
|
||||||
|
VIRTUAL = 'virtual',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BackgroundBlurFactory {
|
export class BackgroundProcessorFactory {
|
||||||
static isSupported() {
|
static isSupported() {
|
||||||
return (
|
return ProcessorWrapper.isSupported || BackgroundCustomProcessor.isSupported
|
||||||
ProcessorWrapper.isSupported || BackgroundBlurCustomProcessor.isSupported
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getProcessor(
|
static getProcessor(
|
||||||
|
type: ProcessorType,
|
||||||
opts: BackgroundOptions
|
opts: BackgroundOptions
|
||||||
): BackgroundBlurProcessorInterface | undefined {
|
): BackgroundProcessorInterface | undefined {
|
||||||
if (ProcessorWrapper.isSupported) {
|
if (type === ProcessorType.BLUR) {
|
||||||
return new BackgroundBlurTrackProcessorJsWrapper(opts)
|
if (ProcessorWrapper.isSupported) {
|
||||||
}
|
return new BackgroundBlurTrackProcessorJsWrapper(opts)
|
||||||
if (BackgroundBlurCustomProcessor.isSupported) {
|
}
|
||||||
return new BackgroundBlurCustomProcessor(opts)
|
if (BackgroundCustomProcessor.isSupported) {
|
||||||
|
return new BackgroundCustomProcessor(opts)
|
||||||
|
}
|
||||||
|
} else if (type === ProcessorType.VIRTUAL) {
|
||||||
|
if (ProcessorWrapper.isSupported) {
|
||||||
|
return new BackgroundVirtualTrackProcessorJsWrapper(opts)
|
||||||
|
}
|
||||||
|
if (BackgroundCustomProcessor.isSupported) {
|
||||||
|
return new BackgroundCustomProcessor(opts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
static deserializeProcessor(data?: ProcessorSerialized) {
|
static deserializeProcessor(data?: ProcessorSerialized) {
|
||||||
if (data?.type === ProcessorType.BLUR) {
|
if (data?.type) {
|
||||||
return BackgroundBlurFactory.getProcessor(data?.options)
|
return BackgroundProcessorFactory.getProcessor(data?.type, data?.options)
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { css } from '@/styled-system/css'
|
|||||||
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
import { ButtonRecipeProps } from '@/primitives/buttonRecipe'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { usePersistentUserChoices } from '../../hooks/usePersistentUserChoices'
|
import { usePersistentUserChoices } from '../../hooks/usePersistentUserChoices'
|
||||||
import { BackgroundBlurFactory } from '../blur'
|
import { BackgroundProcessorFactory } from '../blur'
|
||||||
|
|
||||||
export type ToggleSource = Exclude<
|
export type ToggleSource = Exclude<
|
||||||
Track.Source,
|
Track.Source,
|
||||||
@@ -115,7 +115,7 @@ export const SelectToggleDevice = <T extends ToggleSource>({
|
|||||||
*
|
*
|
||||||
* See https://github.com/numerique-gouv/meet/pull/309#issuecomment-2622404121
|
* See https://github.com/numerique-gouv/meet/pull/309#issuecomment-2622404121
|
||||||
*/
|
*/
|
||||||
const processor = BackgroundBlurFactory.deserializeProcessor(
|
const processor = BackgroundProcessorFactory.deserializeProcessor(
|
||||||
userChoices.processorSerialized
|
userChoices.processorSerialized
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ export const text = cva({
|
|||||||
paddingTop: 'heading',
|
paddingTop: 'heading',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
subTitle: {
|
||||||
|
fontSize: '1rem',
|
||||||
|
color: 'greyscale.600',
|
||||||
|
},
|
||||||
|
bodyXsBold: {
|
||||||
|
textStyle: 'body',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
body: {
|
body: {
|
||||||
textStyle: 'body',
|
textStyle: 'body',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user