react-mosaic

repository·master·Indexed 26 days ago

https://github.com/nomcopter/react-mosaic

A React tiling window manager providing drag-to-resize and drag-to-rearrange layouts inspired by IDEs and i3-style tiling. It supports n-ary splits, tabbed layouts, and both controlled and uncontrolled state patterns. The library uses a MosaicNode tree structure consisting of leaf, split, and tab nodes, and provides utilities like updateTree and factory functions for immutable tree mutations.

Tokens
11.1K
Snippets
33
Records
54
Agent score
89%

What's inside react-mosaic

  1. Key features of react-mosaic

    master

    Layout and Structure

    • N-ary tree layouts: A single split can hold any number of children, not just two.
    • Tabs as first-class citizens: Tab containers are a native node type in the tree structure.

    State Management

    • Controlled or uncontrolled: You can manage the layout tree in your own state by passing value and onChange props, or let the component manage it using initialValue.

    Interaction and Styling

    • Drag-and-drop: Built on react-dnd with support for HTML5 and touch backends.
    • Theming: Ships with a default CSS theme and CSS variables for overrides. It works independently or alongside Blueprint.

    Compatibility

    • Zero-config migration: Automatically converts legacy v6 binary trees to n-ary trees at render time.
  2. Use the uncontrolled pattern with `initialValue`

    master

    Use the uncontrolled pattern when you want Mosaic to manage the layout state internally and you do not need to read or manipulate the tree from outside the component. This is the simplest way to implement a working layout.

    Note: Do not provide both initialValue and value simultaneously; doing so will trigger a runtime warning.

    <Mosaic
      renderTile={(id, path) => (
        <MosaicWindow path={path} title={`Panel ${id}`}>
          <div style={{ padding: 16 }}>Panel {id}</div>
        </MosaicWindow>
      )}
      initialValue={{
        type: 'split',
        direction: 'row',
        children: ['a', 'b', 'c'],
      }}
    />
  3. Migrate from v6 binary tree to v7 n-ary tree

    master

    In v7, react-mosaic-component transitioned from a binary tree layout to an n-ary tree layout. While <Mosaic value={legacyTree}> will automatically normalize legacy v6 shapes on the first render, it is recommended to adopt the modern API.

    Key Changes

    Conceptv6 (binary)v7 (n-ary)
    Split node shape{ direction, first, second }{ type: 'split', direction, children: [...] }
    Split sizingsplitPercentage: numbersplitPercentages: number[] (array sums to 100)
    Path representationMosaicBranch[]['first', 'second']number[][0, 1]
    Tab supportNoneFirst-class { type: 'tabs', tabs, activeTabIndex } node
    Max childrenAlways 2Any number per split

    Migration Workflow

    1. Ship v7 with existing layouts: Existing v6 layouts will work immediately via automatic on-the-fly conversion.
    2. Lazy conversion: When a user saves their layout, use convertLegacyToNary to normalize the shape before persisting it to your database.
    3. Cleanup: Once all stored layouts are normalized, you can remove legacy code paths.
  4. Use the controlled pattern with `value` and `onChange`

    master

    Use the controlled pattern when you need to own the layout state. This is required for:

    • Persisting the layout (e.g., to localStorage, a server, or the URL).
    • Programmatic mutation (e.g., adding a panel via an external button or resetting to a preset).
    • Reacting to changes (e.g., for analytics, undo/redo functionality, or derived UI).

    Set value to null to represent an empty layout, which will render the zeroStateView.

    Note: Do not provide both initialValue and value simultaneously; doing so will trigger a runtime warning.

    import { useState } from 'react';
    import { Mosaic, MosaicWindow, MosaicNode } from 'react-mosaic-component';
    
    function ControlledExample() {
      const [tree, setTree] = useState<MosaicNode<string> | null>({
        type: 'split',
        direction: 'row',
        children: ['a', 'b'],
      });
    
      return (
        <Mosaic<string>
          value={tree}
          onChange={setTree}
          renderTile={(id, path) => (
            <MosaicWindow path={path} title={`Panel ${id}`}>
              <div>{id}</div>
            </MosaicWindow>
          )}
        />
      );
    }
  5. Quick start with react-mosaic

    master

    To implement a basic tiling layout, use the Mosaic component. You provide an initialValue representing the layout tree and a renderTile function to define how each tile (window) is rendered. Wrap the Mosaic component in a container with a defined height (e.g., 100vh) to ensure the layout fills the space.

    import { Mosaic, MosaicWindow } from 'react-mosaic-component';
    import 'react-mosaic-component/react-mosaic-component.css';
    
    export function App() {
      return (
        <div style={{ height: '100vh' }}>
          <Mosaic<string>
            renderTile={(id, path) => (
              <MosaicWindow<string> path={path} title={`Panel ${id}`}>
                <div style={{ padding: 20 }}>Contents of {id}</div>
              </MosaicWindow>
            )}
            initialValue={{
              type: 'split',
              direction: 'row',
              children: ['a', 'b'],
            }}
          />
        </div>
      );
    }
  6. Version the stored layout shape

    master

    When changing the schema of your persisted layout, treat the stored tree like any other on-disk format. You can manage changes by:

    1. Bumping the storage key: Change STORAGE_KEY (e.g., from v1 to v2) to avoid loading incompatible data.
    2. Internal versioning: Include a version field inside the JSON payload and implement a migration function to transform old tree shapes into the current format during the load process.
    interface StoredLayout {
      version: 2;
      tree: MosaicNode<string>;
    }
    
    function load(): MosaicNode<string> {
      const raw = localStorage.getItem(STORAGE_KEY);
      if (!raw) return DEFAULT_LAYOUT;
      const stored = JSON.parse(raw) as { version: number; tree: unknown };
      if (stored.version === 1) return migrateV1toV2(stored.tree);
      return stored.tree as MosaicNode<string>;
    }
  7. Write a custom theme for `<Mosaic>`

    master

    To create a custom theme, add a unique class name to the <Mosaic> component and use it to scope your CSS overrides. All library visual rules are namespaced under .mosaic-*.

    Example of scoping overrides to a .my-theme class:

    .my-theme.mosaic {
      background: #0b1220;
    }
    
    .my-theme .mosaic-window {
      background: #111a2c;
      border: 1px solid #1f2a44;
      border-radius: 6px;
    }
    
    .my-theme .mosaic-window-title {
      background: linear-gradient(90deg, #1a2236, #111a2c);
      color: #cbd5e0;
    }
    
    .my-theme .mosaic-split:hover {
      background: #4c90f0;
    }

    Then apply it to the component:

    <Mosaic className="my-theme" renderTile={/* ... */} />
  8. Customize MosaicWindow toolbars

    master

    Every MosaicWindow includes a title bar with default controls. You can replace, add to, or style these controls by passing React nodes to the toolbarControls prop on the MosaicWindow component.

    To use the default controls without rebuilding them, import the provided presets from react-mosaic-component:

    • DEFAULT_CONTROLS_WITH_CREATION: Includes Split, Expand, and Remove buttons.
    • DEFAULT_CONTROLS_WITHOUT_CREATION: Includes Expand and Remove buttons (disables Split).
    import {
      DEFAULT_CONTROLS_WITH_CREATION,
      DEFAULT_CONTROLS_WITHOUT_CREATION,
    } from 'react-mosaic-component';
    
    // Usage in MosaicWindow
    <MosaicWindow
      path={path}
      title="My Panel"
      toolbarControls={DEFAULT_CONTROLS_WITHOUT_CREATION}
    />
  9. Migrate from binary trees to n-ary splits

    master

    In version 7, react-mosaic moved from binary splits (two children) to n-ary splits (any number of children). This allows for shallower trees and easier layout management.

    If you are using a legacy binary tree structure in your storage:

    1. <Mosaic value={legacyTree}> will automatically convert binary trees to n-ary trees on the fly.
    2. You can use the convertLegacyToNary utility for explicit upgrades.
  10. Apply a theme using the `className` prop on `<Mosaic>`

    master

    You can select a built-in theme by passing a specific string to the className prop on the <Mosaic> component.

    • Default Neutral Theme: Leave className empty.
    • Blueprint Light Theme: Use 'mosaic-blueprint-theme'.
    • Blueprint Dark Theme: Use 'mosaic-blueprint-theme bp5-dark'. When using this, also set blueprintNamespace="bp5" to ensure internal icons render correctly via the Blueprint icon font.
    <Mosaic<string>
      className="mosaic-blueprint-theme bp5-dark"
      blueprintNamespace="bp5"
      renderTile={/* ... */}
      initialValue={/* ... */}
    />
  11. Persist layout to remote storage

    master

    For server-backed persistence, use the onRelease prop to trigger an asynchronous write. It is recommended to use a debounce function (e.g., from lodash-es) to collapse rapid onRelease calls into a single network request, preventing rate-limiting and unnecessary traffic.

    import { useMemo } from 'react';
    import debounce from 'lodash-es/debounce';
    
    const save = useMemo(
      () =>
        debounce((next: MosaicNode<string>) => {
          fetch('/api/layout', {
            method: 'PUT',
            body: JSON.stringify(next),
          });
        }, 500),
      [],
    );
    
    <Mosaic value={tree} onChange={setTree} onRelease={save} renderTile={/*…*/} />