Skip to content

querielo/paneweave

Repository files navigation

paneweave

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

What It Provides

  • 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

Why The Name

paneweave reflects how the library composes UI panes into a woven, recursive split tree.

Installation

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'

Consumer Example

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' }}
    />
  )
}

Redux Example

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-redux

Near-Zero Collision Mode

If 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: include paneweave-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:

  1. Generate a unique, stable prefix per product shell (example: acme-paneweave-7f3a).
  2. Pass that prefix through classNamePrefix on every PaneweaveLayout instance.
  3. Set includeDefaultClassNames={false} in strict-isolation deployments.
  4. Keep theme overrides on instance root class (or wrapper) using --pw-* tokens.
  5. If legacy overrides rely on paneweave-layout-*, temporarily set includeDefaultClassNames={true} during migration.

Optional helper:

import { createPaneweaveDomClassNames } from 'paneweave'

const classes = createPaneweaveDomClassNames({
  classNamePrefix: 'acme-paneweave-7f3a',
  includeDefaultClassNames: false,
})

Examples

Standalone example projects live in examples/.

Run it with:

cd examples/zustand-react
npm install
npm run dev

Contributor Notes

  • Agent/contributor quickstart and task routing: CLAUDE.md
  • Repo-level guardrails for coding agents: CLAUDE.md

API Summary

  • 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

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,
  },
})

Styling

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 :root theme 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;
}

About

Reusable React split-pane layout package for applications

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors