regular-layout 0.3.0 → 0.5.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 (48) hide show
  1. package/README.md +110 -22
  2. package/dist/{layout → core}/types.d.ts +6 -0
  3. package/dist/extensions.d.ts +13 -1
  4. package/dist/index.d.ts +2 -1
  5. package/dist/index.js +10 -7
  6. package/dist/index.js.map +4 -4
  7. package/dist/layout/calculate_edge.d.ts +2 -2
  8. package/dist/layout/calculate_intersect.d.ts +1 -1
  9. package/dist/layout/calculate_path.d.ts +1 -1
  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 +11 -2
  13. package/dist/layout/generate_overlay.d.ts +18 -2
  14. package/dist/layout/insert_child.d.ts +1 -1
  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-frame.d.ts +21 -0
  20. package/dist/regular-layout-tab.d.ts +1 -1
  21. package/dist/regular-layout.d.ts +76 -9
  22. package/package.json +5 -4
  23. package/src/{layout → core}/constants.ts +2 -2
  24. package/src/{layout → core}/types.ts +7 -0
  25. package/src/extensions.ts +25 -1
  26. package/src/index.ts +3 -1
  27. package/src/layout/calculate_edge.ts +2 -2
  28. package/src/layout/calculate_intersect.ts +5 -2
  29. package/src/layout/calculate_path.ts +1 -1
  30. package/src/layout/calculate_presize_paths.ts +93 -0
  31. package/src/layout/flatten.ts +1 -1
  32. package/src/layout/generate_grid.ts +20 -2
  33. package/src/layout/generate_overlay.ts +48 -16
  34. package/src/layout/insert_child.ts +1 -1
  35. package/src/layout/redistribute_panel_sizes.ts +2 -2
  36. package/src/layout/remove_child.ts +2 -2
  37. package/src/model/overlay_controller.ts +161 -0
  38. package/src/model/presize_queue.ts +79 -0
  39. package/src/regular-layout-frame.ts +58 -3
  40. package/src/regular-layout-tab.ts +20 -22
  41. package/src/regular-layout.ts +252 -132
  42. package/themes/borland.css +103 -0
  43. package/themes/chicago.css +55 -49
  44. package/themes/fluxbox.css +64 -60
  45. package/themes/gibson.css +174 -164
  46. package/themes/hotdog.css +53 -47
  47. package/themes/lorax.css +82 -75
  48. /package/dist/{layout → core}/constants.d.ts +0 -0
@@ -16,8 +16,11 @@
16
16
  * @packageDocumentation
17
17
  */
18
18
 
19
- import { EMPTY_PANEL } from "./layout/types.ts";
20
- import { create_css_grid_layout } from "./layout/generate_grid.ts";
19
+ import { EMPTY_PANEL } from "./core/types.ts";
20
+ import {
21
+ create_css_grid_layout,
22
+ create_css_maximize_layout,
23
+ } from "./layout/generate_grid.ts";
21
24
  import type {
22
25
  LayoutPath,
23
26
  Layout,
@@ -26,20 +29,27 @@ import type {
26
29
  OverlayMode,
27
30
  Orientation,
28
31
  LayoutPathTraversal,
29
- } from "./layout/types.ts";
32
+ ViewWindow,
33
+ } from "./core/types.ts";
34
+
30
35
  import { calculate_intersection } from "./layout/calculate_intersect.ts";
31
36
  import { remove_child } from "./layout/remove_child.ts";
32
37
  import { insert_child } from "./layout/insert_child.ts";
33
38
  import { redistribute_panel_sizes } from "./layout/redistribute_panel_sizes.ts";
34
- import { updateOverlaySheet } from "./layout/generate_overlay.ts";
35
- import { calculate_edge } from "./layout/calculate_edge.ts";
39
+ import { viewWindowToLocalRect } from "./layout/generate_overlay.ts";
36
40
  import { flatten } from "./layout/flatten.ts";
