@dannysir/floating-components
v0.3.0
Published
VS Code-style tree-based panel layout for React
Maintainers
Readme
@dannysir/floating-components
한국어 README · Live Demo · API Reference
Tree-based resizable and reorderable panel layout for React. Split panels horizontally or vertically, resize borders by dragging, and reorder panels via drag and drop — just like VS Code or any modern IDE.
Features
- N-ary tree structure —
SplitNodecan hold two or more children, keeping the tree flat without unnecessary nesting - Border drag resize — drag panel borders to resize (requestAnimationFrame optimized)
- Drag-and-drop panel move — reorder panels via HTML5 Drag & Drop API
- Multi-level drop target — distinguishes panel edge, parent split edge, and root edge for depth-aware placement
- Immutable state — all tree updates produce new objects via spread
- View / State separation —
TreeLayout(rendering) anduseLayoutTree(state) can be used independently - TypeScript — full type declarations included
- ESM + CJS — dual-format bundle output
Installation
npm install @dannysir/floating-componentsPeer dependencies:
react >= 18
Quick Start
import {
TreeLayout,
useLayoutTree,
createComponentStore,
type LayoutNode,
} from "@dannysir/floating-components";
// 1. Map string keys to the React nodes they render.
const store = createComponentStore({
"panel-a": <div style={{ padding: 16, background: "#dbeafe", height: "100%" }}>Panel A</div>,
"panel-b": <div style={{ padding: 16, background: "#dcfce7", height: "100%" }}>Panel B</div>,
"panel-c": <div style={{ padding: 16, background: "#ffedd5", height: "100%" }}>Panel C</div>,
});
// 2. The tree stores only string componentKeys — no React elements.
const initialTree: LayoutNode = {
type: "split",
direction: "horizontal",
size: 1,
children: [
{ type: "panel", id: "panel-a", size: 1, componentKey: "panel-a" },
{
type: "split",
direction: "vertical",
size: 1,
children: [
{ type: "panel", id: "panel-b", size: 1, componentKey: "panel-b" },
{ type: "panel", id: "panel-c", size: 1, componentKey: "panel-c" },
],
},
],
};
const App = () => {
const { tree, resizeBorder, movePanel } = useLayoutTree(initialTree);
return (
<div style={{ width: "100vw", height: "100vh" }}>
<TreeLayout tree={tree} components={store} onResizeBorder={resizeBorder} onMovePanel={movePanel} />
</div>
);
};
TreeLayoutfills its parent by default (width: 100%,height: 100%). Use a sized parent as above, or passwidth/heightprops to set explicit dimensions.
The tree holds only primitive values (
id,size,direction,componentKey), soJSON.stringify(tree)round-trips cleanly. See Persistence below.
Recipes
Toggle panel visibility
const { panelIds, removePanel, insertPanel } = useLayoutTree(initialTree);
const togglePanel = (id: string, componentKey: string) => {
if (panelIds.includes(id)) {
removePanel(id);
} else {
insertPanel({ panel: { id, componentKey } });
}
};Persistence
Because the tree contains only primitive values, you can save and restore the layout with plain JSON.stringify / JSON.parse — no custom serializer needed. The ComponentStore (the key → React node mapping) lives separately in your code, so it never needs to be serialized.
const store = createComponentStore({
sidebar: <Sidebar />,
editor: <Editor />,
});
const STORAGE_KEY = "my-layout";
const load = (): LayoutNode => {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? (JSON.parse(saved) as LayoutNode) : defaultTree;
};
const App = () => {
const { tree, resizeBorder, movePanel } = useLayoutTree(load());
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(tree));
}, [tree]);
return <TreeLayout tree={tree} components={store} onResizeBorder={resizeBorder} onMovePanel={movePanel} />;
};On restore, the tree's componentKeys are looked up in the store. If a key isn't registered, the panel renders empty and a dev-mode console warning is emitted — so keep your store keys stable across releases.
Create the store once and keep a stable reference (module-level or
useMemo). Callingregister/unregistermutates the internalMapbut does not trigger a re-render — to change what's on screen dynamically, swap the tree (e.g.setTree) rather than relying on store mutation.
Drag & Drop
Drag any panel to reorder. A translucent preview of the drop target follows the cursor, and dropping near different regions produces different placements:
- Drop on the panel center → split the hovered panel
- Drop near the enclosing split's edge → place as a sibling of the parent split
- Drop near the root's edge → place at the top level
Restrict to a single axis
By default TreeLayout uses 4-edge classification (direction="complex"). Pass direction to lock the layout to one axis:
<TreeLayout
tree={tree}
direction="vertical"
onResizeBorder={resizeBorder}
onMovePanel={movePanel}
/>"vertical"— drops classified by the Y midline (top/bottom only); only vertical splits are produced"horizontal"— drops classified by the X midline (left/right only); only horizontal splits are produced"complex"(default) — 4-edge classification with both axes
If the input tree contains splits whose direction conflicts with the prop, they are auto-normalized and a dev-mode console warning is emitted. useLayoutTree.splitPanel(...) direct calls are not constrained.
See API Reference → direction.
Wire it up with useLayoutTree's movePanel:
<TreeLayout tree={tree} onResizeBorder={resizeBorder} onMovePanel={movePanel} />See API Reference → Drag & Drop for the full placement rules and the depth parameter.
Documentation
- API Reference — full props, hook return values, tree utilities, types
- CHANGELOG
License
ISC
