Reusable React split-pane layout package for applications that need persistent, nested panel layouts without baking app-specific content into the layout engine.
panoweave.mp4
- Recursive split-tree layout model
- Registry-driven panels
- Resize handles with nested split support
- Corner-handle split creation and collapse behavior
- Optional persisted layout state via Zustand
- Package-owned CSS entrypoint at
paneweave/styles.css
paneweave reflects how the library composes UI panes into a woven, recursive split tree.
Install the package and import its stylesheet in the consuming app.
import 'paneweave/styles.css'
import { PaneweaveLayout, type LayoutNode, type PanelDefinition } from 'paneweave'
import { createLayoutStore } from 'paneweave/zustand'import 'paneweave/styles.css'
import {
PaneweaveLayout,
type LayoutNode,
type PanelDefinition,
} from 'paneweave'
import { createLayoutStore } from 'paneweave/zustand'
type PanelId = 'canvas' | 'inspector' | 'assets'
type AppContext = {
projectName: string
}
const initialLayout: LayoutNode<PanelId> = {
type: 'split',
id: 'root',
direction: 'vertical',
ratio: 0.7,
first: { type: 'leaf', id: 'canvas-pane', panelId: 'canvas' },
second: {
type: 'split',
id: 'sidebar',
direction: 'horizontal',
ratio: 0.5,
first: { type: 'leaf', id: 'inspector-pane', panelId: 'inspector' },
second: { type: 'leaf', id: 'assets-pane', panelId: 'assets' },
},
}
const layoutStore = createLayoutStore({
initialLayout,
persistence: {
storageKey: 'my-app-layout',
},
})
const panelDefinitions: readonly PanelDefinition<PanelId, AppContext>[] = [
{
id: 'canvas',
label: 'Canvas',
render: ({ context }) => <div>Canvas for {context.projectName}</div>,
},
{
id: 'inspector',
label: 'Inspector',
render: () => <div>Inspector</div>,
},
{
id: 'assets',
label: 'Assets',
render: () => <div>Assets</div>,
},
]
export function ExampleApp() {
return (
<PaneweaveLayout
store={layoutStore}
panelDefinitions={panelDefinitions}
panelContext={{ projectName: 'Demo' }}
/>
)
}You can use paneweave with Redux by creating a small adapter that matches the LayoutStore contract.
import 'paneweave/styles.css'
import {
PaneweaveLayout,
type LayoutNode,
type LayoutStore,
type LayoutStoreState,
type PanelDefinition,
type SplitDirection,
type SplitResult,
} from 'paneweave'
import { combineReducers, createStore } from 'redux'
import { Provider, useSelector } from 'react-redux'
type PanelId = 'canvas' | 'inspector' | 'assets'
type AppContext = { projectName: string }
const initialLayout: LayoutNode<PanelId> = {
type: 'split',
id: 'root',
direction: 'vertical',
ratio: 0.7,
first: { type: 'leaf', id: 'canvas-pane', panelId: 'canvas' },
second: { type: 'leaf', id: 'inspector-pane', panelId: 'inspector' },
}
type LayoutAction =
| { type: 'layout/set'; layout: LayoutNode<PanelId> }
| { type: 'layout/resize'; splitId: string; ratio: number }
function updateNode(
node: LayoutNode<PanelId>,
id: string,
updater: (node: LayoutNode<PanelId>) => LayoutNode<PanelId>,
): LayoutNode<PanelId> {
if (node.id === id) return updater(node)
if (node.type === 'split') {
return {
...node,
first: updateNode(node.first, id, updater),
second: updateNode(node.second, id, updater),
}
}
return node
}
function layoutReducer(
state: LayoutNode<PanelId> = initialLayout,
action: LayoutAction,
): LayoutNode<PanelId> {
switch (action.type) {
case 'layout/set':
return action.layout
case 'layout/resize':
return updateNode(state, action.splitId, (node) =>
node.type === 'split'
? { ...node, ratio: Math.max(0, Math.min(1, action.ratio)) }
: node,
)
default:
return state
}
}
const rootReducer = combineReducers({
layout: layoutReducer,
})
const reduxStore = createStore(rootReducer)
const layoutActions = {
setAreaPanel(areaId: string, panelId: PanelId) {
const current = reduxStore.getState().layout
const next = updateNode(current, areaId, (node) =>
node.type === 'leaf' ? { ...node, panelId } : node,
)
reduxStore.dispatch({ type: 'layout/set', layout: next })
},
splitArea(_areaId: string, _direction: SplitDirection): SplitResult | null {
// Implement split logic in your app reducer. Returning null keeps this minimal example focused.
return null
},
joinArea(_keepId: string) {},
removeArea(_targetId: string) {},
resizeSplit(splitId: string, ratio: number) {
reduxStore.dispatch({ type: 'layout/resize', splitId, ratio })
},
resetLayout() {
reduxStore.dispatch({ type: 'layout/set', layout: initialLayout })
},
moveArea(_sourceId: string, _targetId: string, _direction: SplitDirection, _insertBefore: boolean) {},
}
const useReduxLayoutStore = Object.assign(
<TSelected,>(selector: (state: LayoutStoreState<PanelId>) => TSelected): TSelected => {
const layout = useSelector((state: ReturnType<typeof rootReducer>) => state.layout)
return selector({
layout,
...layoutActions,
})
},
{
getState: (): LayoutStoreState<PanelId> => {
const layout = reduxStore.getState().layout
return {
layout,
...layoutActions,
}
},
setState: (partial: Partial<LayoutStoreState<PanelId>>) => {
if (partial.layout) {
reduxStore.dispatch({ type: 'layout/set', layout: partial.layout })
}
},
},
) as LayoutStore<PanelId>
const panelDefinitions: readonly PanelDefinition<PanelId, AppContext>[] = [
{
id: 'canvas',
label: 'Canvas',
render: ({ context }) => <div>Canvas for {context.projectName}</div>,
},
{
id: 'inspector',
label: 'Inspector',
render: () => <div>Inspector</div>,
},
{
id: 'assets',
label: 'Assets',
render: () => <div>Assets</div>,
},
]
export function AppWithRedux() {
return (
<Provider store={reduxStore}>
<PaneweaveLayout
store={useReduxLayoutStore}
panelDefinitions={panelDefinitions}
panelContext={{ projectName: 'Redux Demo' }}
/>
</Provider>
)
}Install Redux dependencies in your app:
npm install redux react-reduxIf you host multiple UI libraries on one page and want near-zero class collision risk, use a unique class prefix per app/shell.
PaneweaveLayout supports:
classNamePrefix: custom DOM class prefix for all internal nodes.includeDefaultClassNames: includepaneweave-layout-*classes in addition to your custom prefix.
Example:
<PaneweaveLayout
store={layoutStore}
panelDefinitions={panelDefinitions}
panelContext={ctx}
classNamePrefix="acme-paneweave-7f3a"
includeDefaultClassNames={false}
/>AI implementation guidance:
- Generate a unique, stable prefix per product shell (example:
acme-paneweave-7f3a). - Pass that prefix through
classNamePrefixon everyPaneweaveLayoutinstance. - Set
includeDefaultClassNames={false}in strict-isolation deployments. - Keep theme overrides on instance root class (or wrapper) using
--pw-*tokens. - If legacy overrides rely on
paneweave-layout-*, temporarily setincludeDefaultClassNames={true}during migration.
Optional helper:
import { createPaneweaveDomClassNames } from 'paneweave'
const classes = createPaneweaveDomClassNames({
classNamePrefix: 'acme-paneweave-7f3a',
includeDefaultClassNames: false,
})Standalone example projects live in examples/.
examples/zustand-react: Vite + React + TypeScript sample that consumes the library source and demonstrates persisted nested layouts. Demo: https://paneweave-zustand.vercel.appexamples/redux-react: Vite + React + TypeScript sample that uses a Redux-backed store adapter instead ofpaneweave/zustand. Demo: https://paneweave-redux.vercel.appexamples/vanilla-react: Vite + React + TypeScript sample that uses a custom store adapter with no external state library. Demo: https://paneweave-vanilla.vercel.app
Run it with:
cd examples/zustand-react
npm install
npm run dev- Agent/contributor quickstart and task routing:
CLAUDE.md - Repo-level guardrails for coding agents:
CLAUDE.md
PaneweaveLayout: top-level React component that renders the nested split tree.createLayoutStore: creates the Zustand store used by the layout component.LayoutNode,LayoutLeaf,LayoutSplit: generic tree types for serialized layout state.PanelDefinition: registry record containing the panel id, label, optional className, and renderer.findFirstLeafByPanelId,findRightResizeSplit,findTopResizeSplit,findSplitNode: helper utilities for advanced consumers.
Persistence is optional. When provided, the package stores only the layout tree and leaves the storage key up to the consumer.
const store = createLayoutStore({
initialLayout,
persistence: {
storageKey: 'dashboard-layout',
storage: window.localStorage,
},
})Import paneweave/styles.css and provide theme variables such as --accent, --panel-bg, and --panel-border in the consuming app.
The package is style-agnostic by design:
- No global
:roottheme injection. - Tokens are scoped per instance on
.paneweave-layout-root. - Internal tokens use
--pw-*names and still accept legacy aliases (--accent,--panel-bg, etc.).
Example per-layout override:
.my-layout {
--pw-root-background: transparent;
--pw-panel-bg: #111827;
--pw-panel-bg-alt: #0b1220;
--pw-panel-border: #334155;
--pw-panel-text: #e5e7eb;
--pw-accent: #22d3ee;
}