react-layman
Version:
A dynamic tiling layout manager made for React
434 lines (335 loc) • 11.9 kB
Markdown
<div align="center">
[]()
[](https://github.com/Jeshwin/react-layman)
[](https://github.com/Jeshwin/react-layman)
[](https://github.com/Jeshwin/react-layman/issues)

[](/LICENSE)
</div>
React Layman is a fully-featured, dynamic layout manager made for React. It is written in Typescript and provides typing, but may also be used with regular Javascript. Layman is inspired by [Replit](https://replit.com)'s IDE, [LeetCode](https://leetcode.com)'s new UI, and the pre-existing [React Mosaic](https://github.com/nomcopter/react-mosaic) project.
You can play around with Layman through [this demo](https://jeshwin.github.io/react-layman).
- [x] Dynamic Layout
- [x] Rows and columns
- [x] Adjustable windows
- [x] Drag and drop windows
- [x] Delete windows
- [x] Tabbed windows
- [x] Draggable tabs
- [x] Extra features
- [x] Auto Arrange
- [x] "Add to corner" hueristic
- [x] Add tabs from sources external to layout
To use Layman, add the `LaymanProvider` component, which sets up your initial layout, rendering functions, and other configurations. Add the `Layman` component within the provider to render out the layout. `<Layman />` can be deeply nested within `LaymanProvider`. Also note, `<Layman />`'s parent needs to have a defined width and height, since it's dimensions are relative to it.
```tsx
<LaymanProvider initialLayout={initialLayout} renderPane={renderPane} renderTab={renderTab} renderNull={<NullLayout />}>
<div>
<div>
<div style={{width: 1200, height: 900}}>
<Layman />
</div>
</div>
</div>
</LaymanProvider>
```
To install and run Layman locally, run the following commands in your terminal. This will clone this repository, install all necessary packages, and run the demo page at `localhost:5173/react-layman`
```bash
git clone https://github.com/Jeshwin/react-layman.git
cd react-layman
npm install
npm run dev
```
You can use the default theme in `src/styles/theme.css`, or define your own themes with CSS variables like this:
```css
:root {
/*** Separators ***/
/* Color of the handle on the separator */
--separator-handle-color:
/* Thickness of the separator between windows */
--separator-thickness: 4px;
/* Length of the separator handle */
--separator-handle-length: 16px;
/*** Windows ***/
/* Background color of the window */
--window-bg-color:
/* Border radius for window corners */
--border-radius: 8px;
/*** Window Toolbars ***/
/* Background color of the toolbar */
--toolbar-bg-color:
/* Background color of the toolbar on hover */
--toolbar-hover-bg-color:
/* Background color for toolbar buttons on hover */
--toolbar-button-hover-bg-color:
/* Height of the toolbar at the top of each window */
--toolbar-height: 32px;
/*** Tabs ***/
/* Text color of tab titles */
--tab-text-color:
/* Color of the 'close' icon in tabs, with opacity */
--close-tab-color:
/* Color of indicators (e.g., focus indicator) */
--indicator-color:
/* Thickness of indicators (e.g., focus indicator) */
--indicator-thickness: 1px;
/* Font size for text in tabs */
--tab-font-size: 14px;
}
```
Then, you can import this theme at the root of your project. Note, you must still import the global CSS file, since this is required for Layman to work properly.
```tsx
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
// Import a custom theme
import "./custom_theme.css";
// Or import the default theme
import "../src/styles/theme.css";
// You must still import the global CSS settings
import "../src/styles/global.css";
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
```
Please see [index.d.ts](src/index.d.ts) for the full details.
```ts
export interface Position {
top: number;
left: number;
width: number;
height: number;
}
import {v4 as uuidv4} from "uuid";
interface TabOptions {
[]: unknown; // Allows any custom data
}
export class TabData {
// private UUID representing the tab
id: string;
// Is the tab currently selected in a window?
isSelected: boolean;
// Display name of tab
public name: string;
// Optional data attached to each tab
public options: TabOptions;
/** Creates an instance of the TabData class. */
constructor(name: string, options: TabOptions = {}) {
this.id = uuidv4();
this.isSelected = false;
this.name = name;
this.options = options;
}
}
```
```ts
// Credit: https://blog.replit.com/leaky-uis
// This is a utility type, a dynamically sized tuple
// that requires at least 2 elements be present. This
// guarantees flatness, i.e. no awkward [[[[A]]]] case
export type Children<T> = [T, T, ...T[]];
export type LaymanDirection = "column" | "row";
export type LaymanPath = Array<number>;
export interface LaymanWindow {
viewPercent?: number;
tabs: TabData[];
selectedIndex?: number;
}
export interface LaymanNode {
direction: LaymanDirection;
viewPercent?: number;
children: Children<LaymanLayout>;
}
export type LaymanLayout = LaymanWindow | LaymanNode | undefined;
```
```ts
export const TabType = "TAB";
export const WindowType = "WINDOW";
interface DragTab {
tab: TabData;
path?: LaymanPath;
}
interface DragWindow {
tabs: TabData[];
path: LaymanPath;
selectedIndex: number;
}
export type DragData = DragTab | DragWindow;
```
```ts
type LaymanProviderProps = {
initialLayout: LaymanLayout;
renderPane: (tab: TabData) => JSX.Element;
renderTab: (tab: TabData) => JSX.Element;
renderNull: JSX.Element;
};
```
```ts
export type PaneRenderer = (arg0: TabData) => JSX.Element;
export type TabRenderer = (arg0: TabData) => string | JSX.Element;
export interface LaymanContextType {
globalContainerSize: Position;
setGlobalContainerSize: Dispatch<SetStateAction<Position>>;
layout: LaymanLayout;
layoutDispatch: React.Dispatch<LaymanLayoutAction>;
setDropHighlightPosition: React.Dispatch<Position>;
globalDragging: boolean;
setGlobalDragging: React.Dispatch<boolean>;
draggedWindowTabs: TabData[];
setDraggedWindowTabs: React.Dispatch<TabData[]>;
windowDragStartPosition: {x: number; y: number};
setWindowDragStartPosition: React.Dispatch<{x: number; y: number}>;
renderPane: PaneRenderer;
renderTab: TabRenderer;
renderNull: JSX.Element;
}
```
Updates to the layout are handled through a [React Reducer](https://react.dev/learn/scaling-up-with-reducer-and-context) with the function `layoutDispatch`. Here are the following actions that you can use for controlling changes to the layout
```ts
layoutDispatch({
type: "addTab",
tab: TabData,
path: LaymanPath, // Path of the window to add the tab to
});
```
```ts
layoutDispatch({
type: "removeTab",
tab: TabData,
path: LaymanPath, // Path of the window to remove the tab from
});
```
If the tab does not exist in the path, no changes will be made to the layout.
```ts
layoutDispatch({
type: "selectTab",
tab: TabData,
path: LaymanPath, // Path of the window to select the tab from
});
```
If the tab does not exist in the path, no changes will be made to the layout.
```ts
layoutDispatch({
type: "moveTab",
tab: TabData,
path: LaymanPath, // Original path of the tab
newPath: LaymanPath, // New path for the tab
placement: "top" | "bottom" | "left" | "right" | "center",
});
```
If the tab does not exist in the original path, no changes will be made to the layout.
```ts
layoutDispatch({
type: "moveSeparator",
path: LaymanPath, // Path of the node that the separator is located in
index: number, // Index of the separator within the node
newSplitPercentage: number, // Updated split percentage for the layout left of the separator
});
```
```ts
layoutDispatch({
type: "addWindow",
window: LaymanWindow,
path: LaymanPath, // Path of the window to add next to
placement: "top" | "bottom" | "left" | "right",
});
```
```ts
layoutDispatch({
type: "removeWindow",
path: LaymanPath, // Path of the window to remove
});
```
```ts
layoutDispatch({
type: "moveWindow",
window: LaymanWindow,
path: LaymanPath, // Original path of the window
newPath: LaymanPath, // New path of the window
placement: "top" | "bottom" | "left" | "right" | "center",
});
```
```ts
layoutDispatch({
type: "addTabWithHeuristic";
heuristic: "topleft" | "topright";
tab: TabData;
})
```
#### Auto Arrange Layout
```ts
layoutDispatch({
type: "autoArrange",
});
```
See the [demo](https://jeshwin.github.io/react-layman) for a full example of Laymans' current features!
This is the code for the tab sources in the demo, which support dragging into the layout to create a new tab, or adding using a specified heuristic
```tsx
export default function TabSource({tabName, heuristic}: {tabName: string; heuristic: LaymanHeuristic}) {
const {setGlobalDragging, layoutDispatch} = useContext(LaymanContext);
const [{isDragging}, drag] = useDrag({
type: TabType,
item: {
path: undefined,
tab: new TabData(tabName),
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
useEffect(() => {
setGlobalDragging(isDragging);
}, [isDragging, setGlobalDragging]);
const handleDoubleClick = () => {
// Add tab to the top left window
layoutDispatch({
type: "addTabWithHeuristic",
tab: new TabData(tabName),
heuristic: heuristic,
});
};
return (
<div
ref={drag}
className="tab-source"
style={{
width: 48,
height: 48,
display: "grid",
placeContent: "center",
textAlign: "center",
borderRadius: 8,
backgroundColor: "#babbf1",
margin: 4,
opacity: isDragging ? 0.5 : 1,
cursor: "pointer",
}}
onDoubleClick={handleDoubleClick}
>
{tabName}
</div>
);
}
```
This repository is published under the [MIT license](/LICENSE)