Skip to main content

react-mosaic

Build tiling window manager layouts in React with react-mosaic-component (v7) - resizable, drag-and-drop rearrangeable panels, splits and tab groups. Use when writing or changing code that imports react-mosaic-component, uses Mosaic / MosaicWindow / MosaicNode, or when asked for an IDE-like, dockable, resizable or tiled panel layout in React.

소스 정보

저장소
nomcopter/react-mosaic
최근 소스 활동
2026년 9월 24일 16:13
감지된 SKILL.md 언어
영어
스타
4,800
포크
242

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
react-mosaic
description
Build tiling window manager layouts in React with react-mosaic-component (v7) - resizable, drag-and-drop rearrangeable panels, splits and tab groups. Use when writing or changing code that imports react-mosaic-component, uses Mosaic / MosaicWindow / MosaicNode, or when asked for an IDE-like, dockable, resizable or tiled panel layout in React.
# react-mosaic-component (v7) A React tiling window manager (React 16-19, TypeScript). The layout is an **immutable n-ary tree** of panels. Full docs: https://nomcopter.github.io/react-mosaic/ **v7 is not v6.** Most examples online (and in older training data) use the v6 binary tree (`first` / `second`, `splitPercentage`, string paths like `['first']`). That shape is legacy. Write v7 code as shown here. ## Setup ```bash npm install react-mosaic-component ``` ```tsx import 'react-mosaic-component/react-mosaic-component.css'; // required ``` `<Mosaic>` fills its parent, so the parent **must have a real height** (e.g. `height: 100vh`), otherwise nothing is visible. ## Minimal example ```tsx import { Mosaic, MosaicWindow, type MosaicNode } from 'react-mosaic-component'; import 'react-mosaic-component/react-mosaic-component.css'; const initial: MosaicNode<string> = { type: 'split', direction: 'row', // 'row' = side by side, 'column' = stacked splitPercentages: [30, 70], // optional, sums to 100 children: [ 'files', { type: 'split', direction: 'column', children: ['editor', 'terminal'] }, ], }; export function App() { return ( <div style={{ height: '100vh' }}> <Mosaic<string> initialValue={initial} renderTile={(id, path) => ( <MosaicWindow<string> path={path} title={id}> <Panel id={id} /> </MosaicWindow> )} /> </div> ); } ``` `renderTile(key, path)` is called for every leaf. Pass the `path` it gives you straight to `MosaicWindow`; never compute paths yourself. ## The tree ```ts type MosaicKey = string | number; // a leaf is just its key type MosaicNode<T> = MosaicSplitNode<T> | MosaicTabsNode<T> | T; interface MosaicSplitNode<T> { type: 'split'; direction: 'row' | 'column'; children: MosaicNode<T>[]; // any number of children splitPercentages?: number[]; // one per child, sums to 100; omit for equal sizes } interface MosaicTabsNode<T> { type: 'tabs'; tabs: T[]; // leaf keys only, tab groups don't nest activeTabIndex: number; } ``` - A bare key (`'editor'`) is a valid whole tree: one full-size window. - `null` is an empty layout; `zeroStateView` is shown. - Keys must be unique in the tree. They are your IDs: map them to content in `renderTile`, don't put components or objects in the tree. Keep the tree JSON-serialisable so it can be persisted. - **Paths** are numeric index arrays: `[]` is the root, `[1, 0]` is the first child of the second child. For a tab group, the active tab's path is the group's path plus the tab index. - Narrow with `isSplitNode(node)` / `isTabsNode(node)`; anything else is a leaf. ## Controlled vs uncontrolled Use **either** `initialValue` (Mosaic owns the state) **or** `value` + `onChange` (you own it). Never both. Go controlled whenever you persist the layout or change it from outside. ```tsx const [tree, setTree] = useState<MosaicNode<string> | null>(initial); <Mosaic<string> value={tree} onChange={setTree} // every change, including each frame of a resize drag onRelease={(next) => save(next)} // once per finished interaction: persist here renderTile={renderTile} />; ``` Both callbacks get a second `meta` argument describing the change, e.g. `{ type: 'remove', path, node }`, `{ type: 'resize', path, splitPercentages }`, `{ type: 'drop', node, sourcePath, destinationPath, position?, tabIndex?, swap? }`, `{ type: 'tab-add' | 'tab-remove' | 'tab-select', ... }`. Paths in `meta` refer to the tree **before** the change. See `MosaicChangeMeta`. ## Changing the tree Never mutate a tree in place; the UI won't update. Two ways to change it: **Inside the mosaic** (toolbar buttons, tile content), use the context actions: ```tsx import { useMosaic, useMosaicWindow } from 'react-mosaic-component'; function CloseButton({ path }: { path: MosaicPath }) { const { mosaicActions } = useMosaic<string>(); return <button onClick={() => mosaicActions.remove(path)}>Close</button>; } // mosaicActions: remove(path), expand(path, pct?), replaceWith(path, node), // updateTree(updates), addTab(path), removeTab(path, index), getRoot() // useMosaicWindow().mosaicWindowActions: split(), addTab(), replaceWithNew(), // getPath(), setAdditionalControlsOpen(open | 'toggle'), connectDragSource(el) ``` `split()`, `addTab()` and `replaceWithNew()` need the `createNode` prop (on `Mosaic` or `MosaicWindow`), which returns the new node (or a promise of one). **Outside the mosaic** (controlled mode), compute a new tree and `setTree` it: ```ts import { updateTree, createRemoveUpdate, createExpandUpdate, } from 'react-mosaic-component'; setTree((t) => (t ? updateTree(t, [createRemoveUpdate(t, [1, 0])]) : t)); ``` Helpers (all pure, return new objects): | Helper | Use | | ------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `updateTree(tree, updates)` | apply `MosaicUpdate[]` (`{ path, spec }`, spec is an immutability-helper command) | | `createRemoveUpdate(tree, path)` | remove a node, collapsing its parent | | `createExpandUpdate(path, percentage)` | grow a node at every level up to the root | | `createSwapUpdates(tree, pathA, pathB)` | swap two nodes (returns an array) | | `createDragToUpdates(tree, source, dest, dropInfo)` | what a drag does, e.g. `{ type: 'split', position: 'left' }` (returns an array) | | `getNodeAtPath(tree, path)`, `getParentNode`, `getParentPath` | reads | | `getLeaves(tree)` | all leaf keys, e.g. to check if a panel is open | | `createBalancedTreeFromLeaves(keys, 'row')` | build a layout from a list of keys | | `convertLegacyToNary(tree)` | convert a stored v6 tree | To add a panel from outside, the simplest correct approach is to rebuild: `createBalancedTreeFromLeaves([...getLeaves(tree), 'new'])`, or wrap the root: `{ type: 'split', direction: 'row', children: [tree, 'new'] }`. ## MosaicWindow ```tsx <MosaicWindow<string> path={path} // required, from renderTile title="Editor" // required toolbarControls={[<SplitButton key="s" />, <RemoveButton key="r" />]} // replaces default buttons createNode={() => nextId()} draggable // default true; false disables dragging by the title bar renderToolbar={(props, draggable) => <MyToolbar />} // optional: replace the whole title bar > ... </MosaicWindow> ``` Toolbar building blocks: `ExpandButton`, `SplitButton`, `ReplaceButton`, `RemoveButton`, `Separator`, `DefaultToolbarButton`, `DEFAULT_CONTROLS_WITH_CREATION`, `DEFAULT_CONTROLS_WITHOUT_CREATION`. Using `MosaicWindow` is optional: `renderTile` can return any element. ## Tabs Put a `{ type: 'tabs', tabs: ['a', 'b'], activeTabIndex: 0 }` node anywhere a leaf can go. `renderTile` is called only for the active tab. Tab-related `Mosaic` props: `canClose(tabKey, tabs, index, path)` returning `'canClose' | 'cannotClose' | 'noClose'`, `renderTabTitle`, `renderTabButton`, `renderTabToolbarControls` (compose `DefaultAddTabButton`, `TabSplitButton`, `TabRemoveButton`), and `renderTabToolbar` for a fully custom tab bar. ## Other Mosaic props - `resize`: `'DISABLED'` or `{ minimumPaneSizePercentage?, minimumPaneSizePx?, renderSplitHandle?, preview? }`. `preview: true` only moves the divider while dragging and resizes once on release. - `dropBehavior`: `'split'` (default), `'swap'`, or `'split-and-swap'`. - `zeroStateView`: element shown when the tree is `null`. - `className`: default `'mosaic-blueprint-theme'`. Blueprint is optional; for Blueprint dark mode use `'mosaic-blueprint-theme bp5-dark'` and load Blueprint's CSS. With your own theme, pass your own class name. - `mosaicId`: set a fixed one when external drag sources must drop into the layout. ## Drag and drop `Mosaic` sets up react-dnd itself (HTML5 + touch). If the app already has a `DndProvider`, render `MosaicWithoutDragDropContext` (same props) inside it instead of nesting a second provider, and share one copy of `react-dnd` 16. External drops use drag type `MosaicDragType.WINDOW`; drop targets outside the layout can return `{ remove: true }` to take the dragged window out. Docs: https://nomcopter.github.io/react-mosaic/docs/guides/drag-and-drop ## Common mistakes - v6 shapes: `{ direction, first, second }`, `splitPercentage`, `['first']` paths. Use `type: 'split'`, `children`, `splitPercentages`, numeric paths. - Forgetting the CSS import, or a parent with no height. - Passing both `value` and `initialValue`, or `value` without `onChange`. - Mutating the tree (`tree.children.push(...)`) instead of returning a new one. - Saving on every `onChange` during a resize drag; persist in `onRelease`. - `splitPercentages` whose length differs from `children` or doesn't sum to 100. - Duplicate keys in the tree, or nesting a tabs node inside `tabs`. - Hand-writing paths instead of using the `path` from `renderTile` / `useMosaicWindow().mosaicWindowActions.getPath()`. The TypeScript declarations in this package (`index.d.ts`) are the source of truth for every signature.
GitHub에서 보기