regular-layout 0.0.2 → 0.1.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.
@@ -31,12 +31,15 @@ export declare class RegularLayoutFrame extends HTMLElement {
31
31
  private _drag_state;
32
32
  private _drag_moved;
33
33
  private _tab_to_index_map;
34
+ private _tab_panel_state;
34
35
  constructor();
35
36
  connectedCallback(): void;
36
37
  disconnectedCallback(): void;
37
- private drawTabs;
38
38
  private onPointerDown;
39
39
  private onPointerMove;
40
40
  private onPointerUp;
41
41
  private onPointerLost;
42
+ private drawTabs;
43
+ private createTab;
44
+ private onTabClick;
42
45
  }
@@ -1,4 +1,4 @@
1
- import type { LayoutPath, Layout, TabLayout } from "./common/layout_config.ts";
1
+ import type { LayoutPath, Layout, TabLayout, OverlayMode } from "./common/layout_config.ts";
2
2
  /**
3
3
  * A Web Component that provides a resizable panel layout system.
4
4
  * Panels are arranged using CSS Grid and can be resized by dragging dividers.
@@ -48,46 +48,61 @@ export declare class RegularLayout extends HTMLElement {
48
48
  *
49
49
  * @param x - X coordinate in screen pixels.
50
50
  * @param y - Y coordinate in screen pixels.
51
- * @param layoutPath - Layout path containing the slot identifier.
52
- * @param mode - Overlay rendering mode: "grid" highlights the target,
53
- * "absolute" positions the panel absolutely, "interactive" updates the
54
- * actual layout in real-time. Defaults to "absolute".
51
+ * @param dragTarget - A `LayoutPath` (presumably from `calculateIntersect`)
52
+ * which points to the drag element in the current layout.
53
+ * @param className - The CSS class name to use for the overlay panel
54
+ * (defaults to "overlay").
55
+ * @param mode - Overlay rendering mode: "grid" uses CSS grid to position
56
+ * the target, "absolute" positions the panel absolutely. Defaults to
57
+ * "absolute".
55
58
  */
56
- setOverlayState<T>(x: number, y: number, { slot }: LayoutPath<T>, mode?: "grid" | "absolute" | "interactive"): void;
59
+ setOverlayState: (x: number, y: number, { slot }: LayoutPath<unknown>, className?: string, mode?: OverlayMode) => void;
57
60
  /**
58
61
  * Clears the overlay state and commits the panel placement.
59
62
  *
60
63
  * @param x - X coordinate in screen pixels.
61
64
  * @param y - Y coordinate in screen pixels.
62
- * @param layout_path - Layout path containing the slot identifier.
65
+ * @param dragTarget - A `LayoutPath` (presumably from `calculateIntersect`)
66
+ * which points to the drag element in the current layout.
67
+ * @param className - The CSS class name to use for the overlay panel
68
+ * (defaults to "overlay").
63
69
  * @param mode - Overlay rendering mode that was used, must match the mode
64
- * passed to `setOverlayState`. Defaults to "absolute".
70
+ * passed to `setOverlayState`. Defaults to "absolute".
65
71
  */
66
- clearOverlayState<T>(x: number, y: number, drag_target: LayoutPath<T>, mode?: "grid" | "absolute" | "interactive"): void;
72
+ clearOverlayState: (x: number, y: number, drag_target: LayoutPath<unknown>, className?: string, mode?: OverlayMode) => void;
67
73
  /**
68
74
  * Inserts a new panel into the layout at a specified path.
69
75
  *
70
76
  * @param name - Unique identifier for the new panel.
71
77
  * @param path - Index path defining where to insert.
72
78
  */
73
- insertPanel(name: string, path?: number[]): void;
79
+ insertPanel: (name: string, path?: number[]) => void;
74
80
  /**
75
81
  * Removes a panel from the layout by name.
76
82
  *
77
83
  * @param name - Name of the panel to remove
78
84
  */
79
- removePanel(name: string): void;
80
- getPanel(name: string, layout?: Layout): TabLayout | null;
85
+ removePanel: (name: string) => void;
86
+ /**
87
+ * Retrieves a panel by name from the layout tree.
88
+ *
89
+ * @param name - Name of the panel to find.
90
+ * @param layout - Optional layout tree to search in (defaults to current layout).
91
+ * @returns The TabLayout containing the panel if found, null otherwise.
92
+ */
93
+ getPanel: (name: string, layout?: Layout) => TabLayout | null;
81
94
  /**
82
95
  * Determines which panel is at a given screen coordinate.
83
- * Useful for drag-and-drop operations or custom interactions.
84
96
  *
85
97
  * @param column - X coordinate in screen pixels.
86
98
  * @param row - Y coordinate in screen pixels.
87
99
  * @returns Panel information if a panel is at that position, null otherwise.
88
100
  */
