regular-layout 0.2.2 → 0.4.0

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.
Files changed (47) hide show
  1. package/README.md +110 -22
  2. package/dist/{layout → core}/types.d.ts +14 -4
  3. package/dist/extensions.d.ts +6 -1
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.js +7 -7
  6. package/dist/index.js.map +4 -4
  7. package/dist/layout/calculate_edge.d.ts +4 -4
  8. package/dist/layout/calculate_intersect.d.ts +1 -1
  9. package/dist/layout/calculate_path.d.ts +12 -0
  10. package/dist/layout/calculate_presize_paths.d.ts +10 -0
  11. package/dist/layout/flatten.d.ts +1 -1
  12. package/dist/layout/generate_grid.d.ts +5 -5
  13. package/dist/layout/generate_overlay.d.ts +18 -2
  14. package/dist/layout/insert_child.d.ts +2 -2
  15. package/dist/layout/redistribute_panel_sizes.d.ts +2 -2
  16. package/dist/layout/remove_child.d.ts +1 -1
  17. package/dist/model/overlay_controller.d.ts +34 -0
  18. package/dist/model/presize_queue.d.ts +17 -0
  19. package/dist/regular-layout-tab.d.ts +1 -1
  20. package/dist/regular-layout.d.ts +44 -9
  21. package/package.json +5 -4
  22. package/src/{layout → core}/constants.ts +2 -2
  23. package/src/{layout → core}/types.ts +17 -5
  24. package/src/extensions.ts +13 -1
  25. package/src/index.ts +3 -1
  26. package/src/layout/calculate_edge.ts +17 -8
  27. package/src/layout/calculate_intersect.ts +13 -4
  28. package/src/layout/calculate_path.ts +53 -0
  29. package/src/layout/calculate_presize_paths.ts +93 -0
  30. package/src/layout/flatten.ts +4 -4
  31. package/src/layout/generate_grid.ts +8 -8
  32. package/src/layout/generate_overlay.ts +48 -16
  33. package/src/layout/insert_child.ts +16 -16
  34. package/src/layout/redistribute_panel_sizes.ts +5 -5
  35. package/src/layout/remove_child.ts +6 -6
  36. package/src/model/overlay_controller.ts +162 -0
  37. package/src/model/presize_queue.ts +79 -0
  38. package/src/regular-layout-frame.ts +3 -3
  39. package/src/regular-layout-tab.ts +1 -1
  40. package/src/regular-layout.ts +180 -133
  41. package/themes/borland.css +103 -0
  42. package/themes/chicago.css +55 -49
  43. package/themes/fluxbox.css +64 -60
  44. package/themes/gibson.css +174 -164
  45. package/themes/hotdog.css +53 -47
  46. package/themes/lorax.css +82 -75
  47. /package/dist/{layout → core}/constants.d.ts +0 -0
