githubnext/ace2Public

Ace: a collaborative coding environment on the team's own machines

History

readme.md

@ace/split-tabs

Renderless split-pane and tab state for Ace. The package owns layout math: tabs, panes, grid lines, resize bounds, min-size constraints, and saved state shape. Rendering, labels, icons, storage, tooltips, and drag UI belong to the consuming app.

Model

import * as Split from "@ace/split-tabs";

let state = Split.initial("session");
let api = Split.create(state, { min: tab => tab === "session" ? 450 : 350 });
  • Tab is a string id.
  • Pane stores a grid area, ordered tab ids, and the active tab.
  • State stores rows, cols, panes, tabs, focused pane, and id counters.
  • Config.min is optional and returns the minimum pane size in pixels.

Rendering

let view = api.view({ width, height, flat });
let css = Split.style(view);

root.style.gridTemplateColumns = css.gridTemplateColumns;
root.style.gridTemplateRows = css.gridTemplateRows;

for (let pane of view.panes) {
	element.style.gridArea = Split.place(pane);
}

for (let handle of Split.handles(view)) {
	handleElement.style.gridArea = Split.place(handle);
}
  • view(data) returns the render state for a container size, projecting ratios inside min-size limits when possible.
  • flat: true collapses all panes into one tab strip without changing saved state.
  • style(state) returns CSS grid templates with named lines.
  • place(pane | handle) returns a CSS grid-area.
  • handles(state) returns resize handles for visible shared grid lines.

Mutations

All mutations return false when nothing changed.

api.open({ tab: "notes" });
api.open({ to: { pane: "pane-1", side: "right" }, fit });
api.move({ tab: "notes", to: { pane: "pane-2" }, fit });
api.move({ tab: "notes", to: { pane: "pane-2", before: "logs" }, fit });
api.move({ tab: "notes", to: { pane: "pane-2", side: "bottom" }, fit });
api.select({ tab: "notes" });
api.focus({ pane: "pane-2" });
api.close({ tab: "notes" });
api.resize({ axis: "col", line: "C1", ratio: 0.5, size: width });
  • open creates a tab, optionally into a pane or split.
  • move reorders, moves between panes, or creates a split.
  • select activates a tab and focuses its host pane.
  • focus changes the focused pane.
  • close removes a tab and compacts empty panes.
  • resize moves a named grid line within min-size bounds.

Persistence

let json = JSON.stringify(Split.save(api.get()));
let state = Split.restore(JSON.parse(json));
  • save returns a versioned data object suitable for storage or transfer.
  • restore turns saved data back into state. It also accepts raw State for migration from unversioned data.

Constraints

let fit = { width, height, min };

let allowed = Split.can(state, {
	...fit,
	move: { tab: "notes", to: { pane: "pane-2", side: "right" } },
});

let bounds = Split.bounds(state, {
	axis: "col",
	line: "C1",
	ratio: 0.5,
	size: width,
	min,
});
  • can checks whether an open or move would fit.
  • fits checks whether a layout can fit a container after projection.
  • bounds returns min/max resize ratios for a line.
  • constrain projects a layout back inside min-size limits.
  • collapse returns a one-pane tabbed layout.

Store

let unsubscribe = api.subscribe(render);
let state = api.get();

The store is synchronous and framework-neutral. During pointer resize, update DOM directly with Split.style(next) and call api.resize once on pointer up.