89
- calculateIntersect(x: number, y: number, check_dividers?: boolean): LayoutPath<DOMRect> | null;
90
- clear(): void;
101
+ calculateIntersect: (x: number, y: number, check_dividers?: boolean) => LayoutPath<DOMRect> | null;
102
+ /**
103
+ * Clears the entire layout, unslotting all panels.
104
+ */
105
+ clear: () => void;
91
106
  /**
92
107
  * Restores the layout from a saved state.
93
108
  *
@@ -100,7 +115,7 @@ export declare class RegularLayout extends HTMLElement {
100
115
  * layout.restore(savedState);
101
116
  * ```
102
117
  */
103
- restore(layout: Layout, _is_flattened?: boolean): void;
118
+ restore: (layout: Layout, _is_flattened?: boolean) => void;
104
119
  /**
105
120
  * Serializes the current layout state, which can be restored via `restore`.
106
121
  *
@@ -113,8 +128,22 @@ export declare class RegularLayout extends HTMLElement {
113
128
  * localStorage.setItem('layout', JSON.stringify(state));
114
129
  * ```
115
130
  */
116
- save(): Layout;
117
- private relativeCoordinates;
131
+ save: () => Layout;
132
+ /**
133
+ * Converts screen coordinates to relative layout coordinates.
134
+ *
135
+ * Transforms absolute pixel positions into normalized coordinates (0-1 range)
136
+ * relative to the layout's bounding box.
137
+ *
138
+ * @param clientX - X coordinate in screen pixels (client space).
139
+ * @param clientY - Y coordinate in screen pixels (client space).
140
+ * @returns A tuple containing:
141
+ * - col: Normalized X coordinate (0 = left edge, 1 = right edge)
142
+ * - row: Normalized Y coordinate (0 = top edge, 1 = bottom edge)
143
+ * - box: The layout element's bounding rectangle
144
+ */
145
+ relativeCoordinates: (clientX: number, clientY: number) => [number, number, DOMRect];
146
+ private updateSlots;
118
147
  private onPointerDown;
119
148
  private onPointerMove;
120
149
  private onPointerUp;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "regular-layout",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "A regular CSS `grid` container",
5
5
  "keywords": [],
6
6
  "license": "Apache-2.0",
@@ -20,6 +20,7 @@
20
20
  "clean": "rm -rf dist",
21
21
  "test": "playwright test",
22
22
  "example": "npx http-server . -p 8000",
23
+ "deploy": "node deploy.mjs",
23
24
  "lint": "biome lint src tests",
24
25
  "format": "biome format --write src tests",
25
26
  "check": "biome check --write src tests"