@@ -0,0 +1,79 @@
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, PresizeDetail } from "../core/types.ts";
13
+ import { calculate_presize_paths } from "../layout/calculate_presize_paths.ts";
14
+
15
+ /**
16
+ * Manages cancelable pre-resize gating for layout updates.
17
+ *
18
+ * Before each layout change, a cancelable `resize-before` event is dispatched
19
+ * on the target. If the event is cancelled via `preventDefault()`, the update
20
+ * is suspended until {@link resume} is called. Concurrent updates are queued
21
+ * and processed sequentially.
22
+ */
23
+ export class PresizeQueue {
24
+ #resizing = false;
25
+ #queued: { layout: Layout; fn: () => void } | null = null;
26
+ #pending: (() => void) | null = null;
27
+
28
+ constructor(
29
+ private _target: EventTarget,
30
+ private _eventName: string,
31
+ ) {}
32
+
33
+ async run(layout: Layout, fn: () => void): Promise<void> {
34
+ if (this.#resizing) {
35
+ this.#queued = { layout, fn };
36
+ return;
37
+ }
38
+
39
+ this.#resizing = true;
40
+ try {
41
+ await this.#dispatchAndMaybeWait(layout, fn);
42
+ while (this.#queued) {
43
+ const { layout: nextLayout, fn: nextFn } = this.#queued;
44
+ this.#queued = null;
45
+ await this.#dispatchAndMaybeWait(nextLayout, nextFn);
46
+ }
47
+ } finally {
48
+ this.#resizing = false;
49
+ }
50
+ }
51
+
52
+ resume(): void {
53
+ if (this.#pending) {
54
+ const resolve = this.#pending;
55
+ this.#pending = null;
56
+ resolve();
57
+ }
58
+ }
59
+
60
+ async #dispatchAndMaybeWait(layout: Layout, fn: () => void): Promise<void> {
61
+ const detail: PresizeDetail = {
62
+ calculatePresizePaths: () => calculate_presize_paths(layout),
63
+ };
64
+
65
+ const event = new CustomEvent(this._eventName, {
66
+ cancelable: true,
67
+ detail,
68
+ });
69
+
70
+ const proceed = this._target.dispatchEvent(event);
71
+ if (!proceed) {
72
+ await new Promise<void>((resolve) => {
73
+ this.#pending = resolve;
74
+ });
75
+ }
76
+
77
+ fn();
78
+ }
79
+ }
@@ -9,7 +9,7 @@
9
9
  // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
10
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
11
 
12
- import type { LayoutPath } from "./layout/types.ts";
12
+ import type { LayoutPath } from "./core/types.ts";
13
13
  import type { RegularLayoutEvent } from "./extensions.ts";
14
14
  import type { RegularLayout } from "./regular-layout.ts";
15
15
  import type { RegularLayoutTab } from "./regular-layout-tab.ts";
@@ -26,7 +26,7 @@ const CSS = `
26
26
 
27
27
  const HTML_TEMPLATE = `
28
28
  <div part="titlebar"></div>
