Skip to content

RustamovHumoyunMirzo/Apane

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Apane

Framework-agnostic pane layout library for IDE-style workspaces.

Apane gives you split panes, tab stacks, docking, runtime tab control, drag-and-drop, resize handling, and a full event surface without tying you to a UI framework.

Features

  • Recursive split and stack layout model
  • Runtime tab add, update, move, activate, close, and toggle APIs
  • Stack header control buttons with runtime registration
  • Active-tab header controls that can override stack controls by id
  • Dock tabs left, right, top, bottom, or reorder inside stacks
  • Pointer-based split resizing with subtree-aware minSize
  • Global tab parameters via setGlobalParam(...)
  • Event system with .on(), .off(), .once()
  • Flexible tab icons, dirty icons, and close icons
  • Layout serialization and hydration

Install

npm install

Develop

npm run dev

Build

npm run build

Quick Start

import { createApane, Node, Stack, Tab } from "apane";
import "apane/style.css";

const pane = createApane(document.getElementById("app"), {
  minSize: 0.16,
  layout: new Node({
    id: "root",
    type: "stack",
    minSize: 0.2,
    stack: new Stack("main", [
      new Tab("welcome", "Welcome", "welcome")
    ])
  }),
  contents: {
    welcome: () => "<div>Hello</div>"
  }
});

Core Concepts

Node

Represents either:

  • a split node with direction and children
  • a stack node with a Stack

Example:

new Node({
  id: "workspace",
  type: "split",
  direction: "row",
  children: [
    new Node({
      id: "left",
      type: "stack",
      stack: new Stack("left-stack", [
        new Tab("files", "Files", "files")
      ])
    }),
    new Node({
      id: "right",
      type: "stack",
      stack: new Stack("right-stack", [
        new Tab("editor", "index.js", "editor")
      ])
    })
  ]
});

Stack

Owns an ordered list of tabs and the activeTabId.

Tab

Represents a single tab.

new Tab("editor", "index.js", "editor", "./icons/js.svg", true, {
  closable: true,
  closeIcon: "<span>×</span>",
  isDirtyIcon: "./icons/dirty.svg",
  meta: { language: "javascript" }
});

createApane(...)

const pane = createApane(element, {
  layout,
  contents,
  minSize: 0.12,
  className: "custom-root",
  globalTabParams: {
    closable: true
  }
});

Options

  • layout: required root Node
  • contents: object of content renderers
  • minSize: default minimum normalized size for resizable nodes
  • className: extra class added to the root element
  • globalTabParams: properties applied to all tabs

Content Renderers

Each renderer receives:

{
  tab,
  container,
  layout,
  stack,
  node
}

You can return:

  • an HTML string
  • a DOM node
  • an array of DOM nodes
  • or render directly into container

Example:

const pane = createApane(root, {
  layout,
  contents: {
    editor: ({ tab }) => `<pre>Opened: ${tab.title}</pre>`
  }
});

If a content key is missing, Apane leaves the content area blank and logs a warning in the console.

Runtime API

Lookup

  • getNode(nodeId)
  • getNodes()
  • getStack(stackId)
  • getStacks()
  • getTab(tabId)
  • getTabs()

Layout Control

  • setLayout(root)
  • updateLayout((root, layout) => nextRoot)
  • setFillRoot(stackId, enabled)
  • isFillRootActive()
  • getFillRootState()
  • serialize()
  • toJSON()
  • render()
  • destroy()

Content Registry

  • registerContent(key, renderer)
  • registerContents(renderers)

Tab Control

  • addTab(stackId, tab, options?)
  • insertTab(stackId, tab, index, options?)
  • activateTab(tabId)
  • closeTab(tabId)
  • closeTabs(tabIds)
  • moveTab(tabId, targetNodeId, options?)
  • updateTab(tabId, patch)
  • setTabTitle(tabId, title)
  • setTabIcon(tabId, icon)
  • setTabCloseIcon(tabId, closeIcon)
  • setTabDirtyIcon(tabId, isDirtyIcon)
  • setTabDirty(tabId, isDirty)
  • toggleTabDirty(tabId)
  • setTabClosable(tabId, closable)
  • toggleTabClosable(tabId)

Stack Control

  • setStackActiveTab(stackId, tabId)
  • setFillRoot(stackId, enabled)
  • addStackHeaderControl(config)
  • updateStackHeaderControl(id, patch, stackId?)
  • removeStackHeaderControl(id, stackId?)
  • getStackHeaderControls(stackId?)
  • addTabHeaderControl(config)
  • updateTabHeaderControl(id, patch, tabId)
  • removeTabHeaderControl(id, tabId)
  • getTabHeaderControls(tabId?)
  • getResolvedHeaderControls(stackId?, activeTabId?)

Global Tab Params

  • setGlobalParam(paramName, value)
  • applyGlobalParams()

setGlobalParam(...) updates all current tabs immediately and is also applied to tabs added later.

Example:

pane.setGlobalParam("closeIcon", "./icons/close.svg");
pane.setGlobalParam("closable", false);
pane.setGlobalParam("meta", { source: "runtime" });

To remove a global param, pass undefined:

pane.setGlobalParam("closeIcon", undefined);

Tab Icons

icon, closeIcon, and isDirtyIcon support:

  • plain text
  • image URLs
  • local/relative asset paths
  • HTML markup strings
  • DOM elements

Examples:

new Tab("a", "App", "app", "JS");
new Tab("b", "Image", "image", "/icons/file.svg");
new Tab("c", "HTML", "html", "<strong>H</strong>");
new Tab("d", "DOM", "dom", document.createElement("span"));

Dirty Tabs

If isDirty is true, the dirty indicator lives in the close-icon slot.

You can supply your own dirty-state icon with isDirtyIcon.

Behavior:

  • normal state: dirty dot visible
  • hover: close icon visible
  • drag preview: dirty dot visible

Runtime helpers:

pane.setTabDirty("editor", true);
pane.toggleTabDirty("editor");
pane.setTabDirtyIcon("editor", "./icons/dirty.svg");

Events

Apane includes a built-in event bus:

  • on(eventName, listener)
  • off(eventName, listener)
  • once(eventName, listener)

Example:

const unsubscribe = pane.on("tab:add", (event) => {
  console.log("added", event.tab.id);
});

pane.once("tab:close", (event) => {
  console.log("closed", event.tabId);
});

unsubscribe();

Event Names

  • render:before
  • render
  • content:register
  • layout:set
  • layout:update
  • layout:fill-root
  • global:param
  • tab:add
  • tab:activate
  • tab:update
  • tab:move
  • tab:close
  • stack:change
  • stack:control:add
  • stack:control:update
  • stack:control:remove
  • stack:control:click
  • tab:control:add
  • tab:control:update
  • tab:control:remove
  • tab:control:click

Event Payload Notes

All events include at least:

  • type
  • layout

Common payload fields:

  • tab
  • tabId
  • stack
  • node
  • root
  • previous
  • patch
  • paramName
  • value
  • placement
  • index
  • reason

You can also listen to "*" if you want a catch-all channel:

pane.on("*", (event) => {
  console.log(event.type, event);
});

Runtime Examples

Add a Stack Header Control

const closeAllControl = pane.addStackHeaderControl({
  id: "close-all",
  title: "Close all tabs",
  icon: "<span>×</span>",
  stackId: "main-stack",
  onClick: ({ stack, layout }) => {
    if (!stack) return;
    layout.closeTabs(stack.tabs.map((tab) => tab.id));
  }
});

The returned handle supports:

closeAllControl.update({
  title: "Close every tab"
});

closeAllControl.remove();

Add an Active-Tab Header Control

pane.addTabHeaderControl({
  id: "close-all",
  title: "Close only this tab's stack",
  icon: "./icons/close-all.svg",
  tabId: "editor",
  onClick: ({ stack, layout }) => {
    if (!stack) return;
    layout.closeTabs(stack.tabs.map((tab) => tab.id));
  }
});

If a tab control uses the same id as a stack control, the tab control overrides it only while that tab is active.

Add a New Tab

pane.addTab("main-stack", new Tab("logs", "Logs", "logs"));

Fill One Stack to Root

pane.setFillRoot("editor-a", true);

Restore the previous layout:

pane.setFillRoot("editor-a", false);

Or inspect current state:

pane.isFillRootActive();
pane.getFillRootState();

Insert a Tab at a Specific Position

pane.insertTab("main-stack", new Tab("preview", "Preview", "preview"), 1);

Update a Tab

pane.updateTab("editor", {
  title: "index.ts",
  isDirty: true
});

Move a Tab Into Another Stack

pane.moveTab("editor", "secondary-node", {
  placement: "center",
  index: 0
});

Dock a Tab to the Right

pane.moveTab("editor", "target-node", {
  placement: "right"
});

Replace the Entire Layout

pane.setLayout(new Node({
  id: "root",
  type: "stack",
  stack: new Stack("single", [
    new Tab("welcome", "Welcome", "welcome")
  ])
}));

Mutate Layout with updateLayout

pane.updateLayout((root) => {
  root.size = 1;
  return root;
});

Serialization

const json = pane.serialize();
const snapshot = pane.toJSON();

And restore:

import { ApaneLayout } from "apane";

const restored = ApaneLayout.fromJSON(element, snapshot, {
  contents
});

Notes

  • minSize can be set globally in createApane({ minSize }) and overridden per Node
  • empty stacks render blank content
  • missing renderers do not render fallback UI
  • drag-and-drop, stack ordering, close icons, and dirty indicators work at runtime

Exports

  • createApane
  • ApaneLayout
  • Node
  • Stack
  • Tab
  • getVersion

About

Framework-agnostic pane layout library for IDE-style workspaces

Topics

Resources

License

Stars

Watchers

Forks

Contributors