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,53 @@
1
+ import { calculate_intersection } from "./calculate_intersect";
2
+ import { insert_child } from "./insert_child";
3
+ import {
4
+ SPLIT_EDGE_TOLERANCE,
5
+ type Layout,
6
+ type LayoutPath,
7
+ } from "./layout_config";
8
+
9
+ export function calculate_split(
10
+ col: number,
11
+ row: number,
12
+ panel: Layout,
13
+ slot: string,
14
+ config: LayoutPath,
15
+ ): LayoutPath {
16
+ if (
17
+ config.column_offset < SPLIT_EDGE_TOLERANCE ||
18
+ config.column_offset > 1 - SPLIT_EDGE_TOLERANCE
19
+ ) {
20
+ if (config.orientation === "vertical") {
21
+ const new_panel = insert_child(panel, slot, [
22
+ ...config.path,
23
+ config.column_offset < SPLIT_EDGE_TOLERANCE ? 0 : 1,
24
+ ]);
25
+
26
+ config = calculate_intersection(col, row, new_panel, false);
27
+ } else {
28
+ const new_panel = insert_child(panel, slot, config.path);
29
+ config = calculate_intersection(col, row, new_panel, false);
30
+ }
31
+ }
32
+
33
+ if (
34
+ (config.row_offset < SPLIT_EDGE_TOLERANCE &&
35
+ config.row_offset < config.column_offset) ||
36
+ (config.row_offset > 1 - SPLIT_EDGE_TOLERANCE &&
37
+ config.row_offset > config.column_offset)
38
+ ) {
39
+ if (config.orientation === "horizontal") {
40
+ const new_panel = insert_child(panel, slot, [
41
+ ...config.path,
42
+ config.row_offset < SPLIT_EDGE_TOLERANCE ? 0 : 1,
43
+ ]);
44
+
45
+ config = calculate_intersection(col, row, new_panel, false);
46
+ } else {
47
+ const new_panel = insert_child(panel, slot, config.path);
48
+ config = calculate_intersection(col, row, new_panel, false);
49
+ }
50
+ }
51
+
52
+ return config;
53
+ }
@@ -0,0 +1,57 @@
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
+
14
+ /**
15
+ * Flattens the layout tree by merging parent and child split panels that have
16
+ * the same orientation and type.
17
+ *
18
+ * When a split panel contains a child split panel with the same orientation,
19
+ * they are merged into a single split panel. The sizes array is scaled
20
+ * correctly to maintain proportions.
21
+ *
22
+ * @param layout - The layout tree to flatten
23
+ * @returns A new flattened layout tree (original is not mutated).
24
+ */
25
+ export function flatten(layout: Layout): Layout {
26
+ if (layout.type === "child-panel") {
27
+ return layout;
28
+ }
29
+
30
+ const flattenedChildren: Layout[] = [];
31
+ const flattenedSizes: number[] = [];
32
+ for (let i = 0; i < layout.children.length; i++) {
33
+ const child = layout.children[i];
34
+ const childSize = layout.sizes[i];
35
+ const flattenedChild = flatten(child);
36
+
37
+ if (
38
+ flattenedChild.type === "split-panel" &&
39
+ flattenedChild.orientation === layout.orientation
40
+ ) {
41
+ for (let j = 0; j < flattenedChild.children.length; j++) {
42
+ flattenedChildren.push(flattenedChild.children[j]);
43
+ flattenedSizes.push(flattenedChild.sizes[j] * childSize);
44
+ }
45
+ } else {
46
+ flattenedChildren.push(flattenedChild);
47
+ flattenedSizes.push(childSize);
48
+ }
49
+ }
50
+
51
+ return {
52
+ type: "split-panel",
53
+ orientation: layout.orientation,
54
+ children: flattenedChildren,
55
+ sizes: flattenedSizes,
56
+ };
57
+ }
@@ -0,0 +1,249 @@
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 { GRID_TRACK_COLLAPSE_TOLERANCE, type Layout } from "./layout_config.ts";
13
+ import { remove_child } from "./remove_child.ts";
14
+
15
+ interface GridCell {
16
+ child: string;
17
+ colStart: number;
18
+ colEnd: number;
19
+ rowStart: number;
20
+ rowEnd: number;
21
+ }
22
+
23
+ function dedupePositions(positions: number[]): number[] {
24
+ if (positions.length === 0) {
25
+ return [];
26
+ }
27
+
28
+ const sorted = positions.sort((a, b) => a - b);
29
+ const result = [sorted[0]];
30
+ for (let i = 1; i < sorted.length; i++) {
31
+ if (
32
+ Math.abs(sorted[i] - result[result.length - 1]) >
33
+ GRID_TRACK_COLLAPSE_TOLERANCE
34
+ ) {
35
+ result.push(sorted[i]);
36
+ }
37
+ }
38
+
39
+ return result;
40
+ }
41
+
42
+ function collectTrackPositions(
43
+ panel: Layout,
44
+ orientation: "horizontal" | "vertical",
45
+ start: number,
46
+ end: number,
47
+ ): number[] {
48
+ if (panel.type === "child-panel") {
49
+ return [start, end];
50
+ }
51
+
52
+ if (panel.orientation === orientation) {
53
+ const positions: number[] = [start, end];
54
+ let current = start;
55
+ for (let i = 0; i < panel.children.length; i++) {
56
+ const size = panel.sizes[i];
57
+ const childPositions = collectTrackPositions(
58
+ panel.children[i],
59
+ orientation,
60
+ current,
61
+ current + size * (end - start),
62
+ );
63
+
64
+ positions.push(...childPositions);
65
+ current = current + size * (end - start);
66
+ }
67
+
68
+ return dedupePositions(positions);
69
+ } else {
70
+ const allPositions: number[] = [start, end];
71
+ for (const child of panel.children) {
72
+ const childPositions = collectTrackPositions(
73
+ child,
74
+ orientation,
75
+ start,
76
+ end,
77
+ );
78
+
79
+ allPositions.push(...childPositions);
80
+ }
81
+
82
+ return dedupePositions(allPositions);
83
+ }
84
+ }
85
+
86
+ function findTrackIndex(positions: number[], value: number): number {
87
+ for (let i = 0; i < positions.length; i++) {
88
+ if (Math.abs(positions[i] - value) < 0.0001) {
89
+ return i;
90
+ }
91
+ }
92
+
93
+ throw new Error(`Position ${value} not found in ${positions}`);
94
+ }
95
+
96
+ function buildCells(
97
+ panel: Layout,
98
+ colPositions: number[],
99
+ rowPositions: number[],
100
+ colStart: number,
101
+ colEnd: number,
102
+ rowStart: number,
103
+ rowEnd: number,
104
+ ): GridCell[] {
105
+ if (panel.type === "child-panel") {
106
+ return [
107
+ {
108
+ child: panel.child,
109
+ colStart: findTrackIndex(colPositions, colStart),
110
+ colEnd: findTrackIndex(colPositions, colEnd),
111
+ rowStart: findTrackIndex(rowPositions, rowStart),
112
+ rowEnd: findTrackIndex(rowPositions, rowEnd),
113
+ },
114
+ ];
115
+ }
116
+
117
+ const cells: GridCell[] = [];
118
+ const { children, sizes, orientation } = panel;
119
+ if (orientation === "horizontal") {
120
+ let current = colStart;
121
+ for (let i = 0; i < children.length; i++) {
122
+ const next = current + sizes[i] * (colEnd - colStart);
123
+ cells.push(
124
+ ...buildCells(
125
+ children[i],
126
+ colPositions,
127
+ rowPositions,
128
+ current,
129
+ next,
130
+ rowStart,
131
+ rowEnd,
132
+ ),
133
+ );
134
+
135
+ current = next;
136
+ }
137
+ } else {
138
+ let current = rowStart;
139
+ for (let i = 0; i < children.length; i++) {
140
+ const next = current + sizes[i] * (rowEnd - rowStart);
141
+ cells.push(
142
+ ...buildCells(
143
+ children[i],
144
+ colPositions,
145
+ rowPositions,
146
+ colStart,
147
+ colEnd,
148
+ current,
149
+ next,
150
+ ),
151
+ );
152
+
153
+ current = next;
154
+ }
155
+ }
156
+
157
+ return cells;
158
+ }
159
+
160
+ const host_template = (rowTemplate: string, colTemplate: string) =>
161
+ `:host { display: grid; gap: 0px; grid-template-rows: ${rowTemplate}; grid-template-columns: ${colTemplate}; }`;
162
+
163
+ const child_template = (slot: string, rowPart: string, colPart: string) =>
164
+ `:host ::slotted([slot=${slot}]) { grid-column: ${colPart}; grid-row: ${rowPart}; }`;
165
+
166
+ /**
167
+ * Generates CSS Grid styles to render a layout tree.
168
+ * Creates grid-template-rows, grid-template-columns, and positioning rules for
169
+ * all child panels.
170
+ *
171
+ * @param layout - The layout tree to convert to CSS
172
+ * @param round - If true, rounds percentages to whole numbers. Useful for
173
+ * avoiding sub-pixel rendering issues. Defaults to false.
174
+ * @returns CSS string containing :host and ::slotted rules implementing the
175
+ * layout.
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * const layout = {
180
+ * type: "split-panel",
181
+ * orientation: "horizontal",
182
+ * children: [
183
+ * { type: "child-panel", child: "sidebar" },
184
+ * { type: "child-panel", child: "main" }
185
+ * ],
186
+ * sizes: [0.25, 0.75]
187
+ * };
188
+ *
189
+ * const css = create_css_grid_layout(layout);
190
+ * // Returns CSS like:
191
+ * // :host { display: grid; grid-template-columns: 25% 75%; ... }
192
+ * // :host ::slotted([slot=sidebar]) { grid-column: 1; grid-row: 1; }
193
+ * // :host ::slotted([slot=main]) { grid-column: 2; grid-row: 1; }
194
+ * ```
195
+ */
196
+ export function create_css_grid_layout(
197
+ layout: Layout,
198
+ round: boolean = false,
199
+ overlay?: [string, string],
200
+ ): string {
201
+ if (overlay) {
202
+ layout = remove_child(layout, overlay[0]);
203
+ }
204
+
205
+ if (layout.type === "child-panel") {
206
+ return `${host_template("100%", "100%")}\n${child_template(layout.child, "1", "1")}`;
207
+ }
208
+
209
+ const colPositions = collectTrackPositions(layout, "horizontal", 0, 1);
210
+ const colSizes: number[] = [];
211
+ for (let i = 0; i < colPositions.length - 1; i++) {
212
+ colSizes.push(colPositions[i + 1] - colPositions[i]);
213
+ }
214
+
215
+ const colTemplate = colSizes
216
+ .map((s) => `${round ? Math.round(s * 100) : s * 100}%`)
217
+ .join(" ");
218
+
219
+ const rowPositions = collectTrackPositions(layout, "vertical", 0, 1);
220
+ const rowSizes: number[] = [];
221
+ for (let i = 0; i < rowPositions.length - 1; i++) {
222
+ rowSizes.push(rowPositions[i + 1] - rowPositions[i]);
223
+ }
224
+
225
+ const rowTemplate = rowSizes
226
+ .map((s) => `${round ? Math.round(s * 100) : s * 100}%`)
227
+ .join(" ");
228
+
229
+ const cells = buildCells(layout, colPositions, rowPositions, 0, 1, 0, 1);
230
+ const css = [host_template(rowTemplate, colTemplate)];
231
+ for (const cell of cells) {
232
+ const colPart =
233
+ cell.colEnd - cell.colStart === 1
234
+ ? `${cell.colStart + 1}`
235
+ : `${cell.colStart + 1} / ${cell.colEnd + 1}`;
236
+ const rowPart =
237
+ cell.rowEnd - cell.rowStart === 1
238
+ ? `${cell.rowStart + 1}`
239
+ : `${cell.rowStart + 1} / ${cell.rowEnd + 1}`;
240
+
241
+ css.push(`${child_template(cell.child, rowPart, colPart)}`);
242
+ if (cell.child === overlay?.[1]) {
243
+ css.push(`${child_template(overlay[0], rowPart, colPart)}`);
244
+ css.push(`:host ::slotted([slot=${overlay[0]}]) { z-index: 1; }`);
245
+ }
246
+ }
247
+
248
+ return css.join("\n");
249
+ }
@@ -0,0 +1,25 @@
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 "./layout_config";
13
+
14
+ export function updateOverlaySheet({
15
+ view_window: { row_start, row_end, col_start, col_end },
16
+ box,
17
+ }: LayoutPath<DOMRect>) {
18
+ const margin = 0;
19
+ const top = row_start * box.height + margin / 2;
20
+ const left = col_start * box.width + margin / 2;
21
+ const height = (row_end - row_start) * box.height - margin;
22
+ const width = (col_end - col_start) * box.width - margin;
23
+ const css = `position:absolute!important;z-index:1;top:${top}px;left:${left}px;height:${height}px;width:${width}px;`;
24
+ return `::slotted(:not([slot])){${css}}`;
25
+ }
@@ -0,0 +1,129 @@
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
+
14
+ /**
15
+ * Inserts a new child panel into the layout tree at a specified location.
16
+ * Creates a split panel if necessary and redistributes sizes equally among all
17
+ * children.
18
+ *
19
+ * @param panel - The root layout tree to insert into
20
+ * @param child - Unique identifier for the new child panel
21
+ * @param path - Array of indices defining where to insert. Empty array inserts
22
+ * at root level.
23
+ * @param orientation - Orientation for newly created split panels. Defaults to
24
+ * "horizontal".
25
+ * @returns A new layout tree with the child inserted (original is not mutated).
26
+ */
27
+ export function insert_child(
28
+ panel: Layout,
29
+ child: string,
30
+ path: number[],
31
+ orientation: "horizontal" | "vertical" = "horizontal",
32
+ ): Layout {
33
+ if (path.length === 0) {
34
+ // Insert at root level
35
+ if (panel.type === "child-panel") {
36
+ // Convert single child-panel to split-panel with two children
37
+ return {
38
+ type: "split-panel",
39
+ orientation,
40
+ children: [
41
+ panel,
42
+ {
43
+ type: "child-panel",
44
+ child,
45
+ },
46
+ ],
47
+ sizes: [0.5, 0.5],
48
+ };
49
+ } else {
50
+ // Append to existing split-panel
51
+ const newChildren = [
52
+ ...panel.children,
53
+ {
54
+ type: "child-panel",
55
+ child,
56
+ } as Layout,
57
+ ];
58
+
59
+ const numChildren = newChildren.length;
60
+ const newSizes = Array(numChildren).fill(1 / numChildren);
61
+ return {
62
+ ...panel,
63
+ children: newChildren,
64
+ sizes: newSizes,
65
+ };
66
+ }
67
+ }
68
+
69
+ // Navigate down the path
70
+ const [index, ...restPath] = path;
71
+ if (panel.type === "child-panel") {
72
+ // This shouldn't happen if path.length > 0, but handle it gracefully
73
+ // We need to split this child-panel
74
+ const newPanel: Layout = {
75
+ type: "split-panel",
76
+ orientation,
77
+ children: [panel],
78
+ sizes: [1],
79
+ };
80
+
81
+ return insert_child(newPanel, child, path, orientation);
82
+ }
83
+
84
+ if (restPath.length === 0 || index === panel.children.length) {
85
+ // Insert at this level at the specified index
86
+ const newChildren = [...panel.children];
87
+ newChildren.splice(index, 0, {
88
+ type: "child-panel",
89
+ child,
90
+ });
91
+
92
+ const numChildren = newChildren.length;
93
+ const newSizes = Array(numChildren).fill(1 / numChildren);
94
+ return {
95
+ ...panel,
96
+ children: newChildren,
97
+ sizes: newSizes,
98
+ };
99
+ }
100
+
101
+ const targetChild = panel.children[index];
102
+ if (targetChild.type === "child-panel" && restPath.length > 0) {
103
+ // Need to split this child-panel
104
+ const oppositeOrientation =
105
+ panel.orientation === "horizontal" ? "vertical" : "horizontal";
106
+
107
+ const newSplitPanel = insert_child(
108
+ targetChild,
109
+ child,
110
+ restPath,
111
+ oppositeOrientation,
112
+ );
113
+
114
+ const newChildren = [...panel.children];
115
+ newChildren[index] = newSplitPanel;
116
+ return {
117
+ ...panel,
118
+ children: newChildren,
119
+ };
120
+ }
121
+
122
+ const updatedChild = insert_child(targetChild, child, restPath, orientation);
123
+ const newChildren = [...panel.children];
124
+ newChildren[index] = updatedChild;
125
+ return {
126
+ ...panel,
127
+ children: newChildren,
128
+ };
129
+ }
@@ -0,0 +1,127 @@
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
+ * The percentage of the maximum resize distance that will be clamped.
14
+ *
15
+ */
16
+ export const MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD = 0.15;
17
+
18
+ /**
19
+ * Threshold from panel edge that is considered a split vs drop action.
20
+ */
21
+ export const SPLIT_EDGE_TOLERANCE = 0.15;
22
+
23
+ /**
24
+ * Tolerance threshold for considering two grid track positions as identical.
25
+ *
26
+ * When collecting and deduplicating track positions, any positions closer than
27
+ * this value are treated as the same position to avoid redundant grid tracks.
28
+ */
29
+ export const GRID_TRACK_COLLAPSE_TOLERANCE = 0.0001;
30
+
31
+ /**
32
+ * The representation of a CSS grid, in JSON form.
33
+ */
34
+ export type Layout = SplitLayout | TabLayout;
35
+
36
+ /**
37
+ * The orientation (of a `SplitPanel`).
38
+ */
39
+ export type Orientation = "horizontal" | "vertical";
40
+
41
+ /**
42
+ * A logical rectange in percent-coordinates (of a (1, 1) square).
43
+ */
44
+ export interface ViewWindow {
45
+ row_start: number;
46
+ row_end: number;
47
+ col_start: number;
48
+ col_end: number;
49
+ }
50
+
51
+ /**
52
+ * A split panel that divides space among multiple child layouts
53
+ * .
54
+ * Child panels are arranged either horizontally (side by side) or vertically
55
+ * (stacked), via the `orientation` property `"horizzontal"` and `"vertical"`
56
+ * (respectively).
57
+ */
58
+ export interface SplitLayout {
59
+ type: "split-panel";
60
+ children: Layout[];
61
+ sizes: number[];
62
+ orientation: Orientation;
63
+ }
64
+
65
+ /**
66
+ * A leaf panel node that contains a single named child element.
67
+ */
68
+ export interface TabLayout {
69
+ type: "child-panel";
70
+ child: string;
71
+ }
72
+
73
+ /**
74
+ * Represents a draggable divider between two panels in the layout.
75
+ *
76
+ * Used for hit detection.
77
+ */
78
+ export interface LayoutDivider {
79
+ path: number[];
80
+ view_window: ViewWindow;
81
+ type: Orientation;
82
+ }
83
+
84
+ /**
85
+ * Represents a panel location result from hit detection.
86
+ *
87
+ * Contains both the panel identifier and its grid position in relative units.
88
+ * The generic parameter `T` allows DOM-only properties (e.g. `DOMRect`) to be
89
+ * shared in this cross-platform module.
90
+ */
91
+ export interface LayoutPath<T = undefined> {
92
+ type: "layout-path";
93
+ slot: string;
94
+ path: number[];
95
+ view_window: ViewWindow;
96
+ column_offset: number;
97
+ row_offset: number;
98
+ orientation: Orientation;
99
+ box: T;
100
+ }
101
+
102
+ /**
103
+ * Recursively iterates over all child panel names in the layout tree, yielding
104
+ * panel names in depth-first order.
105
+ *
106
+ * @param panel - The layout tree to iterate over
107
+ * @returns Generator yielding child panel names
108
+ */
109
+ export function* iter_panel_children(panel: Layout): Generator<string> {
110
+ if (panel.type === "split-panel") {
111
+ for (const child of panel.children) {
112
+ yield* iter_panel_children(child);
113
+ }
114
+ } else {
115
+ yield panel.child;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * An empty `Layout` with no panels.
121
+ */
122
+ export const EMPTY_PANEL: Layout = {
123
+ type: "split-panel",
124
+ orientation: "horizontal",
125
+ sizes: [],
126
+ children: [],
127
+ };