Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153import { adopt, define, setState, sheet } from "@nibling/web/internals";import "@nibling/web/tabs";import type { TabElement, TabListElement, TabPanelElement } from "@nibling/web/tabs";import { titleOf } from "../kinds.ts";import type { Layout, PanelId, Slot, Stack } from "../layout.ts";import type { Workbench } from "../workbench.ts";import { TabMenu } from "./tab-menu.ts";
const stackSheet = sheet(` :host { display: flex; flex-direction: column; flex: 1; min-width: 0; min-height: 0; container: nib-stack / size; } ::slotted(nib-tablist) { flex: 0 0 auto; } ::slotted(:not(nib-tablist)) { flex: 1; min-height: 0; } :host(:state(focused)) { --nib-tab-indicator: var(--nib-color-accent); }`);
const SLOTS = ["main", "primary", "secondary", "panel"] as const satisfies readonly Slot[];
const REGION_LABELS: Record<Slot, string> = { primary: "Primary Side Bar", main: "Editor", secondary: "Secondary Side Bar", panel: "Panel",};
/** * One stack of the layout: a region holding a tablist and the content area its active panel's body * sits over. Its children stay in light DOM, so tabs and the shell's tabpanels share a tree and can * reference each other through ElementInternals. */export class StackElement extends HTMLElement { readonly #internals = this.attachInternals(); readonly tablist: TabListElement = document.createElement("nib-tablist"); /** Where the active panel's body is positioned, and where dropped tabs split the stack. */ readonly content: HTMLDivElement = document.createElement("div"); /** The keyboard/pointer alternative to dragging a tab: right-click, or Shift+F10/ContextMenu on it. */ readonly #menu = new TabMenu(); readonly #tabs = new Map<PanelId, TabElement>(); #workbench: Workbench | undefined; #stack: Stack | undefined; /** Finds and focuses a panel's tab wherever it now lives, for after a menu action moves one. */ #focusPanel: ((panel: PanelId) => void) | undefined;
constructor() { super(); this.#internals.role = "region"; const root = this.attachShadow({ mode: "open" }); adopt(root, stackSheet); root.append(document.createElement("slot")); this.tablist.addEventListener("tabselect", (event) => { const id = this.#panelOf((event as Event & { tab: TabElement }).tab); if (id) this.#workbench?.dispatch({ type: "activate", panel: id }); }); this.tablist.addEventListener("tabclose", (event) => { const id = this.#panelOf((event as Event & { tab: TabElement }).tab); if (id) this.#workbench?.dispatch({ type: "close", panel: id }); }); }
connectedCallback(): void { // The menu is a popover: always out of flow (`position: fixed`), so it rides along in the // light DOM without disturbing the host's flex layout of the tablist and content. if (!this.tablist.isConnected) this.append(this.tablist, this.content, this.#menu.element); }
get stack(): Stack | undefined { return this.#stack; }
/** The tab for a panel, if this stack shows it. */ tab(panel: PanelId): TabElement | undefined { return this.#tabs.get(panel); }
/** * Shows `stack`. Tabs are reused by panel id, so a tab that stays keeps focus and drag state. * `body` gives the tabpanel each tab controls. */ update( layout: Layout, stack: Stack, workbench: Workbench, label: string, body: (panel: PanelId) => TabPanelElement | undefined, focusPanel?: (panel: PanelId) => void, ): void { this.#stack = stack; this.#workbench = workbench; this.#focusPanel = focusPanel; this.#internals.ariaLabel = label; this.tablist.label = label; setState(this.#internals, "focused", layout.focus === stack.id); // Slots as states (`nib-stack:state(slot-primary)`) let themes and tests find a part without attributes. for (const slot of SLOTS) setState(this.#internals, `slot-${slot}`, stack.slot === slot); const tabs = stack.panels.map((id) => { const tab = this.#tabs.get(id) ?? this.#tab(id); const panel = layout.panels[id]!; if (tab.textContent !== titleOf(panel)) tab.textContent = titleOf(panel); tab.selected = stack.active === id; const tabpanel = body(id); tab.panel = tabpanel ?? null; if (tabpanel) tabpanel.tab = tab; return [id, tab] as const; }); for (const [id] of this.#tabs) if (!stack.panels.includes(id)) this.#tabs.delete(id); for (const [id, tab] of tabs) this.#tabs.set(id, tab); // Skip the reorder when nothing moved, so a focused tab isn't detached and blurred. const current = [...this.tablist.children]; if (current.length !== tabs.length || tabs.some(([, tab], i) => current[i] !== tab)) { this.tablist.replaceChildren(...tabs.map(([, tab]) => tab)); } }
#tab(id: PanelId): TabElement { const tab = document.createElement("nib-tab"); tab.closable = true; tab.draggable = true; tab.addEventListener("contextmenu", (event) => { event.preventDefault(); this.#openMenu(tab, id, { x: event.clientX, y: event.clientY }); }); tab.addEventListener("keydown", (event) => { if (event.key !== "ContextMenu" && !(event.key === "F10" && event.shiftKey)) return; event.preventDefault(); const rect = tab.getBoundingClientRect(); this.#openMenu(tab, id, { x: rect.left, y: rect.bottom }); }); return tab; }
#openMenu(tab: TabElement, panel: PanelId, at: { x: number; y: number }): void { if (!this.#workbench || !this.#stack) return; this.#menu.open(this.#workbench, this.#stack, panel, tab, at, this.#focusPanel); }
#panelOf(tab: TabElement): PanelId | undefined { for (const [id, candidate] of this.#tabs) if (candidate === tab) return id; return undefined; }}
/** The region name for a stack: its slot's part name, or a numbered editor group. */export function regionLabel(stack: Stack, index: number): string { return stack.slot ? REGION_LABELS[stack.slot] : `Editor Group ${index + 1}`;}
declare global { interface HTMLElementTagNameMap { "nib-stack": StackElement; }}
define("nib-stack", StackElement);