The <Designer />
component is designed to be unopinionated about state management, making it easy to build multi-page editors.
The following example demonstrates how to implement a page-based workflow with persistent storage. Each page maintains its own set of layers, and you can seamlessly switch between pages using custom actions.
Pages
We define a Page
type to represent each page in the editor. A page has an id
, a name
, and a layers
array.
type Page = {
id: string
name: string
layers: Layer[]
}
We use Jotai to manage the state of the editor. We define a pagesAtom
to store the list of pages, and a selectedPageIdAtom
to store the ID of the currently selected page.
const INITIAL_PAGES = [
{
id: "1",
name: "Page 1",
layers: [],
},
]
const pagesAtom = atomWithStorage<Page[]>("pages", INITIAL_PAGES)
const selectedPageIdAtom = atomWithStorage<string>("selectedPageId", INITIAL_PAGES[0].id)
Designer
The <Designer />
component is used to create the editor. We use the layers
prop to pass the layers for the currently selected page and the onLayersChange
prop to update the layers for the currently selected page.
export function CustomDesigner() {
return (
<Designer layers={selectedPage?.layers} onLayersChange={handleLayersChange}>
<DesignerContent>
<DesignerCanvas>
<DesignerFrame />
</DesignerCanvas>
</DesignerContent>
<DesignerToolbar>
</Designer>
)
}
ActionPageSelector
function ActionPageSelector() {
const [pages] = useAtom(pageAtom)
const [selectedPageId, setSelectedPageId] = useAtom(selectedPageIdAtom)
return (
<Action>
<ActionLabel htmlFor="page-selector" className="sr-only">
Page
</ActionLabel>
<ActionControls>
<Select value={selectedPageId} onValueChange={setSelectedPageId}>
<SelectTrigger>
<SelectValue placeholder="Select a page" />
<SelectContent>
{pages.map((page) => (
<SelectItem key={page.id} value={page.id}>
{page.name}
</SelectItem>
))}
</SelectContent>
</SelectTrigger>
</Select>
</ActionControls>
</Action>
)
}
ActionAddPage
function ActionAddPage() {
const [pages, setPages] = useAtom(pageAtom)
const [, setSelectedPageId] = useAtom(selectedPageIdAtom)
return (
<Button
variant="ghost"
onClick={() => {
const newPage = {
id: `page-${pages.length + 1}`,
name: `Page ${pages.length + 1}`,
layers: [],
}
setPages([...pages, newPage])
setSelectedPageId(newPage.id)
}}
>
<IconPlus />
Add Page
</Button>
)
}
ActionReset
function ActionReset() {
const [, setPages] = useAtom(pageAtom)
const [, setSelectedPageId] = useAtom(selectedPageIdAtom)
return (
<Button
variant="outline"
onClick={() => {
setPages(INITIAL_PAGES)
setSelectedPageId("1")
}}
>
Reset
</Button>
)
}
ActionExportPages
function ActionExportPages() {
const [pages] = useAtom(pageAtom)
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="primary">Export</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>View JSON</DialogTitle>
<DialogDescription>
Here's the JSON representation of the current pages.
</DialogDescription>
</DialogHeader>
<div className="max-h-[50vh] overflow-y-auto rounded-md border bg-black p-2 font-mono text-white text-xs">
<pre>{JSON.stringify(pages, null, 2)}</pre>
</div>
<DialogFooter>
<DialogClose asChild>
<Button>Close</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)
}