29
- <slot part="container"></slot>
29
+ <div part="container"><slot></slot></div>
30
30
  `;
31
31
 
32
32
  type DragState = { moved?: boolean; path: LayoutPath };
@@ -162,7 +162,7 @@ export class RegularLayoutFrame extends HTMLElement {
162
162
  let new_tab_panel = this._layout.getPanel(slot, new_panel);
163
163
  if (!new_tab_panel) {
164
164
  new_tab_panel = {
165
- type: "child-panel",
165
+ type: "tab-layout",
166
166
  tabs: [slot],
167
167
  selected: 0,
168
168
  };
@@ -9,7 +9,7 @@
9
9
  // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
10
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
11
 
12
- import type { TabLayout } from "./layout/types.ts";
12
+ import type { TabLayout } from "./core/types.ts";
13
13
  import type { RegularLayout } from "./regular-layout.ts";
14
14
 
15
15
  /**
@@ -16,7 +16,7 @@
16
16
  * @packageDocumentation
17
17
  */
18
18
 
19
- import { EMPTY_PANEL } from "./layout/types.ts";
19
+ import { EMPTY_PANEL } from "./core/types.ts";
20
20
  import { create_css_grid_layout } from "./layout/generate_grid.ts";
21
21
  import type {
22
22
  LayoutPath,
@@ -25,19 +25,28 @@ import type {
25
25
  TabLayout,
26
26
  OverlayMode,
27
27
  Orientation,
28
- } from "./layout/types.ts";
28
+ LayoutPathTraversal,
29
+ ViewWindow,
30
+ } from "./core/types.ts";
31
+
29
32
  import { calculate_intersection } from "./layout/calculate_intersect.ts";
30
33
  import { remove_child } from "./layout/remove_child.ts";
31
34
  import { insert_child } from "./layout/insert_child.ts";
32
35
  import { redistribute_panel_sizes } from "./layout/redistribute_panel_sizes.ts";
33
- import { updateOverlaySheet } from "./layout/generate_overlay.ts";
34
- import { calculate_edge } from "./layout/calculate_edge.ts";
36
+ import { viewWindowToLocalRect } from "./layout/generate_overlay.ts";
35
37
  import { flatten } from "./layout/flatten.ts";
38
+ import { calculate_path } from "./layout/calculate_path.ts";
39
+ import { PresizeQueue } from "./model/presize_queue.ts";
40
+ import {
41
+ OverlayController,
42
+ type OverlayHost,
43
+ } from "./model/overlay_controller.ts";
44
+
36
45
  import {
37
46
  DEFAULT_PHYSICS,
38
47
  type PhysicsUpdate,
39
48
  type Physics,
40
- } from "./layout/constants.ts";
49
+ } from "./core/constants.ts";
41
50
 
42
51
  /**
43
52
  * An interface which models the fields of `PointerEvent` that
@@ -107,6 +116,8 @@ export class RegularLayout extends HTMLElement {
107
116
  private _cursor_override: boolean;
108
117
  private _dimensions?: { box: DOMRect; style: CSSStyleDeclaration };
109
118
  private _physics: Physics;
119
+ private _presizeQueue: PresizeQueue;
120
+ private _overlayController: OverlayController;
110
121
 
111
122
  constructor() {
112
123
  super();
@@ -117,6 +128,9 @@ export class RegularLayout extends HTMLElement {
117
128
  this._stylesheet = new CSSStyleSheet();
118
129
  this._cursor_stylesheet = new CSSStyleSheet();
119
130
  this._cursor_override = false;
131
+ const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-before-resize`;
132
+ this._presizeQueue = new PresizeQueue(this, event_name);
133
+ this._overlayController = new OverlayController(this.create_overlay_host());
120
134
  this._shadowRoot.adoptedStyleSheets = [
121
135
  this._stylesheet,
122
136
  this._cursor_stylesheet,
@@ -151,6 +165,16 @@ export class RegularLayout extends HTMLElement {
151
165
  return calculate_intersection(col, row, this._panel);
152
166
  };
153
167
 
168
+ /**
169
+ * Calculates the index path for a panel with the given name.
170
+ *
171
+ * @param name - The name of the panel to find.
172
+ * @returns The panel's index path if found, `null` otherwise.
173
+ */
174
+ calculatePath = (name: string): number[] | null => {
175
+ return calculate_path(name, this._panel);
176
+ };
177
+
154
178
  /**
155
179
  * Sets the visual overlay state during drag-and-drop operations.
156
180
  * Displays a preview of where a panel would be placed at the given coordinates.
@@ -165,54 +189,13 @@ export class RegularLayout extends HTMLElement {
165
189
  * the target, "absolute" positions the panel absolutely. Defaults to
166
190
  * "absolute".
167
191
  */
168
- setOverlayState = (
192
+ setOverlayState = async (
169
193
  event: PointerEventCoordinates,
170
- { slot }: LayoutPath,
171
- className: string = this._physics.OVERLAY_CLASSNAME,
172
- mode: OverlayMode = this._physics.OVERLAY_DEFAULT,
194
+ target: LayoutPath,
195
+ className?: string,
196
+ mode?: OverlayMode,
173
197
  ) => {
174
- const panel = remove_child(this._panel, slot);
175
- const query = `:scope > [${this._physics.CHILD_ATTRIBUTE_NAME}="${slot}"]`;
176
- const drag_element = this.querySelector(query);
177
- if (drag_element) {
178
- drag_element.classList.add(className);
179
- }
180
-
181
- // TODO: Don't recalculate box (but this currently protects against resize).
182
- const [col, row, box, style] = this.relativeCoordinates(event, true);
183
- let drop_target = calculate_intersection(col, row, panel);
184
- if (drop_target) {
185
- drop_target = calculate_edge(
186
- col,
187
- row,
188
- panel,
189
- slot,
190
- drop_target,
191
- box,
192
- this._physics,
193
- );
194
- }
195
-
196
- if (mode === "grid" && drop_target) {
197
- const path: [string, string] = [slot, drop_target?.slot];
198
- const css = create_css_grid_layout(panel, path, this._physics);
199
- this._stylesheet.replaceSync(css);
200
- } else if (mode === "absolute") {
201
- const grid_css = create_css_grid_layout(panel, undefined, this._physics);
202
- const overlay_css = updateOverlaySheet(
203
- slot,
204
- box,
205
- style,
206
- drop_target,
207
- this._physics,
208
- );
209
-
210
- this._stylesheet.replaceSync([grid_css, overlay_css].join("\n"));
211
- }
212
-
213
- const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-before-update`;
214
- const custom_event = new CustomEvent<Layout>(event_name, { detail: panel });
215
- this.dispatchEvent(custom_event);
198
+ await this._overlayController.set(event, target, className, mode);
216
199
  };
217
200
 
218
201
  /**
@@ -227,54 +210,12 @@ export class RegularLayout extends HTMLElement {
227
210
  * @param mode - Overlay rendering mode that was used, must match the mode
228
211
  * passed to `setOverlayState`. Defaults to "absolute".
229
212
  */
230
- clearOverlayState = (
213
+ clearOverlayState = async (
231
214
  event: PointerEventCoordinates | null,
232
- { slot, layout }: LayoutPath,
233
- className: string = this._physics.OVERLAY_CLASSNAME,
215
+ target: LayoutPath,
216
+ className?: string,
234
217
  ) => {
235
- let panel = this._panel;
236
- panel = remove_child(panel, slot);
237
- const query = `:scope > [${this._physics.CHILD_ATTRIBUTE_NAME}="${slot}"]`;
238
- const drag_element = this.querySelector(query);
239
- if (drag_element) {
240
- drag_element.classList.remove(className);
241
- }
242
-
243
- if (event === null) {
244
- this.restore(layout);
245
- return;
246
- }
247
-
248
- const [col, row, box] = this.relativeCoordinates(event, false);
249
- let drop_target = calculate_intersection(col, row, panel);
250
- if (drop_target) {
251
- drop_target = calculate_edge(
252
- col,
253
- row,
254
- panel,
255
- slot,
256
- drop_target,
257
- box,
258
- this._physics,
259
- );
260
- }
261
-
262
- if (drop_target) {
263
- const orientation = drop_target?.is_edge
264
- ? drop_target.orientation
265
- : undefined;
266
-
267
- const new_layout = insert_child(
268
- panel,
269
- slot,
270
- drop_target.path,
271
- orientation,
272
- );
273
-
274
- this.restore(new_layout);
275
- } else {
276
- this.restore(layout);
277
- }
218
+ await this._overlayController.clear(event, target, className);
278
219
  };
279
220
 
280
221
  /**
@@ -288,9 +229,9 @@ export class RegularLayout extends HTMLElement {
288
229
  * the new `SplitPanel` _if_ there is an option of orientation (e.g. if
289
230
  * the layout had no pre-existing `SplitPanel`)
290
231
  */
291
- insertPanel = (
232
+ insertPanel = async (
292
233
  name: string,
293
- path: number[] = [],
234
+ path: LayoutPathTraversal = [],
294
235
  split?: boolean | Orientation,
295
236
  ) => {
296
237
  let orientation: Orientation | undefined;
@@ -300,7 +241,7 @@ export class RegularLayout extends HTMLElement {
300
241
  orientation = split;
301
242
  }
302
243
 
303
- this.restore(insert_child(this._panel, name, path, orientation));
244
+ await this.restore(insert_child(this._panel, name, path, orientation));
304
245
  };
305
246
 
306
247
  /**
@@ -308,8 +249,8 @@ export class RegularLayout extends HTMLElement {
308
249
  *
309
250
  * @param name - Name of the panel to remove
310
251
  */
311
- removePanel = (name: string) => {
312
- this.restore(remove_child(this._panel, name));
252
+ removePanel = async (name: string) => {
253
+ await this.restore(remove_child(this._panel, name));
313
254
  };
314
255
 
315
256
  /**
@@ -320,7 +261,7 @@ export class RegularLayout extends HTMLElement {
320
261
  * @returns The TabLayout containing the panel if found, null otherwise.
321
262
  */
322
263
  getPanel = (name: string, layout: Layout = this._panel): TabLayout | null => {
323
- if (layout.type === "child-panel") {
264
+ if (layout.type === "tab-layout") {
324
265
  if (layout.tabs.includes(name)) {
325
266
  return layout;
326
267
  }
@@ -341,29 +282,61 @@ export class RegularLayout extends HTMLElement {
341
282
  /**
342
283
  * Clears the entire layout, unslotting all panels.
343
284
  */
344
- clear = () => {
345
- this.restore(EMPTY_PANEL);
285
+ clear = async () => {
286
+ await this.restore(EMPTY_PANEL);
287
+ };
288
+
289
+ /**
290
+ * Restores the layout from a saved state synchronously, without
291
+ * dispatching the `regular-layout-before-resize` event.
292
+ *
293
+ * @param layout - The layout tree to restore
294
+ */
295
+ restoreSync = (layout: Layout, _is_flattened: boolean = false) => {
296
+ this._panel = !_is_flattened ? flatten(layout) : layout;
297
+ const css = create_css_grid_layout(this._panel, undefined, this._physics);
298
+ this._stylesheet.replaceSync(css);
299
+ const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-update`;
300
+ const event = new CustomEvent<Layout>(event_name, { detail: this._panel });
301
+ this.dispatchEvent(event);
346
302
  };
347
303
 
348
304
  /**
349
305
  * Restores the layout from a saved state.
350
306
  *
307
+ * Before applying, dispatches a cancelable `regular-layout-before-resize`
308
+ * event. If the event is cancelled via `preventDefault()`, the layout
309
+ * update is suspended until {@link resumeResize} is called.
310
+ *
351
311
  * @param layout - The layout tree to restore
352
312
  *
353
313
  * @example
354
314
  * ```typescript
355
315
  * const layout = document.querySelector('regular-layout');
356
316
  * const savedState = JSON.parse(localStorage.getItem('layout'));
357
- * layout.restore(savedState);
317
+ * await layout.restore(savedState);
358
318
  * ```
359
319
  */
360
- restore = (layout: Layout, _is_flattened: boolean = false) => {
361
- this._panel = !_is_flattened ? flatten(layout) : layout;
362
- const css = create_css_grid_layout(this._panel, undefined, this._physics);
363
- this._stylesheet.replaceSync(css);
364
- const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-update`;
365
- const event = new CustomEvent<Layout>(event_name, { detail: this._panel });
366
- this.dispatchEvent(event);
320
+ restore = async (layout: Layout, _is_flattened: boolean = false) => {
321
+ const panel = !_is_flattened ? flatten(layout) : layout;
322
+ await this._presizeQueue.run(panel, () => {
323
+ this._panel = panel;
324
+ const css = create_css_grid_layout(this._panel, undefined, this._physics);
325
+ this._stylesheet.replaceSync(css);
326
+ const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-update`;
327
+ const event = new CustomEvent<Layout>(event_name, {
328
+ detail: this._panel,
329
+ });
330
+ this.dispatchEvent(event);
331
+ });
332
+ };
333
+
334
+ /**
335
+ * Resumes a layout update that was suspended by cancelling the
336
+ * `regular-layout-before-resize` event.
337
+ */
338
+ resumeResize = () => {
339
+ this._presizeQueue.resume();
367
340
  };
