feat(maestro): layout-tree model for multi-view
Source-of-truth state for what's on screen: tabs of binary split trees whose leaves are sessions, plus leaves/findLeaf/activeTab/emptyLayout helpers. Splits and tabs UI build on this in later issues; #29 only renders a single leaf. Refs #29 (epic #37). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
// Multi-view layout model. The layout tree is the single source of truth for
|
||||
// what's on screen: a set of tabs, each holding a binary split tree whose leaves
|
||||
// are sessions. Desktop renders the tree as nested splits; mobile projects its
|
||||
// leaves to tabs. (Splits/tabs UI land in later issues — see epic #37. This
|
||||
// issue introduces the model and renders a single-leaf tree at parity.)
|
||||
|
||||
import type { Uuid } from "./types.js";
|
||||
|
||||
export type Dir = "row" | "col";
|
||||
|
||||
export type Node =
|
||||
| { kind: "leaf"; id: string; sessionId: Uuid }
|
||||
| { kind: "split"; dir: Dir; a: Node; b: Node; ratio: number };
|
||||
|
||||
export type Leaf = Extract<Node, { kind: "leaf" }>;
|
||||
|
||||
export interface Tab {
|
||||
id: string;
|
||||
title: string;
|
||||
/** The split tree, or null for an empty tab. */
|
||||
root: Node | null;
|
||||
/** Which leaf has keyboard focus / drives the sidebar highlight. */
|
||||
focusedLeafId: string | null;
|
||||
}
|
||||
|
||||
export interface Layout {
|
||||
tabs: Tab[];
|
||||
activeTabId: string;
|
||||
}
|
||||
|
||||
let seq = 0;
|
||||
/** Monotonic id for tabs and leaves (not persisted across reloads — restore
|
||||
* assigns fresh ids). */
|
||||
export function freshId(prefix = "n"): string {
|
||||
return `${prefix}${++seq}`;
|
||||
}
|
||||
|
||||
/** Every leaf under `node`, left-to-right. */
|
||||
export function leaves(node: Node | null): Leaf[] {
|
||||
if (!node) return [];
|
||||
if (node.kind === "leaf") return [node];
|
||||
return [...leaves(node.a), ...leaves(node.b)];
|
||||
}
|
||||
|
||||
export function findLeaf(node: Node | null, leafId: string): Leaf | null {
|
||||
return leaves(node).find((l) => l.id === leafId) ?? null;
|
||||
}
|
||||
|
||||
export function activeTab(layout: Layout): Tab {
|
||||
return layout.tabs.find((t) => t.id === layout.activeTabId) ?? layout.tabs[0];
|
||||
}
|
||||
|
||||
/** A layout with one empty tab. */
|
||||
export function emptyLayout(): Layout {
|
||||
const tab: Tab = { id: freshId("t"), title: "Tab 1", root: null, focusedLeafId: null };
|
||||
return { tabs: [tab], activeTabId: tab.id };
|
||||
}
|
||||
Reference in New Issue
Block a user