Most Forkshop installs need only the built-ins: gallery and tree
layouts, plus the three Node kinds (inline-react, iframe-route,
iframe-component). For everything else, the engine exposes three
extension points.
Custom NodeType
A NodeType tells the engine how to render a kind of Node. To add
one, write an object that satisfies the NodeType<T> interface and
register it in forkshop.config.tsx.
import type { NodeType, AnyNode, RenderProps } from "@forkshop/engine"
type StorybookStoryNode = AnyNode & {
kind: "storybook-story"
storyId: string
}
export const storybookStoryNodeType: NodeType<StorybookStoryNode> = {
id: "storybook-story",
match: (node): node is StorybookStoryNode => node.kind === "storybook-story",
render: ({ node }: RenderProps<StorybookStoryNode>) => (
<iframe
src={`http://localhost:6006/?path=/story/${node.storyId}`}
style={{ width: "100%", height: "100%", border: 0 }}
/>
),
}
Register it in forkshop.config.tsx:
import { defineConfig, BUILTIN_NODE_TYPES } from "@forkshop/engine"
import { storybookStoryNodeType } from "./node-types/storybook-story-node-type"
export const forkshopConfig = defineConfig({
mount: "app/forkshop",
nodeTypes: [...BUILTIN_NODE_TYPES, storybookStoryNodeType],
// ...
})
<BoardRegistry> picks it up automatically and dispatches any Node
with kind: "storybook-story" to your custom render.
Custom Layout
A Layout arranges Nodes on the canvas. Two ship by id (gallery,
tree); add your own with defineLayout().
import { defineLayout, forkshopIcons, type Layout } from "@forkshop/engine"
export const radialLayout: Layout<{ radius: number }> = defineLayout({
id: "radial",
icon: forkshopIcons.flows,
defaultOptions: { radius: 300 },
stageSize: () => ({ width: 800, height: 800 }),
render: ({ entries, options, nodePositions, onPositionChange }) => {
// Compute positions around a circle of radius `options.radius`
// Render each entry at its computed (x, y) — use NodeView or
// your own React tree
return /* ... */
},
})
Register and reference in a Board:
import { defineConfig, BUILTIN_LAYOUTS } from "@forkshop/engine"
import { radialLayout } from "./layouts/radial"
export const forkshopConfig = defineConfig({
// ...
layouts: [...BUILTIN_LAYOUTS, radialLayout],
})
// In a Board:
import { defineBoard } from "@forkshop/engine"
import { radialLayout } from "./layouts/radial"
export default defineBoard({
id: "radial-demo",
match: (s) => s.kind === "section" && s.sectionId === "radial",
layout: radialLayout,
layoutOptions: { radius: 400 },
useEntries: () => /* ... */,
})
Escape hatch — withBoardMeta
When the useEntries + Layout model doesn't fit (e.g., a Board that
manages its own canvas via direct <ForkshopCanvas>), wrap a raw React
component:
import { withBoardMeta } from "@forkshop/engine"
const CustomBoard = withBoardMeta(
function MyBoard() {
return /* whatever JSX renders directly on the canvas */
},
{
id: "custom",
label: "Custom",
match: (s) => s.kind === "section" && s.sectionId === "custom",
},
)
<BoardRegistry> treats this like any other Board for sidebar
rendering and selection matching, but skips the Layout dispatch and
just renders your component.
Use sparingly — the canonical pattern is defineBoard() with a Layout.