Skip to content

hyp3rflow/panecake

Repository files navigation

panecake

A flexible, headless split pane library for React.

Features

  • Headless - Full control over styling with render props
  • Dynamic layouts - Layout managed by state, not JSX structure
  • Drag & drop - Rearrange panes by dragging
  • Resizable dividers - Resize panes with constraints support
  • N-way splits - Not limited to binary splits
  • TypeScript - Full type safety

Installation

npm install panecake jotai bunja
# or
pnpm add panecake jotai bunja

Note: jotai and bunja are peer dependencies.

Quick Start

import { Provider } from "jotai";
import { Root, Pane, createLayout } from "panecake";

const layout = createLayout((b) => {
  const sidebar = b.leaf("sidebar");
  const editor = b.leaf("editor");
  const preview = b.leaf("preview");

  const right = b.vertical([editor, preview], [0.6, 0.4]);
  return b.horizontal([sidebar, right], [0.25, 0.75]);
});

function App() {
  return (
    <Provider>
      <Root layout={layout}>
        <Pane id="sidebar">
          <Sidebar />
        </Pane>
        <Pane id="editor">
          <Editor />
        </Pane>
        <Pane id="preview">
          <Preview />
        </Pane>
      </Root>
    </Provider>
  );
}

Creating Layouts

Use createLayout with LayoutBuilder to define your layout structure:

import { createLayout } from "panecake";

const layout = createLayout((b) => {
  // Create leaf nodes (panes)
  const pane1 = b.leaf("pane-1");
  const pane2 = b.leaf("pane-2");
  const pane3 = b.leaf("pane-3");

  // Combine into branches
  // horizontal: children laid out left-to-right
  // vertical: children laid out top-to-bottom
  const rightColumn = b.vertical([pane2, pane3], [0.5, 0.5]);
  
  // Return the root node
  return b.horizontal([pane1, rightColumn], [0.3, 0.7]);
});

Layout Structure

Layouts are tree structures with two node types:

  • LeafNode - Contains a pane with content
  • BranchNode - Splits children horizontally or vertically
interface LayoutState {
  nodes: Record<NodeId, LayoutNode>;
  rootId: NodeId | null;
}

Components

<Root>

Container component that renders the layout.

<Root
  layout={layout}              // Layout state (controlled)
  onLayoutChange={setLayout}   // Layout change callback
  renderDivider={CustomDivider}      // Custom divider
  renderDropIndicator={DropIndicator} // Custom drop indicator
  className="my-pane-root"
  style={{ height: "100vh" }}
>
  {/* Pane components */}
</Root>

<Pane>

Defines a pane and its content. Must be a direct child of <Root>.

// Static content
<Pane id="sidebar">
  <Sidebar />
</Pane>

// With nodeId access via render prop
<Pane id="editor">
  {(nodeId) => <Editor nodeId={nodeId} />}
</Pane>

// With constraints
<Pane id="panel" minWidth={200} minHeight={100}>
  <Panel />
</Pane>

<Handle>

Defines a draggable area within a pane. Without a Handle, panes cannot be dragged.

<Pane id="sidebar">
  <div className="sidebar">
    <Handle className="drag-handle">
      <GripIcon />
    </Handle>
    <SidebarContent />
  </div>
</Pane>

Hooks

useLayout

Access layout state and actions from any component inside <Root>.

import { useLayout } from "panecake";

function LayoutControls() {
  const {
    layout,      // Current layout state
    setLayout,   // Replace entire layout
    addPane,     // Add a new pane
    removePane,  // Remove a pane
    movePane,    // Move a pane
    split,       // Split a pane
    resize,      // Resize panes
  } = useLayout();

  const handleReset = () => {
    const newLayout = createLayout((b) => {
      // ... build new layout
    });
    setLayout(newLayout);
  };

  const handleAddPane = () => {
    addPane("new-pane", "existing-node-id", "right");
  };

  const handleRemove = (nodeId: string) => {
    removePane(nodeId);
  };

  return (
    <div>
      <button onClick={handleReset}>Reset</button>
      <button onClick={handleAddPane}>Add Pane</button>
    </div>
  );
}

Custom Rendering

Custom Divider

import type { DividerRenderProps } from "panecake";

const CustomDivider = ({
  direction,
  onMouseDown,
  onKeyDown,
  ref,
}: DividerRenderProps) => (
  <div
    ref={ref}
    className={`divider ${direction}`}
    role="separator"
    tabIndex={0}
    onMouseDown={onMouseDown}
    onKeyDown={onKeyDown}
  />
);

<Root renderDivider={CustomDivider}>
  {/* ... */}
</Root>

Custom Drop Indicator

import type { DropIndicatorRenderProps } from "panecake";

const CustomDropIndicator = ({ side }: DropIndicatorRenderProps) => (
  <div className={`drop-indicator drop-${side}`} />
);

<Root renderDropIndicator={CustomDropIndicator}>
  {/* ... */}
</Root>

Controlled vs Uncontrolled

Controlled Mode

Pass layout and onLayoutChange to manage state externally:

function App() {
  const [layout, setLayout] = useState(initialLayout);

  return (
    <Root layout={layout} onLayoutChange={setLayout}>
      {/* ... */}
    </Root>
  );
}

Uncontrolled Mode

Omit layout prop to let panecake manage state internally:

function App() {
  return (
    <Root>
      {/* Use useLayout() hook to access/modify layout */}
    </Root>
  );
}

Advanced: Direct Bunja Access

For advanced use cases, access the underlying Jotai atoms directly:

import { useBunja } from "bunja/react";
import { useAtomValue, useSetAtom } from "jotai";
import { paneBunja, paneItemBunja } from "panecake";

// Root-level state
function AdvancedControls() {
  const { layoutAtom, dispatchAtom, draggingNodeAtom } = useBunja(paneBunja);
  const layout = useAtomValue(layoutAtom);
  const dispatch = useSetAtom(dispatchAtom);
  const draggingNode = useAtomValue(draggingNodeAtom);
  // ...
}

// Pane-level state (inside a Pane)
function PaneContent() {
  const {
    nodeIdAtom,
    paneIdAtom,
    isDraggingAtom,
    isDropTargetAtom,
    dropSideAtom,
  } = useBunja(paneItemBunja);
  
  const isDragging = useAtomValue(isDraggingAtom);
  const isDropTarget = useAtomValue(isDropTargetAtom);
  // ...
}

API Reference

Types

type PaneId = string;
type NodeId = string;
type Direction = "horizontal" | "vertical";
type DropSide = "left" | "right" | "top" | "bottom" | "center";

interface LayoutState {
  nodes: Record<NodeId, LayoutNode>;
  rootId: NodeId | null;
}

type LayoutNode = LeafNode | BranchNode;

interface LeafNode {
  type: "leaf";
  id: NodeId;
  paneId: PaneId;
}

interface BranchNode {
  type: "branch";
  id: NodeId;
  direction: Direction;
  children: NodeId[];
  ratios: number[];
}

Exports

// Components
export { Root, Pane, Handle, Divider, NodeRenderer };

// Hooks
export { useLayout };

// Layout utilities
export { createLayout, LayoutBuilder, layoutReducer };

// State (advanced)
export { paneBunja, paneItemBunja };
export { PaneContext, PaneScope, PaneItemContext, PaneItemScope };

// Utilities
export { generateId, findParent, createLeafNode, createBranchNode };

License

MIT

About

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors