-
-
Notifications
You must be signed in to change notification settings - Fork 30
[SDK] UI Components
⚠️ Note: Do not import components outside of the @/sdk/components
folder. This does not refer to your own components that you create in your template folder.
All components are imported from @/sdk/components
.
A comprehensive component that allows users to select and input their own colors, gradients, and background images.
You can also enable or disable options by passing them in the mode
prop. The available modes are solid
, gradient
and image
.
If you pass also it an uploadBackground
function, users will be able to upload their own images and use as backgrounds.
The upload functionality is not handled by the component, you must build it yourself using the uploadImage
function. This is to enable more dynamic behaviors, as you may want to do some additional processing of the uploaded image before it is used, or you may want to use the image in other places in the Frame too.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { ColorPicker } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<ColorPicker
className="w-full"
background={config.bg || 'black'}
setBackground={(value) => updateConfig({ bg: value })}
uploadBackground={async (base64String, contentType) => {
const { filePath } = await uploadImage({
frameId: frameId,
base64String: base64String,
contentType: contentType,
})
return filePath
}}
/>
</Configuration.Section>
</Configuration.Root>
)
}
A selector component that allows users to select a font family from a predefined list of Google Fonts supported by FrameTrain.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { FontFamilyPicker } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<FontFamilyPicker
defaultValue={tweet.fontFamily}
onSelect={(font) => {
// An example where we change the font family for a tweet
const newTweets = config?.tweets?.map((t: any) =>
t.link === tweet.link
? {
...t,
fontFamily: font,
}
: t
)
updateConfig({ tweets: newTweets })
}}
/>
</Configuration.Section>
</Configuration.Root>
)
}
A selector component that allows users to select a font weight from a predefined list.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { FontStylePicker } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<FontWeightPicker
onSelect={(weight) =>
updateConfig({
title: {
...config.title,
fontWeight: weight,
},
})
}
/>
</Configuration.Section>
</Configuration.Root>
)
}
A selector component that allows users to select a font style from a predefined list.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { FontWeightPicker } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<FontStylePicker
defaultValue={config?.title?.fontStyle || 'normal'}
onSelect={(style) =>
updateConfig({
title: {
...config.title,
fontStyle: style,
},
})
}
/>
</Configuration.Section>
</Configuration.Root>
)
}
A pre-made component that allows Users to configure gating options.
You can have different gating options for one View
, and different options for another View
. To achieve this simply create different objects in your config
that stores the gating options for each View
. Afterwards make sure you pass the right object to the GatingInspector
component and to its runGatingChecks
counterpart.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig, useFarcasterId } from '@/sdk/hooks'
import { GatingInspector } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
const fid = useFarcasterId()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<GatingInspector
config={config.gating}
fid={fid}
// disabledOptions={[]} optional, if you want to disable some gating options
onUpdate={(newConfig) => {
updateConfig({ gating: newConfig }) // we are updating the **`gating`** property in the **`config`**
}}
/>
</Configuration.Section>
</Configuration.Root>
)
}
import { runGatingChecks } from '@/lib/gating'
export default function myHandler({
body,
config,
storage,
}: {
body: FramePayloadValidated
config: Config
storage: Storage
}): Promise<BuildFrameData> {
runGatingChecks(body, config.gating)
...
}
To learn more about the gating functionality in FrameTrain, see the Gating section.
This component is the Inspector counterpart of the BasicView
. You use the BasicViewInspector
to let the user configure the BasicView
customization settings. Both the View, as well as the Inspector for it, have been created in order for you to not have to write the same code over and over again.
There is no limit to the number of BasicViewInspector
s you can have in your Inspector, it depends on how many BasicViews you have in your template for the User to configure.
The BasicViewInspector
component takes in a config
parameter and an onUpdate
function. The onUpdate
function is called when the User changes the configuration. The config
parameter is the configuration of this specific BasicView
.
Do not confuse the config
parameter of the BasicViewInspector
with the root config
of your template (from the useFrameConfig
hook).
As a general rule, even if you don't use the BasicViewInspector
and BasicView
components, the way you structure your config
is the same. Each View
that has customization options needs to have its own key/object in the root config
object.
For example, if you have 2 Views (say Cover and Page) and both of them are customizable, you structure your config
as follows:
config: {
cover: {
...
},
page: {
...
}
...rest of config
}
The power of the BasicViewInspector
and its View
counterpart is that you only have to feed it the correct config
object, and then do the same in your Handler to the BasicView
component. It will take care of the rest.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { BasicViewInspector } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<BasicViewInspector
config={config.cover}
initialConfig={initialConfig.cover}
onUpdate={(newConfig) => {
updateConfig({ cover: newConfig }) // we are updating the **`cover`** property in the **`config`**
}}
/>
</Configuration.Section>
</Configuration.Root>
)
}
import { BasicView } from '@/sdk/views'
export default function myHandler({
}) {
...
return {
...
component: BasicView(config.cover), // the same object as the one passed to the BasicViewInspector
}
}
A native html
Select
component, that is displayed as a native element on mobile devices such as iOS
and Android
.
It is used the same way as select
in HTML, specifying options using the option
element.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Select } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<Select
defaultValue={config.selectedMeme}
placeholder="Select meme template"
onChange={(newSelection) => {
updateConfig({
selectedMeme: newSelection,
})
}}
>
{memeTemplates.map((meme) => (
<option key={meme.id} value={meme.id}>
{meme.name}
</option>
))}
</Select>
</Configuration.Section>
</Configuration.Root>
)
}
A button component with 3 variants
(primary
, secondary
and destructive
).
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Button } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<Button
variant="destructive"
onClick={() => {
updateConfig({
myObject: { ...config.myObject, image: null },
})
}}
>
Remove
</Button>
</Configuration.Section>
</Configuration.Root>
)
}
A checkbox component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Checkbox } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<Checkbox
defaultChecked={config.myValue || defaultConfig.myValue}
onCheckedChange={(newValue) => {
onChange({ myValue: newValue })
}}
/>
</Configuration.Section>
</Configuration.Root>
)
}
A label component, used in combination with other input-like components.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Label } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<div className="flex items-center space-x-2">
<RadioGroup.Item value="image" id="image" />
<Label htmlFor="image">Image</Label>
</div>
</Configuration.Section>
</Configuration.Root>
)
}
An iOS style toggle component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Switch } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<Switch
checked={config.myValue}
onCheckedChange={(newValue) => {
updateConfig({
myValue: newValue,
})
}}
/>
</Configuration.Section>
</Configuration.Root>
)
}
A slider component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Slider } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<Slider
defaultValue={config.myValue || [1]} //! WTF
max={140}
step={2}
onValueChange={(newRange) => {
const value = newRange[0]
setPairNameFontSize(fontSize)
updateConfig({
myValue: value,
})
}}
/>
</Configuration.Section>
</Configuration.Root>
)
}
A textarea component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Textarea } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<Textarea
defaultValue={config.myValue}
placeholder={'Placeholder text'}
onChange={(e) => {
updateConfig({
myValue: e.target.value,
})
}}
/>
</Configuration.Section>
</Configuration.Root>
)
}
An input component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Input } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<Input
defaultValue={1}
type="number"
required={true}
placeholder="4"
max={4}
min={2}
onChange={(e) => {}}
/>
</Configuration.Section>
</Configuration.Root>
)
}
A toggle component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Toggle } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
</Configuration.Section>
</Configuration.Root>
)
}
A badge/chip component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Badge } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<Badge
variant={'outline'}
onClick={() => {}}
>
My Badge
</Badge>
</Configuration.Section>
</Configuration.Root>
)
}
A separator component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Separator } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<Separator />
</Configuration.Section>
</Configuration.Root>
)
}
An avatar component. Can be used both with text and images.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Separator } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<Avatar.Root style={{ width: 36, height: 36 }}>
<Avatar.Image src={'https://your-image-url'} />
<AvatarFallback>FB</AvatarFallback>
</Avatar.Root>
</Configuration.Section>
</Configuration.Root>
)
}
A modal component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Separator } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<Modal.Root>
<Modal.Trigger asChild={true}>
<Button>Show Submissions</Button>
</Modal.Trigger>
<Modal.Content>
<Modal.Header>
<Modal.Title>My Modal Title</Modal.Title>
<Modal.Description>My Modal Content</Modal.Description>
</Modal.Header>
</Modal.Content>
</Modal.Root>
</Configuration.Section>
</Configuration.Root>
)
}
A table component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Separator } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
</Configuration.Section>
</Configuration.Root>
)
}
A tabs component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Separator } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<Tabs.Root defaultValue="tabOne" className="w-full">
<Tabs.List className="grid w-full grid-cols-3">
<Tabs.Trigger value="tabOne">Tab One</Tabs.Trigger>
<Tabs.Trigger value="tabTwo">Tab Two</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tabOne">
</Tabs.Content>
<Tabs.Content value="tabTwo">
</Tabs.Content>
</Tabs.Root>
</Configuration.Section>
</Configuration.Root>
)
}
An alert dialog component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Separator } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<AlertDialog.Root>
<AlertDialog.Trigger>
<Button>Press to open Alert</Button>
</AlertDialog.Trigger>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>Resolution Notice</AlertDialog.Title>
<AlertDialog.Description className="flex flex-col gap-4">
Your description
<div className="flex flex-row items-center gap-2">
<Checkbox
id="understood"
onCheckedChange={(e) => {}}
/>
<Label htmlFor="understood">
Don't show again.
</Label>
</div>
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
<AlertDialog.Cancel>Back</AlertDialog.Cancel>
<AlertDialog.Action
onClick={() => {}}
>
Understood
</AlertDialog.Action>
</AlertDialog.Footer>
</AlertDialog.Content>
</AlertDialog.Root>
</Configuration.Section>
</Configuration.Root>
)
}
A radio group component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { RadioGroup } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<RadioGroup.Root
defaultValue={coverType}
className="flex flex-row"
onValueChange={(val) => {
const value = val as 'image' | 'text'
setCoverType(value)
if (val === 'text' && config.cover.image) {
updateConfig({
cover: {
...config.cover,
image: null,
},
})
}
}}
>
<div className="flex items-center">
<RadioGroup.Item value="text" id="text" />
<Label htmlFor="text">Text</Label>
</div>
<div className="flex items-center">
<RadioGroup.Item value="image" id="image" />
<Label htmlFor="image">Image</Label>
</div>
</RadioGroup.Root>
</Configuration.Section>
</Configuration.Root>
)
}
A toggle group component.
import { Configuration } from '@/sdk/inspector'
import { useFrameConfig } from '@/sdk/hooks'
import { Separator } from '@/sdk/components'
export default function Inspector() {
const [config, updateConfig] = useFrameConfig<Config>()
return (
<Configuration.Root>
<Configuration.Section title="My Section">
<ToggleGroup.Root
type="single"
defaultValue={config.pool.primary}
>
<ToggleGroup.Item
value="token0"
onClick={() => {
updateConfig({
pool: {
...config.pool,
primary: 'token0',
},
})
}}
>
{config.pool.token0.symbol}
</ToggleGroup.Item>
<ToggleGroup.Item
value="token1"
onClick={() => {
updateConfig({
pool: {
...config.pool,
primary: 'token1',
},
})
}}
>
{config.pool.token1.symbol}
</ToggleGroup.Item>
</ToggleGroup.Root>
</Configuration.Section>
</Configuration.Root>
)
}