regular-layout 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
2
+ // ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░
3
+ // ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░
4
+ // ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░
5
+ // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
6
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
7
+ // ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃
8
+ // ┃ * of the Regular Layout library, distributed under the terms of the * ┃
9
+ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
+
12
+ import {
13
+ MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD,
14
+ type Layout,
15
+ } from "./layout_config.ts";
16
+
17
+ /**
18
+ * Adjusts panel sizes during a drag operation on a divider.
19
+ *
20
+ * The `delta` is distributed proportionally among affected panels, maintaining
21
+ * the sum:
22
+ *
23
+ * - Panels before and including the path index shrink by delta.
24
+ * - Panels after the path index grow by delta.
25
+ *
26
+ * @param panel - The root layout tree to modify.
27
+ * @param path - Path to the divider being dragged (identifies which split panel
28
+ * to resize).
29
+ * @param delta - Amount to resize, as a fraction (0-1). Positive values grow
30
+ * panels before the divider, negative values shrink them.
31
+ * @returns A new layout tree with updated sizes (original is not mutated).
32
+ * ```
33
+ */
34
+ export function redistribute_panel_sizes(
35
+ panel: Layout,
36
+ path: number[],
37
+ delta: number,
38
+ ): Layout {
39
+ // Clone the entire panel structure
40
+ const result = structuredClone(panel);
41
+
42
+ // Find the orientation of the insertion panel,
43
+ // and scale the delta on the respective axis if aligned.
44
+ let current: Layout = result;
45
+ const deltas = { horizontal: delta, vertical: delta };
46
+ for (let i = 0; i < path.length - 1; i++) {
47
+ if (current.type === "split-panel") {
48
+ deltas[current.orientation] /= current.sizes[path[i]];
49
+ current = current.children[path[i]];
50
+ }
51
+ }
52
+
53
+ // Apply the redistribution at the final path index
54
+ if (current.type === "split-panel") {
55
+ const delta = deltas[current.orientation];
56
+ const index = path[path.length - 1];
57
+ current.sizes = add_and_redistribute(current.sizes, index, delta);
58
+ }
59
+
60
+ return result;
61
+ }
62
+
63
+ function add_and_redistribute(
64
+ arr: number[],
65
+ index: number,
66
+ delta: number,
67
+ ): number[] {
68
+ const result = [...arr];
69
+ let before_total = 0;
70
+ for (let i = 0; i <= index; i++) {
71
+ before_total += arr[i];
72
+ }
73
+
74
+ let after_total = 0;
75
+ for (let i = index + 1; i < arr.length; i++) {
76
+ after_total += arr[i];
77
+ }
78
+
79
+ // Clamp `delta` to prevent redistributing either side to 0.
80
+ delta =
81
+ Math.sign(delta) *
82
+ Math.min(
83
+ Math.abs(delta),
84
+ (1 - MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD) *
85
+ (delta > 0 ? before_total : after_total),
86
+ );
87
+
88
+ // Redistribute elements
89
+ for (let i = 0; i <= index; i++) {
90
+ const proportion = arr[i] / before_total;
91
+ result[i] = arr[i] - delta * proportion;
92
+ }
93
+
94
+ for (let i = index + 1; i < arr.length; i++) {
95
+ const proportion = arr[i] / after_total;
96
+ result[i] = arr[i] + delta * proportion;
97
+ }
98
+
99
+ return result;
100
+ }
@@ -0,0 +1,102 @@
1
+ // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
2
+ // ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░
3
+ // ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░
4
+ // ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░
5
+ // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
6
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
7
+ // ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃
8
+ // ┃ * of the Regular Layout library, distributed under the terms of the * ┃
9
+ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
+
12
+ import type { Layout } from "./layout_config.ts";
13
+ import { EMPTY_PANEL } from "./layout_config.ts";
14
+
15
+ /**
16
+ * Removes a child panel from the layout tree by its name.
17
+ *
18
+ * Redistributes the removed panel's space proportionally among remaining
19
+ * siblings. Automatically collapses split panels when only one child remains.
20
+ *
21
+ * @param panel - The root layout tree to remove from.
22
+ * @param child - Name of the child panel to remove.
23
+ * @returns A new layout tree with the child removed (original is not mutated).
24
+ * Returns `EMPTY_PANEL` if the last panel is removed.
25
+ */
26
+ export function remove_child(panel: Layout, child: string): Layout {
27
+ // If this is a child panel and it matches, we can't remove ourselves
28
+ // The caller should handle this case
29
+ if (panel.type === "child-panel") {
30
+ return structuredClone(EMPTY_PANEL);
31
+ }
32
+
33
+ // Clone the panel structure
34
+ const result = structuredClone(panel);
35
+
36
+ // Try to remove the child from this split panel's children
37
+ const index = result.children.findIndex((p) => {
38
+ if (p.type === "child-panel") {
39
+ return p.child === child;
40
+ }
41
+ return false;
42
+ });
43
+
44
+ if (index !== -1) {
45
+ // Found the child at this level - remove it
46
+ const newChildren = result.children.filter((_, i) => i !== index);
47
+ const newSizes = remove_and_redistribute(result.sizes, index);
48
+
49
+ // If only one child remains, collapse the split panel
50
+ if (newChildren.length === 1) {
51
+ return newChildren[0];
52
+ }
53
+
54
+ result.children = newChildren;
55
+ result.sizes = newSizes;
56
+ return result;
57
+ }
58
+
59
+ // Child not found at this level - recursively search children
60
+ let modified = false;
61
+ const newChildren = result.children.map((p) => {
62
+ if (p.type === "split-panel") {
63
+ const updated = remove_child(p, child);
64
+ if (updated !== p) {
65
+ modified = true;
66
+ }
67
+ return updated;
68
+ }
69
+ return p;
70
+ });
71
+
72
+ if (modified) {
73
+ result.children = newChildren;
74
+ }
75
+
76
+ return result;
77
+ }
78
+
79
+ function remove_and_redistribute(arr: number[], index: number): number[] {
80
+ const result = [];
81
+
82
+ // Get the size of the element being removed
83
+ const removed_size = arr[index];
84
+
85
+ // Calculate the total of remaining elements
86
+ let remaining_total = 0;
87
+ for (let i = 0; i < arr.length; i++) {
88
+ if (i !== index) {
89
+ remaining_total += arr[i];
90
+ }
91
+ }
92
+
93
+ // Distribute the removed size proportionally to remaining elements
94
+ for (let i = 0; i < arr.length; i++) {
95
+ if (i !== index) {
96
+ const proportion = arr[i] / remaining_total;
97
+ result.push(arr[i] + removed_size * proportion);
98
+ }
99
+ }
100
+
101
+ return result;
102
+ }
@@ -0,0 +1,40 @@
1
+ // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
2
+ // ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░
3
+ // ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░
4
+ // ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░
5
+ // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
6
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
7
+ // ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃
8
+ // ┃ * of the Regular Layout library, distributed under the terms of the * ┃
9
+ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
+
12
+ import { RegularLayout } from "./regular-layout.ts";
13
+ import { RegularLayoutFrame } from "./regular-layout-frame.ts";
14
+
15
+ customElements.define("regular-layout-frame", RegularLayoutFrame);
16
+
17
+ customElements.define("regular-layout", RegularLayout);
18
+
19
+ declare global {
20
+ interface Document {
21
+ createElement(
22
+ tagName: "regular-layout",
23
+ options?: ElementCreationOptions,
24
+ ): RegularLayout;
25
+
26
+ createElement(
27
+ tagName: "regular-layout-frame",
28
+ options?: ElementCreationOptions,
29
+ ): RegularLayoutFrame;
30
+
31
+ querySelector<E extends Element = Element>(selectors: string): E | null;
32
+ querySelector(selectors: "regular-layout"): RegularLayout | null;
33
+ querySelector(selectors: "regular-layout-frame"): RegularLayoutFrame | null;
34
+ }
35
+
36
+ interface CustomElementRegistry {
37
+ get(tagName: "regular-layout"): typeof RegularLayout;
38
+ get(tagName: "regular-layout-frame"): typeof RegularLayoutFrame;
39
+ }
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,72 @@
1
+ // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
2
+ // ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░
3
+ // ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░
4
+ // ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░
5
+ // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
6
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
7
+ // ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃
8
+ // ┃ * of the Regular Layout library, distributed under the terms of the * ┃
9
+ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
+
12
+ /**
13
+ * Regular Layout - Resizable Panel Layout System
14
+ *
15
+ * A Web Component library for creating resizable, re-arrangeable layouts using
16
+ * CSS `grid`.
17
+ *
18
+ * ## Basic Usage
19
+ *
20
+ * ```html
21
+ * <regular-layout>
22
+ * <regular-layout-frame slot="sidebar">Sidebar content</regular-layout-frame>
23
+ * <regular-layout-frame slot="main">Main content</regular-layout-frame>
24
+ * </regular-layout>
25
+ * ```
26
+ *
27
+ * ```typescript
28
+ * import { RegularLayout } from 'regular-layout';
29
+ *
30
+ * const layout = document.querySelector('regular-layout');
31
+ *
32
+ * // Insert panels into the grid layout.
33
+ * layout.insertPanel('sidebar', [0]);
34
+ * layout.insertPanel('main', [1]);
35
+ *
36
+ * // Remove a panel (DOM child remains connected, but not slotted).
37
+ * layout.removePanel('sidebar');
38
+ *
39
+ * // Save current layout state.
40
+ * const state = layout.save();
41
+ * localStorage.setItem('layout', JSON.stringify(state));
42
+ *
43
+ * // Restore layout later.
44
+ * const savedState = JSON.parse(localStorage.getItem('layout'));
45
+ * layout.restore(savedState);
46
+ * ```
47
+ *
48
+ * ## Core Components
49
+ *
50
+ * - {@link RegularLayout}: The main `<regular-layout>` custom element.
51
+ * - {@link RegularLayoutFrame}: Optional frame component with titlebar support.
52
+ *
53
+ * ## Type Definitions
54
+ *
55
+ * - {@link Layout}: The layout tree structure for panel configuration.
56
+ * - {@link LayoutPath}: Information about a panel's position and viewport.
57
+ * - {@link LayoutDivider}: Information about a resizable divider.
58
+ *
59
+ * @packageDocumentation
60
+ */
61
+
62
+ export type {
63
+ LayoutPath,
64
+ Layout,
65
+ LayoutDivider,
66
+ } from "./common/layout_config.ts";
67
+
68
+ export { RegularLayout } from "./regular-layout.ts";
69
+ export { RegularLayoutFrame } from "./regular-layout-frame.ts";
70
+
71
+ // Side effects
72
+ import "./extensions.ts";
@@ -0,0 +1,114 @@
1
+ // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
2
+ // ░░░░░░░░▄▀░█▀▄░█▀▀░█▀▀░█░█░█░░░█▀█░█▀▄░░░░░█░░░█▀█░█░█░█▀█░█░█░▀█▀░▀▄░░░░░░░░
3
+ // ░░░░░░░▀▄░░█▀▄░█▀▀░█░█░█░█░█░░░█▀█░█▀▄░▀▀▀░█░░░█▀█░░█░░█░█░█░█░░█░░░▄▀░░░░░░░
4
+ // ░░░░░░░░░▀░▀░▀░▀▀▀░▀▀▀░▀▀▀░▀▀▀░▀░▀░▀░▀░░░░░▀▀▀░▀░▀░░▀░░▀▀▀░▀▀▀░░▀░░▀░░░░░░░░░
5
+ // ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
6
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
7
+ // ┃ * Copyright (c) 2026, the Regular Layout Authors. This file is part * ┃
8
+ // ┃ * of the Regular Layout library, distributed under the terms of the * ┃
9
+ // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
+
12
+ import type { LayoutPath } from "./common/layout_config.ts";
13
+ import type { RegularLayout } from "./regular-layout.ts";
14
+
15
+ const CSS = `
16
+ :host{--titlebar--height:24px;box-sizing:border-box}
17
+ :host([slot]){margin-top:calc(var(--titlebar--height) + 3px)!important;}
18
+ :host([slot])::part(container){position:absolute;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;background-color:inherit;border-radius:inherit}
19
+ :host([slot])::part(titlebar){height:var(--titlebar--height);margin-top:calc(-2px - var(--titlebar--height));user-select: none;}
20
+ :host([slot])::part(body){flex:1 1 auto;}
21
+ `;
22
+
23
+ /**
24
+ * A custom element that represents a draggable panel within a
25
+ * `<regular-layout>`.
26
+ *
27
+ * `<regular-layout-frame>` is optional - you may also use a `<regular-layout>`
28
+ * with just plain `<div>` children (for example), but panels will not be
29
+ * moveable within the layout unless you manually call `setOverlayState` and
30
+ * `clearOverlayState` (or otherwise impement panel moving via the
31
+ * `<regular-layout>` API).
32
+ *
33
+ * `<regular-layout-frame>` simple and highly customizable implementations
34
+ * based on [CSS `part`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::part)
35
+ * for custom styling, and symmetric
36
+ * [named `slot`s](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_templates_and_slots)
37
+ * for wholesale replacement of the underlying Shadow DOM.
38
+ *
39
+ * @example
40
+ * ```html
41
+ * <regular-layout>
42
+ * <regular-layout-frame slot="panel-1">
43
+ * <!-- Panel content here -->
44
+ * </regular-layout-frame>
45
+ * </regular-layout>
46
+ * ```
47
+ */
48
+ export class RegularLayoutFrame extends HTMLElement {
49
+ private _shadowRoot: ShadowRoot;
50
+ private _container_sheet: CSSStyleSheet;
51
+ private _layout!: RegularLayout;
52
+ private _header!: HTMLElement;
53
+ private _drag_state: LayoutPath<DOMRect> | null = null;
54
+ constructor() {
55
+ super();
56
+ this._container_sheet = new CSSStyleSheet();
57
+ this._container_sheet.replaceSync(CSS);
58
+ this._shadowRoot = this.attachShadow({ mode: "open" });
59
+ this._shadowRoot.adoptedStyleSheets = [this._container_sheet];
60
+ this._shadowRoot.innerHTML = `<slot part="container"><slot part="titlebar">header</slot><slot part="body"><slot></slot></slot></slot>`;
61
+ this._layout = this.parentElement as RegularLayout;
62
+ this._header = this._shadowRoot.children[0].children[0] as HTMLElement;
63
+ }
64
+
65
+ connectedCallback() {
66
+ this._header.addEventListener("pointerdown", this.onPointerDown);
67
+ this._header.addEventListener("pointermove", this.onPointerMove);
68
+ this._header.addEventListener("pointerup", this.onPointerUp);
69
+ }
70
+
71
+ disconnectedCallback() {
72
+ this._header.removeEventListener("pointerdown", this.onPointerDown);
73
+ this._header.removeEventListener("pointermove", this.onPointerMove);
74
+ this._header.removeEventListener("pointerup", this.onPointerUp);
75
+ }
76
+
77
+ private onPointerDown = (event: PointerEvent): void => {
78
+ this._drag_state = this._layout.calculateIntersect(
79
+ event.clientX,
80
+ event.clientY,
81
+ );
82
+
83
+ if (!this._drag_state) {
84
+ return;
85
+ }
86
+
87
+ this._header.setPointerCapture(event.pointerId);
88
+ event.preventDefault();
89
+ event.stopImmediatePropagation();
90
+ };
91
+
92
+ private onPointerMove = (event: PointerEvent): void => {
93
+ if (this._drag_state) {
94
+ this._layout.setOverlayState(
95
+ event.clientX,
96
+ event.clientY,
97
+ this._drag_state,
98
+ );
99
+ }
100
+ };
101
+
102
+ private onPointerUp = (event: PointerEvent): void => {
103
+ if (this._drag_state) {
104
+ this._layout.clearOverlayState(
105
+ event.clientX,
106
+ event.clientY,
107
+ this._drag_state,
108
+ );
109
+
110
+ this._header.releasePointerCapture(event.pointerId);
111
+ this._drag_state = null;
112
+ }
113
+ };
114
+ }