368
341
 
369
342
  /**
@@ -429,21 +402,53 @@ export class RegularLayout extends HTMLElement {
429
402
 
430
403
  const box = this._dimensions.box;
431
404
  const style = this._dimensions.style;
432
- const col =
433
- (event.clientX - box.left - parseFloat(style.paddingLeft)) /
434
- (box.width -
435
- parseFloat(style.paddingLeft) -
436
- parseFloat(style.paddingRight));
437
-
438
- const row =
439
- (event.clientY - box.top - parseFloat(style.paddingTop)) /
440
- (box.height -
441
- parseFloat(style.paddingTop) -
442
- parseFloat(style.paddingBottom));
443
-
405
+ const paddingLeft = parseFloat(style.paddingLeft);
406
+ const paddingTop = parseFloat(style.paddingTop);
407
+ const contentWidth =
408
+ box.width - paddingLeft - parseFloat(style.paddingRight);
409
+
410
+ const contentHeight =
411
+ box.height - paddingTop - parseFloat(style.paddingBottom);
412
+
413
+ const localX = event.clientX - box.left - paddingLeft;
414
+ const localY = event.clientY - box.top - paddingTop;
415
+ const col = Math.max(0, Math.min(1, localX / contentWidth));
416
+ const row = Math.max(0, Math.min(1, localY / contentHeight));
444
417
  return [col, row, box, style];
445
418
  };
446
419
 
420
+ /**
421
+ * Converts a {@link ViewWindow} (normalized 0–1 coordinates) to a
422
+ * `DOMRect` in screen pixels, accounting for padding and optionally
423
+ * CSS `gap` and child `margin`.
424
+ *
425
+ * @param window - The view window to convert.
426
+ * @returns A `DOMRect` representing the window in screen coordinates.
427
+ */
428
+ realCoordinates = (window: ViewWindow, child?: HTMLElement): DOMRect => {
429
+ if (!this._dimensions) {
430
+ this._dimensions = {
431
+ box: this.getBoundingClientRect(),
432
+ style: getComputedStyle(this),
433
+ };
434
+ }
435
+
436
+ const box = this._dimensions.box;
437
+ const style = this._dimensions.style;
438
+ let childStyle: CSSStyleDeclaration | undefined;
439
+ if (child) {
440
+ childStyle = getComputedStyle(child);
441
+ }
442
+
443
+ const local = viewWindowToLocalRect(window, box, style, childStyle);
444
+ return new DOMRect(
445
+ box.left + local.x,
446
+ box.top + local.y,
447
+ local.width,
448
+ local.height,
449
+ );
450
+ };
451
+
447
452
  /**
448
453
  * Calculates the Euclidean distance in pixels between the current pointer
449
454
  * coordinates and a drag target's position within the layout.
@@ -464,7 +469,47 @@ export class RegularLayout extends HTMLElement {
464
469
  return Math.sqrt(dx ** 2 + dy ** 2);
465
470
  };
466
471
 
467
- private onDblClick = (event: MouseEvent) => {
472
+ // ▇█████████████████■■■■ȺȺȺȺȺ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ȺȺȺȺȺ■■■■■█████████████████
473
+ // ███████▛▀▀█▃▄▄▅▆▆▆▇▇▇██████████████████████████████▇▇▇▆▆▆▅▅▄▃█▀▀▜███████
474
+ // ████▛▚▅▇███▍ ▗▄▃¯"▜██▘ ▜██▍ ▀█▋ ▐█▀▔▂▄▃ ▔▜█ ▗▃▃▃▄▊ ▄▄▃ ▔▜██▇▅▃▀████
475
+ // ███▋╺██████▍ ▐██▋ █▘ ◨Ƚ "██▍ ▙▁ ▀ ▐▋ █▛▀▀▀▜█ ´▂▂Ŋ█▊ °"▔ ▃██████▍▐███
476
+ // ████▄▝▜████▍ ▝▀▀ ▂▟▘ ▄▄▄▄ "█▍ ██▄ ▐█▃ ▝▀▀ ▐█ ▝▀▀▀▀▊ ██▖ ▝████▛▀▄████
477
+ // ██████▇▆▄▃█▀▀Ⱥ▼■███▇▇████▇▇█▇▇████▇████▇▇▇████▇▇▇▇▇▇▇█▇■◘▀▀▀▀▂▃▄▅▇██████
478
+ // ███████████████▇▇▆▆▆▅▅▅▅▅▅▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▅▅▅▅▅▅▆▆▆▇▇███████████████
479
+ //
480
+ //
481
+ // ┏▅▅▖ ▗▅▅▶ ▅▅▅▅▅▅▅ ▗▅▅▅▅▅▅▖ ▅▅▅▅▅▄▄▂ ▃▅▆▆▆▄▁ ┏▅▅▖ ▅▅▖▅▅▅▅▅▅▅▅▅
482
+ // ┣██▌▗██▛ ██▊▔▔▔▔ ▐██▋▔▔▔` ███▍"▜██▙ ▟██▘▔███▖ ▐██▌ ██▌^▔▔███^▔^
483
+ // ┣██▙██▋ ██▊▂▂▂ ▐██▋▂▂▂ ███▍ ▐██▋ ▐███ ▐██▋ ▐██▌ ██▌ ███▎
484
+ // ┣██████▌ ███▀▀▀ ▐███▀▀▘ ██████▛▀ ▐███ ▐██▋ ▐██▌ ██▌ ███▎
485
+ // ┣██▛ ▜██▖ ██▊ ▐██▋ ███▍ ▝███ ▟██▌ ▐██▌ ██▌ ███▎
486
+ // ┣██▌ ███ ███▆▆▆▆▎▐███▆▆▆▅ ███▍ ▀██▙▅██▛ ▝███▅▆██▘ ███▎
487
+ // ▔▔▔` ´▔▔` ▔▔▔▔▔▔▔ ´▔▔▔▔▔▔▔ ▔▔▔ ▔""▔¯ ▔""^▔ ▔▔▔
488
+
489
+ private create_overlay_host(): OverlayHost {
490
+ const self = this;
491
+ return {
492
+ get panel() {
493
+ return self._panel;
494
+ },
495
+ get physics() {
496
+ return self._physics;
497
+ },
498
+ get stylesheet() {
499
+ return self._stylesheet;
500
+ },
501
+ get presizeQueue() {
502
+ return self._presizeQueue;
503
+ },
504
+ relativeCoordinates: (event, recalculate) =>
505
+ self.relativeCoordinates(event, recalculate),
506
+ restore: (layout, isFlattened) => self.restore(layout, isFlattened),
507
+ querySelector: (selectors) => self.querySelector(selectors),
508
+ dispatchEvent: (event) => self.dispatchEvent(event),
509
+ };
510
+ }
511
+
512
+ private onDblClick = async (event: MouseEvent) => {
468
513
  const [col, row, rect] = this.relativeCoordinates(event, false);
469
514
  const divider = calculate_intersection(col, row, this._panel, {
470
515
  rect,
@@ -478,7 +523,7 @@ export class RegularLayout extends HTMLElement {
478
523
  undefined,
479
524
  );
480
525
 
481
- this.restore(panel, true);
526
+ await this.restore(panel, true);
482
527
  }
483
528
  };
484
529
 
@@ -495,15 +540,17 @@ export class RegularLayout extends HTMLElement {
495
540
  }
496
541
  };
497
542
 
498
- private onPointerMove = (event: PointerEvent) => {
543
+ private onPointerMove = async (event: PointerEvent) => {
499
544
  if (this._drag_target) {
500
545
  const [col, row] = this.relativeCoordinates(event, false);
501
546
  const [{ path, type }, old_col, old_row] = this._drag_target;
502
547
  const offset = type === "horizontal" ? old_col - col : old_row - row;
503
548
  const panel = redistribute_panel_sizes(this._panel, path, offset);
504
- this._stylesheet.replaceSync(
505
- create_css_grid_layout(panel, undefined, this._physics),
506
- );
549
+ await this._presizeQueue.run(panel, () => {
550
+ this._stylesheet.replaceSync(
551
+ create_css_grid_layout(panel, undefined, this._physics),
552
+ );
553
+ });
507
554
  }
508
555
 
509
556
  if (this._physics.GRID_DIVIDER_CHECK_TARGET && event.target !== this) {
@@ -533,14 +580,14 @@ export class RegularLayout extends HTMLElement {
533
580
  }
534
581
  };
535
582
 
536
- private onPointerUp = (event: PointerEvent) => {
583
+ private onPointerUp = async (event: PointerEvent) => {
537
584
  if (this._drag_target) {
538
585
  this.releasePointerCapture(event.pointerId);
539
586
  const [col, row] = this.relativeCoordinates(event, false);
540
587
  const [{ path, type }, old_col, old_row] = this._drag_target;
541
588
  const offset = type === "horizontal" ? old_col - col : old_row - row;
542
589
  const panel = redistribute_panel_sizes(this._panel, path, offset);
543
- this.restore(panel, true);
590
+ await this.restore(panel, true);
544
591
  this._drag_target = undefined;
545
592
  }
546
593
  };
@@ -0,0 +1,103 @@
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.borland {
14
+ background-color: #0000aa;
15
+ font-family:
16
+ "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas",
17
+ "Liberation Mono", monospace;
18
+ padding: 16px;
19
+ }
20
+
21
+ /* Frame */
22
+ regular-layout.borland regular-layout-frame {
23
+ position: relative;
24
+ box-sizing: border-box;
25
+ margin: 4px;
26
+ background: #0000aa;
27
+ border: 1px solid #00aaaa;
28
+ box-shadow: 1px 1px 0 #000000;
29
+ }
30
+
31
+ regular-layout.borland regular-layout-frame::part(container) {
32
+ padding: 4px 6px;
33
+ color: #ffff55;
34
+ }
35
+
36
+ regular-layout.borland regular-layout-frame::part(close) {
37
+ background: #00aa00;
38
+ height: 16px;
39
+ align-self: center;
40
+ display: flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ padding: 0 3px;
44
+ }
45
+
46
+ regular-layout.borland regular-layout-frame::part(close):hover {
47
+ background: #00ff00;
48
+ }
49
+
50
+ regular-layout.borland regular-layout-frame::part(close):before {
51
+ content: "[×]";
52
+ font-size: 10px;
53
+ font-weight: bold;
54
+ color: #000000;
55
+ }
56
+
57
+ regular-layout.borland regular-layout-frame::part(titlebar) {
58
+ display: flex;
59
+ align-items: stretch;
60
+ padding-right: 0;
61
+ height: 22px;
62
+ background: #0000aa;
63
+ }
64
+
65
+ regular-layout.borland regular-layout-frame::part(tab) {
66
+ display: flex;
67
+ flex: 1 1 150px;
68
+ align-items: center;
69
+ padding: 0 3px 0 8px;
70
+ cursor: pointer;
71
+ text-overflow: ellipsis;
72
+ background: #0000aa;
73
+ color: #aaaaaa;
74
+ font-size: 11px;
75
+ font-weight: bold;
76
+ border-right: 1px solid #00aaaa;
77
+ }
78
+
79
+ regular-layout.borland regular-layout-frame::part(active-tab) {
80
+ background: #00aaaa;
81
+ color: #000000;
82
+ }
83
+
84
+ regular-layout.borland:has(.overlay) > * {
85
+ opacity: 0.6;
86
+ }
87
+
88
+ regular-layout.borland:has(.overlay) > .overlay {
89
+ opacity: 1;
90
+ }
91
+
92
+ /* Frame in Overlay Mode */
93
+ regular-layout.borland regular-layout-frame.overlay {
94
+ background: rgba(0, 170, 170, 0.9);
95
+ border: 1px solid #ffff55;
96
+ box-shadow: none;
97
+ margin: 0;
98
+ transition:
99
+ top 0.05s linear,
100
+ height 0.05s linear,
101
+ width 0.05s linear,
102
+ left 0.05s linear;
103
+ }