@@ -0,0 +1,104 @@
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 { calculate_intersection } from "./calculate_intersect";
13
+ import { SPLIT_EDGE_TOLERANCE } from "./constants";
14
+ import { insert_child } from "./insert_child";
15
+ import type { Layout, LayoutPath, Orientation } from "./layout_config";
16
+
17
+ /**
18
+ * Calculates an insertion point (which may involve splitting a single
19
+ * `"child-panel"` into a new `"split-panel"`), based on the cursor position.
20
+ * *
21
+ * @param col - The cursor column.
22
+ * @param row - The cursor row.
23
+ * @param panel - The `Layout` to insert into.
24
+ * @param slot - The slot identifier where the insert should occur
25
+ * @param drop_target - The `LayoutPath` (from `calculateIntersect`) of the
26
+ * panel to either insert next to, or split by.
27
+ * @returns A new `LayoutPath` reflecting the updated (maybe) `"split-panel"`,
28
+ * which is enough to draw the overlay.
29
+ */
30
+ export function calculate_edge(
31
+ col: number,
32
+ row: number,
33
+ panel: Layout,
34
+ slot: string,
35
+ drop_target: LayoutPath,
36
+ ): LayoutPath {
37
+ const is_column_edge =
38
+ drop_target.column_offset < SPLIT_EDGE_TOLERANCE ||
39
+ drop_target.column_offset > 1 - SPLIT_EDGE_TOLERANCE;
40
+
41
+ const is_row_edge =
42
+ drop_target.row_offset < SPLIT_EDGE_TOLERANCE ||
43
+ drop_target.row_offset > 1 - SPLIT_EDGE_TOLERANCE;
44
+
45
+ if (is_column_edge) {
46
+ return handle_axis(
47
+ col,
48
+ row,
49
+ panel,
50
+ slot,
51
+ drop_target,
52
+ drop_target.column_offset,
53
+ "horizontal",
54
+ );
55
+ } else if (is_row_edge) {
56
+ return handle_axis(
57
+ col,
58
+ row,
59
+ panel,
60
+ slot,
61
+ drop_target,
62
+ drop_target.row_offset,
63
+ "vertical",
64
+ );
65
+ }
66
+
67
+ return drop_target;
68
+ }
69
+
70
+ function handle_axis(
71
+ col: number,
72
+ row: number,
73
+ panel: Layout,
74
+ slot: string,
75
+ drop_target: LayoutPath,
76
+ axis_offset: number,
77
+ axis_orientation: Orientation,
78
+ ): LayoutPath {
79
+ const is_before = axis_offset < SPLIT_EDGE_TOLERANCE;
80
+ if (drop_target.orientation === axis_orientation) {
81
+ if (drop_target.path.length === 0) {
82
+ const insert_index = is_before ? 0 : 1;
83
+ const new_panel = insert_child(panel, slot, [insert_index]);
84
+ drop_target = calculate_intersection(col, row, new_panel, false);
85
+ } else {
86
+ const path_without_last = drop_target.path.slice(0, -1);
87
+ const last_index = drop_target.path[drop_target.path.length - 1];
88
+ const insert_index = is_before ? last_index : last_index + 1;
89
+ const new_panel = insert_child(panel, slot, [
90
+ ...path_without_last,
91
+ insert_index,
92
+ ]);
93
+
94
+ drop_target = calculate_intersection(col, row, new_panel, false);
95
+ }
96
+ } else {
97
+ const path = [...drop_target.path, is_before ? 0 : 1];
98
+ const new_panel = insert_child(panel, slot, path, axis_orientation);
99
+ drop_target = calculate_intersection(col, row, new_panel, false);
100
+ }
101
+
102
+ drop_target.is_edge = true;
103
+ return drop_target;
104
+ }
@@ -86,6 +86,7 @@ function calculate_intersection_recursive(
86
86
  const column_offset =
87
87
  (column - view_window.col_start) /
88
88
  (view_window.col_end - view_window.col_start);
89
+
89
90
  const row_offset =
90
91
  (row - view_window.row_start) /
91
92
  (view_window.row_end - view_window.row_start);
@@ -98,6 +99,8 @@ function calculate_intersection_recursive(
98
99
  path: path,
99
100
  view_window: view_window,
100
101
  is_edge: false,
102
+ column,
103
+ row,
101
104
  column_offset,
102
105
  row_offset,
103
106
  orientation: parent_orientation || "horizontal",
@@ -0,0 +1,46 @@
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 { OverlayMode } from "./layout_config";
13
+
14
+ /**
15
+ * The minimum number of pixels the mouse must move to be considered a drag.
16
+ */
17
+ export const MIN_DRAG_DISTANCE = 10;
18
+
19
+ /**
20
+ * Class name to use for child elements in overlay position (dragging).
21
+ */
22
+ export const OVERLAY_CLASSNAME = "overlay";
23
+
24
+ /**
25
+ * The percentage of the maximum resize distance that will be clamped.
26
+ *
27
+ */
28
+ export const MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD = 0.15;
29
+
30
+ /**
31
+ * Threshold from panel edge that is considered a split vs drop action.
32
+ */
33
+ export const SPLIT_EDGE_TOLERANCE = 0.25;
34
+
35
+ /**
36
+ * Tolerance threshold for considering two grid track positions as identical.
37
+ *
38
+ * When collecting and deduplicating track positions, any positions closer than
39
+ * this value are treated as the same position to avoid redundant grid tracks.
40
+ */
41
+ export const GRID_TRACK_COLLAPSE_TOLERANCE = 0.001;
42
+
43
+ /**
44
+ * The overlay default behavior.
45
+ */
46
+ export const OVERLAY_DEFAULT: OverlayMode = "absolute";
@@ -24,6 +24,7 @@ import type { Layout } from "./layout_config.ts";
24
24
  */
25
25
  export function flatten(layout: Layout): Layout {
26
26
  if (layout.type === "child-panel") {
27
+ layout.selected = layout.selected || 0;
27
28
  return layout;
28
29
  }
29
30
 
@@ -9,7 +9,8 @@
9
9
  // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
10
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
11
 
12
- import { GRID_TRACK_COLLAPSE_TOLERANCE, type Layout } from "./layout_config.ts";
12
+ import { GRID_TRACK_COLLAPSE_TOLERANCE } from "./constants.ts";
13
+ import type { Layout } from "./layout_config.ts";
13
14
  import { remove_child } from "./remove_child.ts";
14
15
 
15
16
  interface GridCell {
@@ -11,15 +11,18 @@
11
11
 
12
12
  import type { LayoutPath } from "./layout_config";
13
13
 
14
- export function updateOverlaySheet({
15
- view_window: { row_start, row_end, col_start, col_end },
16
- box,
17
- }: LayoutPath<DOMRect>) {
14
+ export function updateOverlaySheet(
15
+ slot: string,
16
+ {
17
+ view_window: { row_start, row_end, col_start, col_end },
18
+ box,
19
+ }: LayoutPath<DOMRect>,
20
+ ) {
18
21
  const margin = 0;
19
22
  const top = row_start * box.height + margin / 2;
20
23
  const left = col_start * box.width + margin / 2;
21
24
  const height = (row_end - row_start) * box.height - margin;
22
25
  const width = (col_end - col_start) * box.width - margin;
23
26
  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}}`;
27
+ return `::slotted([slot="${slot}"]){${css}}`;
25
28
  }
@@ -77,6 +77,7 @@ export function insert_child(
77
77
  if (restPath.length === 0 || index === panel.children.length) {
78
78
  if (is_edge && panel.children[index]?.type === "child-panel") {
79
79
  panel.children[index].child.unshift(child);
80
+ panel.children[index].selected = 0;
80
81
  return panel;
81
82
  }
82
83
 
@@ -9,34 +9,10 @@
9
9
  // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
10
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
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.25;
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.001;
30
-
31
- /**
32
- * The overlay default behavior.
33
- */
34
- export const OVERLAY_DEFAULT: OverlayMode = "absolute";
35
-
36
12
  /**
37
13
  * The overlay behavior type.
38
14
  */
39
- export type OverlayMode = "grid" | "absolute" | "interactive";
15
+ export type OverlayMode = "grid" | "absolute";
40
16
 
41
17
  /**
42
18
  * The representation of a CSS grid, in JSON form.
@@ -105,6 +81,8 @@ export interface LayoutPath<T = undefined> {
105
81
  panel: TabLayout;
106
82
  path: number[];
107
83
  view_window: ViewWindow;
84
+ column: number;
85
+ row: number;
108
86
  column_offset: number;
109
87
  row_offset: number;
110
88
  orientation: Orientation;
@@ -125,7 +103,7 @@ export function* iter_panel_children(panel: Layout): Generator<string> {
125
103
  yield* iter_panel_children(child);
126
104
  }
127
105
  } else {
128
- yield* panel.child;
106
+ yield panel.child[panel.selected || 0];
129
107
  }
130
108
  }
131
109
 
@@ -9,10 +9,8 @@
9
9
  // ┃ * [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). * ┃
10
10
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
11
11
 
12
- import {
13
- MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD,
14
- type Layout,
15
- } from "./layout_config.ts";
12
+ import { MINIMUM_REDISTRIBUTION_SIZE_THRESHOLD } from "./constants.ts";
13
+ import type { Layout } from "./layout_config.ts";
16
14
 
17
15
  /**
18
16
  * Adjusts panel sizes during a drag operation on a divider.
@@ -36,6 +36,7 @@ export function remove_child(panel: Layout, child: string): Layout {
36
36
  child: newChild,
37
37
  };
38
38
  }
39
+
39
40
  return structuredClone(panel);
40
41
  }
41
42
 
package/src/extensions.ts CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  import { RegularLayout } from "./regular-layout.ts";
13
13
  import { RegularLayoutFrame } from "./regular-layout-frame.ts";
14
- import { Layout } from "./common/layout_config.ts";
14
+ import type { Layout } from "./common/layout_config.ts";
15
15
 
16
16
  customElements.define("regular-layout", RegularLayout);
17
17
  customElements.define("regular-layout-frame", RegularLayoutFrame);
@@ -45,20 +45,24 @@ declare global {
45
45
  options?: { signal: AbortSignal },
46
46
  ): void;
47
47
 
48
- removeEventListener(name: "regular-layout-update", cb: any): void;
48
+ addEventListener(
49
+ name: "regular-layout-before-update",
50
+ cb: (e: RegularLayoutEvent) => void,
51
+ options?: { signal: AbortSignal },
52
+ ): void;
53
+
54
+ removeEventListener(
55
+ name: "regular-layout-update",
56
+ cb: (e: RegularLayoutEvent) => void,
57
+ ): void;
58
+
59
+ removeEventListener(
60
+ name: "regular-layout-before-update",
61
+ cb: (e: RegularLayoutEvent) => void,
62
+ ): void;
49
63
  }
50
64
  }
51
65
 
52
66
  export interface RegularLayoutEvent extends CustomEvent {
53
67
  detail: Layout;
54
68
  }
55
-
56
- export interface PerspectiveViewerElementExt {
57
- addEventListener(
58
- name: "regular-layout-update",
59
- cb: (e: RegularLayoutEvent) => void,
60
- options?: { signal: AbortSignal },
61
- ): void;
62
-
63
- removeEventListener(name: "regular-layout-update", cb: any): void;
64
- }