37
41
  import { calculate_path } from "./layout/calculate_path.ts";
42
+ import { PresizeQueue } from "./model/presize_queue.ts";
43
+ import {
44
+ OverlayController,
45
+ type OverlayHost,
46
+ } from "./model/overlay_controller.ts";
47
+
38
48
  import {
39
49
  DEFAULT_PHYSICS,
40
50
  type PhysicsUpdate,
41
51
  type Physics,
42
- } from "./layout/constants.ts";
52
+ } from "./core/constants.ts";
43
53
 
44
54
  /**
45
55
  * An interface which models the fields of `PointerEvent` that
@@ -99,6 +109,14 @@ export interface PointerEventCoordinates {
99
109
  * latter implementation would require synchronizing the light DOM
100
110
  * and shadow DOM slots/slotted children continuously.
101
111
  *
112
+ * Note that `::slotted()` only matches *directly* slotted nodes - it
113
+ * does not pierce a slotted `<slot>`. A consumer that wants to forward
114
+ * its own light-DOM content through `<regular-layout>` therefore cannot
115
+ * place a bare `<slot name="X">` as a layout child and expect it to be
116
+ * grid-positioned; instead, wrap the forwarding slot in a concrete
117
+ * `<regular-layout-frame name="X">` (which *is* directly slotted and so
118
+ * matched by the generated `::slotted([name="X"])` rules).
119
+ *
102
120
  */
103
121
  export class RegularLayout extends HTMLElement {
104
122
  private _shadowRoot: ShadowRoot;
@@ -109,6 +127,9 @@ export class RegularLayout extends HTMLElement {
109
127
  private _cursor_override: boolean;
110
128
  private _dimensions?: { box: DOMRect; style: CSSStyleDeclaration };
111
129
  private _physics: Physics;
130
+ private _presizeQueue: PresizeQueue;
131
+ private _overlayController: OverlayController;
132
+ private _maximized?: string;
112
133
 
113
134
  constructor() {
114
135
  super();
@@ -119,6 +140,9 @@ export class RegularLayout extends HTMLElement {
119
140
  this._stylesheet = new CSSStyleSheet();
120
141
  this._cursor_stylesheet = new CSSStyleSheet();
121
142
  this._cursor_override = false;
143
+ const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-before-resize`;
144
+ this._presizeQueue = new PresizeQueue(this, event_name);
145
+ this._overlayController = new OverlayController(this.create_overlay_host());
122
146
  this._shadowRoot.adoptedStyleSheets = [
123
147
  this._stylesheet,
124
148
  this._cursor_stylesheet,
@@ -177,54 +201,13 @@ export class RegularLayout extends HTMLElement {
177
201
  * the target, "absolute" positions the panel absolutely. Defaults to
178
202
  * "absolute".
179
203
  */
180
- setOverlayState = (
204
+ setOverlayState = async (
181
205
  event: PointerEventCoordinates,
182
- { slot }: LayoutPath,
183
- className: string = this._physics.OVERLAY_CLASSNAME,
184
- mode: OverlayMode = this._physics.OVERLAY_DEFAULT,
206
+ target: LayoutPath,
207
+ className?: string,
208
+ mode?: OverlayMode,
185
209
  ) => {
186
- const panel = remove_child(this._panel, slot);
187
- const query = `:scope > [${this._physics.CHILD_ATTRIBUTE_NAME}="${slot}"]`;
188
- const drag_element = this.querySelector(query);
189
- if (drag_element) {
190
- drag_element.classList.add(className);
191
- }
192
-
193
- // TODO: Don't recalculate box (but this currently protects against resize).
194
- const [col, row, box, style] = this.relativeCoordinates(event, true);
195
- let drop_target = calculate_intersection(col, row, panel);
196
- if (drop_target) {
197
- drop_target = calculate_edge(
198
- col,
199
- row,
200
- panel,
201
- slot,
202
- drop_target,
203
- box,
204
- this._physics,
205
- );
206
- }
207
-
208
- if (mode === "grid" && drop_target) {
209
- const path: [string, string] = [slot, drop_target?.slot];
210
- const css = create_css_grid_layout(panel, path, this._physics);
211
- this._stylesheet.replaceSync(css);
212
- } else if (mode === "absolute") {
213
- const grid_css = create_css_grid_layout(panel, undefined, this._physics);
214
- const overlay_css = updateOverlaySheet(
215
- slot,
216
- box,
217
- style,
218
- drop_target,
219
- this._physics,
220
- );
221
-
222
- this._stylesheet.replaceSync([grid_css, overlay_css].join("\n"));
223
- }
224
-
225
- const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-before-update`;
226
- const custom_event = new CustomEvent<Layout>(event_name, { detail: panel });
227
- this.dispatchEvent(custom_event);
210
+ await this._overlayController.set(event, target, className, mode);
228
211
  };
229
212
 
230
213
  /**
@@ -239,54 +222,12 @@ export class RegularLayout extends HTMLElement {
239
222
  * @param mode - Overlay rendering mode that was used, must match the mode
240
223
  * passed to `setOverlayState`. Defaults to "absolute".
241
224
  */
242
- clearOverlayState = (
225
+ clearOverlayState = async (
243
226
  event: PointerEventCoordinates | null,
244
- { slot, layout }: LayoutPath,
245
- className: string = this._physics.OVERLAY_CLASSNAME,
227
+ target: LayoutPath,
228
+ className?: string,
246
229
  ) => {
247
- let panel = this._panel;
248
- panel = remove_child(panel, slot);
249
- const query = `:scope > [${this._physics.CHILD_ATTRIBUTE_NAME}="${slot}"]`;
250
- const drag_element = this.querySelector(query);
251
- if (drag_element) {
252
- drag_element.classList.remove(className);
253
- }
254
-
255
- if (event === null) {
256
- this.restore(layout);
257
- return;
258
- }
259
-
260
- const [col, row, box] = this.relativeCoordinates(event, false);
261
- let drop_target = calculate_intersection(col, row, panel);
262
- if (drop_target) {
263
- drop_target = calculate_edge(
264
- col,
265
- row,
266
- panel,
267
- slot,
268
- drop_target,
269
- box,
270
- this._physics,
271
- );
272
- }
273
-
274
- if (drop_target) {
275
- const orientation = drop_target?.is_edge
276
- ? drop_target.orientation
277
- : undefined;
278
-
279
- const new_layout = insert_child(
280
- panel,
281
- slot,
282
- drop_target.path,
283
- orientation,
284
- );
285
-
286
- this.restore(new_layout);
287
- } else {
288
- this.restore(layout);
289
- }
230
+ await this._overlayController.clear(event, target, className);
290
231
  };
291
232
 
292
233
  /**
@@ -300,7 +241,7 @@ export class RegularLayout extends HTMLElement {
300
241
  * the new `SplitPanel` _if_ there is an option of orientation (e.g. if
301
242
  * the layout had no pre-existing `SplitPanel`)
302
243
  */
303
- insertPanel = (
244
+ insertPanel = async (
304
245
  name: string,
305
246
  path: LayoutPathTraversal = [],
306
247
  split?: boolean | Orientation,
@@ -312,7 +253,7 @@ export class RegularLayout extends HTMLElement {
312
253
  orientation = split;
313
254
  }
314
255
 
315
- this.restore(insert_child(this._panel, name, path, orientation));
256
+ await this.restore(insert_child(this._panel, name, path, orientation));
316
257
  };
317
258
 
318
259
  /**
@@ -320,8 +261,38 @@ export class RegularLayout extends HTMLElement {
320
261
  *
321
262
  * @param name - Name of the panel to remove
322
263
  */
323
- removePanel = (name: string) => {
324
- this.restore(remove_child(this._panel, name));
264
+ removePanel = async (name: string) => {
265
+ await this.restore(remove_child(this._panel, name));
266
+ };
267
+
268
+ /**
269
+ * Selects a panel by `name`, making it the front-most tab within its
270
+ * containing stack, then dispatches a `<prefix>-select` event with
271
+ * `detail: { name }`.
272
+ *
273
+ * The event fires whenever a tab is selected - including re-selecting the
274
+ * already-active tab, or selecting the sole tab of a single-element stack -
275
+ * so consumers can always map a tab interaction to an "active panel". When
276
+ * the selection actually changes, a `<prefix>-update` event is dispatched
277
+ * first (via {@link restore}).
278
+ *
279
+ * @param name - The name of the panel to select.
280
+ */
281
+ select = async (name: string) => {
282
+ const layout = this.save();
283
+ const tab_panel = this.getPanel(name, layout);
284
+ if (!tab_panel) {
285
+ return;
286
+ }
287
+
288
+ const index = tab_panel.tabs.indexOf(name);
289
+ if (index !== -1 && tab_panel.selected !== index) {
290
+ tab_panel.selected = index;
291
+ await this.restore(layout);
292
+ }
293
+
294
+ const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-select`;
295
+ this.dispatchEvent(new CustomEvent(event_name, { detail: { name } }));
325
296
  };
326
297
 
327
298
  /**
@@ -353,29 +324,100 @@ export class RegularLayout extends HTMLElement {
353
324
  /**
354
325
  * Clears the entire layout, unslotting all panels.
355
326
  */
356
- clear = () => {
357
- this.restore(EMPTY_PANEL);
327
+ clear = async () => {
328
+ await this.restore(EMPTY_PANEL);
329
+ };
330
+
331
+ /**
332
+ * Maximizes a panel by `name`, rendering it full-size and hiding every
333
+ * other panel. This is transient display state - it is **not** persisted by
334
+ * {@link save}, and any subsequent {@link restore} resets the layout to the
335
+ * minimized (normal, multi-panel) view.
336
+ *
337
+ * Has no effect if `name` is not present in the current layout.
338
+ *
339
+ * @param name - The name of the panel to maximize.
340
+ */
341
+ maximize = (name: string) => {
342
+ if (!this.getPanel(name)) {
343
+ return;
344
+ }
345
+
346
+ this._maximized = name;
347
+ this._stylesheet.replaceSync(
348
+ create_css_maximize_layout(name, this._physics),
349
+ );
350
+ };
351
+
352
+ /**
353
+ * Restores the normal multi-panel view after a {@link maximize}, without
354
+ * altering the layout tree. No-op if no panel is currently maximized.
355
+ */
356
+ minimize = () => {
357
+ if (this._maximized === undefined) {
358
+ return;
359
+ }
360
+
361
+ this._maximized = undefined;
362
+ this._stylesheet.replaceSync(
363
+ create_css_grid_layout(this._panel, undefined, this._physics),
364
+ );
365
+ };
366
+
367
+ /**
368
+ * Restores the layout from a saved state synchronously, without
369
+ * dispatching the `regular-layout-before-resize` event.
370
+ *
371
+ * @param layout - The layout tree to restore
372
+ */
373
+ restoreSync = (layout: Layout, _is_flattened: boolean = false) => {
374
+ this._maximized = undefined;
375
+ this._panel = !_is_flattened ? flatten(layout) : layout;
376
+ const css = create_css_grid_layout(this._panel, undefined, this._physics);
377
+ this._stylesheet.replaceSync(css);
378
+ const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-update`;
379
+ const event = new CustomEvent<Layout>(event_name, { detail: this._panel });
380
+ this.dispatchEvent(event);
358
381
  };
359
382
 
360
383
  /**
361
384
  * Restores the layout from a saved state.
362
385
  *
386
+ * Before applying, dispatches a cancelable `regular-layout-before-resize`
387
+ * event. If the event is cancelled via `preventDefault()`, the layout
388
+ * update is suspended until {@link resumeResize} is called.
389
+ *
363
390
  * @param layout - The layout tree to restore
364
391
  *
365
392
  * @example
366
393
  * ```typescript
367
394
  * const layout = document.querySelector('regular-layout');
368
395
  * const savedState = JSON.parse(localStorage.getItem('layout'));
369
- * layout.restore(savedState);
396
+ * await layout.restore(savedState);
370
397
  * ```
371
398
  */
372
- restore = (layout: Layout, _is_flattened: boolean = false) => {
373
- this._panel = !_is_flattened ? flatten(layout) : layout;
374
- const css = create_css_grid_layout(this._panel, undefined, this._physics);
375
- this._stylesheet.replaceSync(css);
376
- const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-update`;
377
- const event = new CustomEvent<Layout>(event_name, { detail: this._panel });
378
- this.dispatchEvent(event);
399
+ restore = async (layout: Layout, _is_flattened: boolean = false) => {
400
+ const panel = !_is_flattened ? flatten(layout) : layout;
401
+ await this._presizeQueue.run(panel, () => {
402
+ this._maximized = undefined;
403
+ this._panel = panel;
404
+ const css = create_css_grid_layout(this._panel, undefined, this._physics);
405
+ this._stylesheet.replaceSync(css);
406
+ const event_name = `${this._physics.CUSTOM_EVENT_NAME_PREFIX}-update`;
407
+ const event = new CustomEvent<Layout>(event_name, {
408
+ detail: this._panel,
409
+ });
410
+
411
+ this.dispatchEvent(event);
412
+ });
413
+ };
414
+
415
+ /**
416
+ * Resumes a layout update that was suspended by cancelling the
417
+ * `regular-layout-before-resize` event.
418
+ */
419
+ resumeResize = () => {
420
+ this._presizeQueue.resume();
379
421
  };
380
422
 
381
423
  /**
@@ -441,21 +483,53 @@ export class RegularLayout extends HTMLElement {
441
483
 
442
484
  const box = this._dimensions.box;
443
485
  const style = this._dimensions.style;
444
- const col =
445
- (event.clientX - box.left - parseFloat(style.paddingLeft)) /
446
- (box.width -
447
- parseFloat(style.paddingLeft) -
448
- parseFloat(style.paddingRight));
449
-
450
- const row =
451
- (event.clientY - box.top - parseFloat(style.paddingTop)) /
452
- (box.height -
453
- parseFloat(style.paddingTop) -
454
- parseFloat(style.paddingBottom));
455
-
486
+ const paddingLeft = parseFloat(style.paddingLeft);
487
+ const paddingTop = parseFloat(style.paddingTop);
488
+ const contentWidth =
489
+ box.width - paddingLeft - parseFloat(style.paddingRight);
490
+
491
+ const contentHeight =
492
+ box.height - paddingTop - parseFloat(style.paddingBottom);
493
+
494
+ const localX = event.clientX - box.left - paddingLeft;
495
+ const localY = event.clientY - box.top - paddingTop;
496
+ const col = Math.max(0, Math.min(1, localX / contentWidth));
497
+ const row = Math.max(0, Math.min(1, localY / contentHeight));
456
498
  return [col, row, box, style];
457
499
  };
458
500
 
501
+ /**
502
+ * Converts a {@link ViewWindow} (normalized 0–1 coordinates) to a
503
+ * `DOMRect` in screen pixels, accounting for padding and optionally
504
+ * CSS `gap` and child `margin`.
505
+ *
506
+ * @param window - The view window to convert.
507
+ * @returns A `DOMRect` representing the window in screen coordinates.
508
+ */
509
+ realCoordinates = (window: ViewWindow, child?: HTMLElement): DOMRect => {
510
+ if (!this._dimensions) {
511
+ this._dimensions = {
512
+ box: this.getBoundingClientRect(),
513
+ style: getComputedStyle(this),
514
+ };
515
+ }
516
+
517
+ const box = this._dimensions.box;
518
+ const style = this._dimensions.style;
519
+ let childStyle: CSSStyleDeclaration | undefined;
520
+ if (child) {
521
+ childStyle = getComputedStyle(child);
522
+ }
523
+
524
+ const local = viewWindowToLocalRect(window, box, style, childStyle);
525
+ return new DOMRect(
526
+ box.left + local.x,
527
+ box.top + local.y,
528
+ local.width,
529
+ local.height,
530
+ );
531
+ };
532
+
459
533
  /**
460
534
  * Calculates the Euclidean distance in pixels between the current pointer
461
535
  * coordinates and a drag target's position within the layout.
@@ -476,7 +550,47 @@ export class RegularLayout extends HTMLElement {
476
550
  return Math.sqrt(dx ** 2 + dy ** 2);
477
551
  };
478
552
 
479
- private onDblClick = (event: MouseEvent) => {
553
+ // ▇█████████████████■■■■ȺȺȺȺȺ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ȺȺȺȺȺ■■■■■█████████████████
554
+ // ███████▛▀▀█▃▄▄▅▆▆▆▇▇▇██████████████████████████████▇▇▇▆▆▆▅▅▄▃█▀▀▜███████
555
+ // ████▛▚▅▇███▍ ▗▄▃¯"▜██▘ ▜██▍ ▀█▋ ▐█▀▔▂▄▃ ▔▜█ ▗▃▃▃▄▊ ▄▄▃ ▔▜██▇▅▃▀████
556
+ // ███▋╺██████▍ ▐██▋ █▘ ◨Ƚ "██▍ ▙▁ ▀ ▐▋ █▛▀▀▀▜█ ´▂▂Ŋ█▊ °"▔ ▃██████▍▐███
557
+ // ████▄▝▜████▍ ▝▀▀ ▂▟▘ ▄▄▄▄ "█▍ ██▄ ▐█▃ ▝▀▀ ▐█ ▝▀▀▀▀▊ ██▖ ▝████▛▀▄████
558
+ // ██████▇▆▄▃█▀▀Ⱥ▼■███▇▇████▇▇█▇▇████▇████▇▇▇████▇▇▇▇▇▇▇█▇■◘▀▀▀▀▂▃▄▅▇██████
559
+ // ███████████████▇▇▆▆▆▅▅▅▅▅▅▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▅▅▅▅▅▅▆▆▆▇▇███████████████
560
+ //
561
+ //
562
+ // ┏▅▅▖ ▗▅▅▶ ▅▅▅▅▅▅▅ ▗▅▅▅▅▅▅▖ ▅▅▅▅▅▄▄▂ ▃▅▆▆▆▄▁ ┏▅▅▖ ▅▅▖▅▅▅▅▅▅▅▅▅
563
+ // ┣██▌▗██▛ ██▊▔▔▔▔ ▐██▋▔▔▔` ███▍"▜██▙ ▟██▘▔███▖ ▐██▌ ██▌^▔▔███^▔^
564
+ // ┣██▙██▋ ██▊▂▂▂ ▐██▋▂▂▂ ███▍ ▐██▋ ▐███ ▐██▋ ▐██▌ ██▌ ███▎
565
+ // ┣██████▌ ███▀▀▀ ▐███▀▀▘ ██████▛▀ ▐███ ▐██▋ ▐██▌ ██▌ ███▎
566
+ // ┣██▛ ▜██▖ ██▊ ▐██▋ ███▍ ▝███ ▟██▌ ▐██▌ ██▌ ███▎
567
+ // ┣██▌ ███ ███▆▆▆▆▎▐███▆▆▆▅ ███▍ ▀██▙▅██▛ ▝███▅▆██▘ ███▎
568
+ // ▔▔▔` ´▔▔` ▔▔▔▔▔▔▔ ´▔▔▔▔▔▔▔ ▔▔▔ ▔""▔¯ ▔""^▔ ▔▔▔
569
+
570
+ private create_overlay_host(): OverlayHost {
571
+ const self = this;
572
+ return {
573
+ get panel() {
574
+ return self._panel;
575
+ },
576
+ get physics() {
577
+ return self._physics;
578
+ },
579
+ get stylesheet() {
580
+ return self._stylesheet;
581
+ },
582
+ get presizeQueue() {
583
+ return self._presizeQueue;
584
+ },
585
+ relativeCoordinates: (event, recalculate) =>
586
+ self.relativeCoordinates(event, recalculate),
587
+ restore: (layout, isFlattened) => self.restore(layout, isFlattened),
588
+ querySelector: (selectors) => self.querySelector(selectors),
589
+ dispatchEvent: (event) => self.dispatchEvent(event),
590
+ };
591
+ }
592
+
593
+ private onDblClick = async (event: MouseEvent) => {
480
594
  const [col, row, rect] = this.relativeCoordinates(event, false);
481
595
  const divider = calculate_intersection(col, row, this._panel, {
482
596
  rect,
@@ -490,11 +604,15 @@ export class RegularLayout extends HTMLElement {
490
604
  undefined,
491
605
  );
492
606
 
493
- this.restore(panel, true);
607
+ await this.restore(panel, true);
494
608
  }
495
609
  };
496
610
 
497
611
  private onPointerDown = (event: PointerEvent) => {
612
+ if (event.button !== 0) {
613
+ return;
614
+ }
615
+
498
616
  if (!this._physics.GRID_DIVIDER_CHECK_TARGET || event.target === this) {
499
617
  const [col, row, rect] = this.relativeCoordinates(event);
500
618
  const size = this._physics.GRID_DIVIDER_SIZE;
@@ -507,15 +625,17 @@ export class RegularLayout extends HTMLElement {
507
625
  }
508
626
  };
509
627
 
510
- private onPointerMove = (event: PointerEvent) => {
628
+ private onPointerMove = async (event: PointerEvent) => {
511
629
  if (this._drag_target) {
512
630
  const [col, row] = this.relativeCoordinates(event, false);
513
631
  const [{ path, type }, old_col, old_row] = this._drag_target;
514
632
  const offset = type === "horizontal" ? old_col - col : old_row - row;
515
633
  const panel = redistribute_panel_sizes(this._panel, path, offset);
516
- this._stylesheet.replaceSync(
517
- create_css_grid_layout(panel, undefined, this._physics),
518
- );
634
+ await this._presizeQueue.run(panel, () => {
635
+ this._stylesheet.replaceSync(
636
+ create_css_grid_layout(panel, undefined, this._physics),
637
+ );
638
+ });
519
639
  }
520
640
 
521
641
  if (this._physics.GRID_DIVIDER_CHECK_TARGET && event.target !== this) {
@@ -545,14 +665,14 @@ export class RegularLayout extends HTMLElement {
545
665
  }
546
666
  };
547
667
 
548
- private onPointerUp = (event: PointerEvent) => {
668
+ private onPointerUp = async (event: PointerEvent) => {
549
669
  if (this._drag_target) {
550
670
  this.releasePointerCapture(event.pointerId);
551
671
  const [col, row] = this.relativeCoordinates(event, false);
552
672
  const [{ path, type }, old_col, old_row] = this._drag_target;
553
673
  const offset = type === "horizontal" ? old_col - col : old_row - row;
554
674
  const panel = redistribute_panel_sizes(this._panel, path, offset);
555
- this.restore(panel, true);
675
+ await this.restore(panel, true);
556
676
  this._drag_target = undefined;
557
677
  }
558
678
  };
@@ -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
+ }