@base44-preview/vite-plugin 0.2.22-pr.36.019589c → 0.2.22-pr.36.425440e

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 (40) hide show
  1. package/dist/injections/layer-dropdown/consts.d.ts +3 -0
  2. package/dist/injections/layer-dropdown/consts.d.ts.map +1 -1
  3. package/dist/injections/layer-dropdown/consts.js +3 -0
  4. package/dist/injections/layer-dropdown/consts.js.map +1 -1
  5. package/dist/injections/layer-dropdown/controller.d.ts +4 -0
  6. package/dist/injections/layer-dropdown/controller.d.ts.map +1 -0
  7. package/dist/injections/layer-dropdown/controller.js +85 -0
  8. package/dist/injections/layer-dropdown/controller.js.map +1 -0
  9. package/dist/injections/layer-dropdown/{component/dropdown-ui.d.ts → dropdown-ui.d.ts} +1 -4
  10. package/dist/injections/layer-dropdown/dropdown-ui.d.ts.map +1 -0
  11. package/dist/injections/layer-dropdown/{component/dropdown-ui.js → dropdown-ui.js} +76 -74
  12. package/dist/injections/layer-dropdown/dropdown-ui.js.map +1 -0
  13. package/dist/injections/layer-dropdown/types.d.ts +21 -0
  14. package/dist/injections/layer-dropdown/types.d.ts.map +1 -0
  15. package/dist/injections/layer-dropdown/types.js +3 -0
  16. package/dist/injections/layer-dropdown/types.js.map +1 -0
  17. package/dist/injections/layer-dropdown/utils.d.ts +6 -8
  18. package/dist/injections/layer-dropdown/utils.d.ts.map +1 -1
  19. package/dist/injections/layer-dropdown/utils.js +74 -33
  20. package/dist/injections/layer-dropdown/utils.js.map +1 -1
  21. package/dist/injections/utils.d.ts +0 -2
  22. package/dist/injections/utils.d.ts.map +1 -1
  23. package/dist/injections/utils.js +0 -13
  24. package/dist/injections/utils.js.map +1 -1
  25. package/dist/injections/visual-edit-agent.d.ts.map +1 -1
  26. package/dist/injections/visual-edit-agent.js +74 -199
  27. package/dist/injections/visual-edit-agent.js.map +1 -1
  28. package/dist/statics/index.mjs +1 -1
  29. package/dist/statics/index.mjs.map +1 -1
  30. package/package.json +1 -1
  31. package/src/injections/layer-dropdown/LAYERS.md +258 -0
  32. package/src/injections/layer-dropdown/consts.ts +3 -0
  33. package/src/injections/layer-dropdown/controller.ts +105 -0
  34. package/src/injections/layer-dropdown/{component/dropdown-ui.ts → dropdown-ui.ts} +109 -96
  35. package/src/injections/layer-dropdown/types.ts +24 -0
  36. package/src/injections/layer-dropdown/utils.ts +88 -41
  37. package/src/injections/utils.ts +0 -16
  38. package/src/injections/visual-edit-agent.ts +83 -237
  39. package/dist/injections/layer-dropdown/component/dropdown-ui.d.ts.map +0 -1
  40. package/dist/injections/layer-dropdown/component/dropdown-ui.js.map +0 -1
@@ -0,0 +1,105 @@
1
+ /** Controller that encapsulates layer-dropdown integration logic */
2
+
3
+ import { getElementSelectorId } from "../utils.js";
4
+ import { buildLayerChain } from "./utils.js";
5
+ import {
6
+ enhanceLabelWithChevron,
7
+ showDropdown,
8
+ closeDropdown,
9
+ isDropdownOpen,
10
+ } from "./dropdown-ui.js";
11
+ import type { LayerInfo, LayerControllerDeps, LayerController } from "./types.js";
12
+
13
+ export function createLayerController(deps: LayerControllerDeps): LayerController {
14
+ let layerPreviewOverlay: HTMLDivElement | null = null;
15
+ let escapeHandler: ((e: KeyboardEvent) => void) | null = null;
16
+ let dropdownSourceElement: Element | null = null;
17
+
18
+ const clearLayerPreview = () => {
19
+ if (layerPreviewOverlay && layerPreviewOverlay.parentNode) {
20
+ layerPreviewOverlay.remove();
21
+ }
22
+ layerPreviewOverlay = null;
23
+ };
24
+
25
+ const showLayerPreview = (layer: LayerInfo) => {
26
+ clearLayerPreview();
27
+ if (getElementSelectorId(layer.element) === deps.getSelectedElementId()) return;
28
+
29
+ layerPreviewOverlay = deps.createPreviewOverlay(layer.element);
30
+ };
31
+
32
+ const selectElementFromLayer = (layer: LayerInfo) => {
33
+ clearLayerPreview();
34
+ closeDropdown();
35
+ if (escapeHandler) {
36
+ document.removeEventListener("keydown", escapeHandler, true);
37
+ escapeHandler = null;
38
+ }
39
+ dropdownSourceElement = null;
40
+
41
+ const firstOverlay = deps.selectElement(layer.element);
42
+ attachToOverlay(firstOverlay, layer.element);
43
+ };
44
+
45
+ const reselectDropdownSource = () => {
46
+ if (escapeHandler) {
47
+ document.removeEventListener("keydown", escapeHandler, true);
48
+ escapeHandler = null;
49
+ }
50
+ if (dropdownSourceElement) {
51
+ selectElementFromLayer({
52
+ element: dropdownSourceElement,
53
+ tagName: dropdownSourceElement.tagName.toLowerCase(),
54
+ selectorId: getElementSelectorId(dropdownSourceElement),
55
+ });
56
+ dropdownSourceElement = null;
57
+ }
58
+ };
59
+
60
+ const attachToOverlay = (
61
+ overlay: HTMLDivElement | undefined,
62
+ element: Element
63
+ ) => {
64
+ if (!overlay) return;
65
+
66
+ const label = overlay.querySelector("div") as HTMLDivElement | null;
67
+ if (!label) return;
68
+
69
+ const layers = buildLayerChain(element);
70
+ if (layers.length <= 1) return;
71
+
72
+ const currentId = getElementSelectorId(element);
73
+ enhanceLabelWithChevron(label);
74
+
75
+ label.addEventListener("click", (e: MouseEvent) => {
76
+ e.stopPropagation();
77
+ e.preventDefault();
78
+ if (isDropdownOpen()) {
79
+ closeDropdown();
80
+ reselectDropdownSource();
81
+ } else {
82
+ dropdownSourceElement = element;
83
+ deps.onDeselect();
84
+
85
+ escapeHandler = (ev: KeyboardEvent) => {
86
+ if (ev.key === "Escape") {
87
+ ev.stopPropagation();
88
+ closeDropdown();
89
+ reselectDropdownSource();
90
+ }
91
+ };
92
+ document.addEventListener("keydown", escapeHandler, true);
93
+
94
+ showDropdown(label, layers, currentId, selectElementFromLayer, showLayerPreview, clearLayerPreview);
95
+ }
96
+ });
97
+ };
98
+
99
+ const cleanup = () => {
100
+ clearLayerPreview();
101
+ closeDropdown();
102
+ };
103
+
104
+ return { attachToOverlay, cleanup };
105
+ }
@@ -9,20 +9,59 @@ import {
9
9
  DROPDOWN_ITEM_HOVER_BG,
10
10
  DEPTH_INDENT_PX,
11
11
  LABEL_CHEVRON,
12
+ LABEL_CHEVRON_OPEN,
12
13
  LAYER_DROPDOWN_ATTR,
13
- } from "../consts.js";
14
- import { getLayerDisplayName } from "../utils.js";
15
- import type { LayerInfo } from "../utils.js";
16
-
17
- export type OnLayerSelect = (layer: LayerInfo) => void;
18
- export type OnLayerHover = (layer: LayerInfo) => void;
19
- export type OnLayerHoverEnd = () => void;
14
+ } from "./consts.js";
15
+ import { applyStyles, getLayerDisplayName } from "./utils.js";
16
+ import type { LayerInfo, OnLayerSelect, OnLayerHover, OnLayerHoverEnd } from "./types.js";
20
17
 
21
18
  let activeDropdown: HTMLDivElement | null = null;
19
+ let activeLabel: HTMLDivElement | null = null;
22
20
  let outsideMousedownHandler: ((e: MouseEvent) => void) | null = null;
23
21
  let activeOnHoverEnd: OnLayerHoverEnd | null = null;
24
22
  let activeKeydownHandler: ((e: KeyboardEvent) => void) | null = null;
25
23
 
24
+ function createDropdownItem(
25
+ layer: LayerInfo,
26
+ isActive: boolean,
27
+ onSelect: OnLayerSelect,
28
+ onHover?: OnLayerHover,
29
+ onHoverEnd?: OnLayerHoverEnd
30
+ ): HTMLDivElement {
31
+ const item = document.createElement("div");
32
+ item.textContent = getLayerDisplayName(layer);
33
+ applyStyles(item, DROPDOWN_ITEM_BASE_STYLES);
34
+
35
+ const depth = layer.depth ?? 0;
36
+ if (depth > 0) {
37
+ item.style.paddingLeft = `${12 + depth * DEPTH_INDENT_PX}px`;
38
+ }
39
+
40
+ if (isActive) {
41
+ item.style.color = DROPDOWN_ITEM_ACTIVE_COLOR;
42
+ item.style.backgroundColor = DROPDOWN_ITEM_ACTIVE_BG;
43
+ item.style.fontWeight = DROPDOWN_ITEM_ACTIVE_FONT_WEIGHT;
44
+ }
45
+
46
+ item.addEventListener("mouseenter", () => {
47
+ if (!isActive) item.style.backgroundColor = DROPDOWN_ITEM_HOVER_BG;
48
+ if (onHover) onHover(layer);
49
+ });
50
+
51
+ item.addEventListener("mouseleave", () => {
52
+ if (!isActive) item.style.backgroundColor = "transparent";
53
+ if (onHoverEnd) onHoverEnd();
54
+ });
55
+
56
+ item.addEventListener("click", (e: MouseEvent) => {
57
+ e.stopPropagation();
58
+ e.preventDefault();
59
+ onSelect(layer);
60
+ });
61
+
62
+ return item;
63
+ }
64
+
26
65
  /** Create the dropdown DOM element with layer items */
27
66
  export function createDropdownElement(
28
67
  layers: LayerInfo[],
@@ -33,54 +72,11 @@ export function createDropdownElement(
33
72
  ): HTMLDivElement {
34
73
  const container = document.createElement("div");
35
74
  container.setAttribute(LAYER_DROPDOWN_ATTR, "true");
36
-
37
- Object.keys(DROPDOWN_CONTAINER_STYLES).forEach((key) => {
38
- container.style[key as any] = DROPDOWN_CONTAINER_STYLES[key]!;
39
- });
75
+ applyStyles(container, DROPDOWN_CONTAINER_STYLES);
40
76
 
41
77
  layers.forEach((layer) => {
42
- const item = document.createElement("div");
43
78
  const isActive = layer.selectorId === currentSelectorId;
44
-
45
- item.textContent = getLayerDisplayName(layer);
46
-
47
- Object.keys(DROPDOWN_ITEM_BASE_STYLES).forEach((key) => {
48
- item.style[key as any] = DROPDOWN_ITEM_BASE_STYLES[key]!;
49
- });
50
-
51
- // Indent based on depth
52
- const depth = layer.depth ?? 0;
53
- if (depth > 0) {
54
- item.style.paddingLeft = `${12 + depth * DEPTH_INDENT_PX}px`;
55
- }
56
-
57
- if (isActive) {
58
- item.style.color = DROPDOWN_ITEM_ACTIVE_COLOR;
59
- item.style.backgroundColor = DROPDOWN_ITEM_ACTIVE_BG;
60
- item.style.fontWeight = DROPDOWN_ITEM_ACTIVE_FONT_WEIGHT;
61
- }
62
-
63
- item.addEventListener("mouseenter", () => {
64
- if (!isActive) {
65
- item.style.backgroundColor = DROPDOWN_ITEM_HOVER_BG;
66
- }
67
- if (onHover) onHover(layer);
68
- });
69
-
70
- item.addEventListener("mouseleave", () => {
71
- if (!isActive) {
72
- item.style.backgroundColor = "transparent";
73
- }
74
- if (onHoverEnd) onHoverEnd();
75
- });
76
-
77
- item.addEventListener("click", (e: MouseEvent) => {
78
- e.stopPropagation();
79
- e.preventDefault();
80
- onSelect(layer);
81
- });
82
-
83
- container.appendChild(item);
79
+ container.appendChild(createDropdownItem(layer, isActive, onSelect, onHover, onHoverEnd));
84
80
  });
85
81
 
86
82
  return container;
@@ -88,68 +84,38 @@ export function createDropdownElement(
88
84
 
89
85
  /** Add chevron indicator and pointer-events to the label */
90
86
  export function enhanceLabelWithChevron(label: HTMLDivElement): void {
91
- if (label.textContent?.includes(LABEL_CHEVRON)) return;
87
+ const t = label.textContent ?? "";
88
+ if (t.endsWith(LABEL_CHEVRON) || t.endsWith(LABEL_CHEVRON_OPEN)) return;
92
89
 
93
- label.textContent = label.textContent + LABEL_CHEVRON;
90
+ label.textContent = t + LABEL_CHEVRON;
94
91
  label.style.cursor = "pointer";
95
92
  label.style.userSelect = "none";
96
93
  label.style.pointerEvents = "auto";
97
94
  label.setAttribute(LAYER_DROPDOWN_ATTR, "true");
98
95
  }
99
96
 
100
- /** Show the dropdown below the label element */
101
- export function showDropdown(
102
- label: HTMLDivElement,
97
+ function setupKeyboardNavigation(
98
+ dropdown: HTMLDivElement,
103
99
  layers: LayerInfo[],
104
100
  currentSelectorId: string | null,
105
101
  onSelect: OnLayerSelect,
106
102
  onHover?: OnLayerHover,
107
103
  onHoverEnd?: OnLayerHoverEnd
108
104
  ): void {
109
- closeDropdown();
110
-
111
- const dropdown = createDropdownElement(
112
- layers,
113
- currentSelectorId,
114
- (layer) => {
115
- if (onHoverEnd) onHoverEnd();
116
- onSelect(layer);
117
- closeDropdown();
118
- },
119
- onHover,
120
- onHoverEnd
121
- );
122
-
123
- const overlay = label.parentElement;
124
- if (!overlay) return;
125
-
126
- // Position below the label
127
- dropdown.style.top = `${label.offsetTop + label.offsetHeight + 2}px`;
128
- dropdown.style.left = `${label.offsetLeft}px`;
129
-
130
- overlay.appendChild(dropdown);
131
- activeDropdown = dropdown;
132
- activeOnHoverEnd = onHoverEnd ?? null;
133
-
134
- // --- Arrow key navigation ---
135
105
  const items = Array.from(dropdown.children) as HTMLDivElement[];
136
- let focusedIndex = -1;
106
+ let focusedIndex = layers.findIndex((l) => l.selectorId === currentSelectorId);
137
107
 
138
108
  const setFocusedItem = (index: number) => {
139
- // Clear previous focus styling
140
109
  if (focusedIndex >= 0 && focusedIndex < items.length) {
141
110
  const prev = items[focusedIndex]!;
142
- const prevIsActive = prev.style.color === DROPDOWN_ITEM_ACTIVE_COLOR;
143
- if (!prevIsActive) {
111
+ if (prev.style.color !== DROPDOWN_ITEM_ACTIVE_COLOR) {
144
112
  prev.style.backgroundColor = "transparent";
145
113
  }
146
114
  }
147
115
  focusedIndex = index;
148
- // Apply hover styling to new focused item
149
116
  if (focusedIndex >= 0 && focusedIndex < items.length) {
150
117
  const cur = items[focusedIndex]!;
151
- const curIsActive = cur.style.color === DROPDOWN_ITEM_ACTIVE_COLOR;
152
- if (!curIsActive) {
118
+ if (cur.style.color !== DROPDOWN_ITEM_ACTIVE_COLOR) {
153
119
  cur.style.backgroundColor = DROPDOWN_ITEM_HOVER_BG;
154
120
  }
155
121
  cur.scrollIntoView({ block: "nearest" });
@@ -161,13 +127,11 @@ export function showDropdown(
161
127
  if (e.key === "ArrowDown") {
162
128
  e.preventDefault();
163
129
  e.stopPropagation();
164
- const next = focusedIndex < items.length - 1 ? focusedIndex + 1 : 0;
165
- setFocusedItem(next);
130
+ setFocusedItem(focusedIndex < items.length - 1 ? focusedIndex + 1 : 0);
166
131
  } else if (e.key === "ArrowUp") {
167
132
  e.preventDefault();
168
133
  e.stopPropagation();
169
- const prev = focusedIndex > 0 ? focusedIndex - 1 : items.length - 1;
170
- setFocusedItem(prev);
134
+ setFocusedItem(focusedIndex > 0 ? focusedIndex - 1 : items.length - 1);
171
135
  } else if (e.key === "Enter" && focusedIndex >= 0) {
172
136
  e.preventDefault();
173
137
  e.stopPropagation();
@@ -177,9 +141,12 @@ export function showDropdown(
177
141
  }
178
142
  };
179
143
  document.addEventListener("keydown", activeKeydownHandler, true);
144
+ }
180
145
 
181
- // Close on outside mousedown (mousedown fires before click, avoiding
182
- // conflict with handleElementClick's stopPropagation on click events)
146
+ function setupOutsideClickHandler(
147
+ dropdown: HTMLDivElement,
148
+ label: HTMLDivElement
149
+ ): void {
183
150
  setTimeout(() => {
184
151
  outsideMousedownHandler = (e: MouseEvent) => {
185
152
  const target = e.target as Node;
@@ -191,8 +158,54 @@ export function showDropdown(
191
158
  }, 0);
192
159
  }
193
160
 
161
+ /** Show the dropdown below the label element */
162
+ export function showDropdown(
163
+ label: HTMLDivElement,
164
+ layers: LayerInfo[],
165
+ currentSelectorId: string | null,
166
+ onSelect: OnLayerSelect,
167
+ onHover?: OnLayerHover,
168
+ onHoverEnd?: OnLayerHoverEnd
169
+ ): void {
170
+ closeDropdown();
171
+
172
+ const dropdown = createDropdownElement(
173
+ layers,
174
+ currentSelectorId,
175
+ (layer) => {
176
+ if (onHoverEnd) onHoverEnd();
177
+ onSelect(layer);
178
+ closeDropdown();
179
+ },
180
+ onHover,
181
+ onHoverEnd
182
+ );
183
+
184
+ const overlay = label.parentElement;
185
+ if (!overlay) return;
186
+
187
+ dropdown.style.top = `${label.offsetTop + label.offsetHeight + 2}px`;
188
+ dropdown.style.left = `${label.offsetLeft}px`;
189
+
190
+ overlay.appendChild(dropdown);
191
+ activeDropdown = dropdown;
192
+ activeLabel = label;
193
+ if (label.textContent?.endsWith(LABEL_CHEVRON.trim())) {
194
+ label.textContent = label.textContent.slice(0, -LABEL_CHEVRON.length) + LABEL_CHEVRON_OPEN;
195
+ }
196
+ activeOnHoverEnd = onHoverEnd ?? null;
197
+
198
+ setupKeyboardNavigation(dropdown, layers, currentSelectorId, onSelect, onHover, onHoverEnd);
199
+ setupOutsideClickHandler(dropdown, label);
200
+ }
201
+
194
202
  /** Close the active dropdown and clean up listeners */
195
203
  export function closeDropdown(): void {
204
+ if (activeLabel?.textContent?.includes(LABEL_CHEVRON_OPEN)) {
205
+ activeLabel.textContent = activeLabel.textContent.replace(LABEL_CHEVRON_OPEN, LABEL_CHEVRON);
206
+ }
207
+ activeLabel = null;
208
+
196
209
  if (activeOnHoverEnd) {
197
210
  activeOnHoverEnd();
198
211
  activeOnHoverEnd = null;
@@ -0,0 +1,24 @@
1
+ /** Shared types for the layer-dropdown module */
2
+
3
+ export interface LayerInfo {
4
+ element: Element;
5
+ tagName: string;
6
+ selectorId: string | null;
7
+ depth?: number;
8
+ }
9
+
10
+ export type OnLayerSelect = (layer: LayerInfo) => void;
11
+ export type OnLayerHover = (layer: LayerInfo) => void;
12
+ export type OnLayerHoverEnd = () => void;
13
+
14
+ export interface LayerControllerDeps {
15
+ createPreviewOverlay: (element: Element) => HTMLDivElement;
16
+ getSelectedElementId: () => string | null;
17
+ selectElement: (element: Element) => HTMLDivElement | undefined;
18
+ onDeselect: () => void;
19
+ }
20
+
21
+ export interface LayerController {
22
+ attachToOverlay: (overlay: HTMLDivElement | undefined, element: Element) => void;
23
+ cleanup: () => void;
24
+ }
@@ -1,13 +1,13 @@
1
- /** DOM traversal utilities for building the layer chain of instrumented elements */
1
+ /** DOM utilities for the layer-dropdown module */
2
2
 
3
3
  import { isInstrumentedElement, getElementSelectorId } from "../utils.js";
4
4
  import { MAX_PARENT_DEPTH, MAX_CHILD_DEPTH } from "./consts.js";
5
5
 
6
- export interface LayerInfo {
7
- element: Element;
8
- tagName: string;
9
- selectorId: string | null;
10
- depth?: number;
6
+ import type { LayerInfo } from "./types.js";
7
+
8
+ /** Apply a style map to an element */
9
+ export function applyStyles(element: HTMLElement, styles: Record<string, string>): void {
10
+ Object.assign(element.style, styles);
11
11
  }
12
12
 
13
13
  /** Display name for a layer — just the real tag name */
@@ -47,17 +47,18 @@ export function getInstrumentedDescendants(
47
47
  return result;
48
48
  }
49
49
 
50
- /**
51
- * Build the layer chain for the dropdown:
52
- *
53
- * Parents – up to MAX_PARENT_DEPTH instrumented ancestors, outer → inner.
54
- * Current – the selected element.
55
- * Children – instrumented descendants within MAX_CHILD_DEPTH levels, DOM order.
56
- *
57
- * Each item carries a `depth` for visual indentation.
58
- */
59
- export function buildLayerChain(selectedElement: Element): LayerInfo[] {
60
- // --- Parents (walk up, collect at most MAX_PARENT_DEPTH) ---
50
+ function toLayerInfo(element: Element, depth?: number): LayerInfo {
51
+ const info: LayerInfo = {
52
+ element,
53
+ tagName: element.tagName.toLowerCase(),
54
+ selectorId: getElementSelectorId(element),
55
+ };
56
+ if (depth !== undefined) info.depth = depth;
57
+ return info;
58
+ }
59
+
60
+ /** Collect instrumented ancestors from selected element up to MAX_PARENT_DEPTH (outermost first). */
61
+ function collectInstrumentedParents(selectedElement: Element): LayerInfo[] {
61
62
  const parents: LayerInfo[] = [];
62
63
  let current = selectedElement.parentElement;
63
64
  while (
@@ -67,44 +68,90 @@ export function buildLayerChain(selectedElement: Element): LayerInfo[] {
67
68
  parents.length < MAX_PARENT_DEPTH
68
69
  ) {
69
70
  if (isInstrumentedElement(current)) {
70
- parents.push({
71
- element: current,
72
- tagName: current.tagName.toLowerCase(),
73
- selectorId: getElementSelectorId(current),
74
- });
71
+ parents.push(toLayerInfo(current));
75
72
  }
76
73
  current = current.parentElement;
77
74
  }
78
- // Reverse so outermost parent comes first
79
75
  parents.reverse();
76
+ return parents;
77
+ }
80
78
 
81
- // --- Build the chain with depth ---
82
- const chain: LayerInfo[] = [];
83
- const baseDepth = 0;
84
-
85
- // Parents: depth 0, 1, …
79
+ /** Append parents to chain with depth 0, 1, …; returns depth of selected (parents.length). */
80
+ function appendParentsWithDepth(chain: LayerInfo[], parents: LayerInfo[]): number {
86
81
  parents.forEach((p, i) => {
87
- chain.push({ ...p, depth: baseDepth + i });
88
- });
89
-
90
- // Self
91
- const selfDepth = parents.length;
92
- chain.push({
93
- element: selectedElement,
94
- tagName: selectedElement.tagName.toLowerCase(),
95
- selectorId: getElementSelectorId(selectedElement),
96
- depth: selfDepth,
82
+ chain.push({ ...p, depth: i });
97
83
  });
84
+ return parents.length;
85
+ }
98
86
 
99
- // Children: up to MAX_CHILD_DEPTH instrumented levels below selected
87
+ /** Append selected element and its descendants at the given depth. */
88
+ function appendSelfAndDescendants(
89
+ chain: LayerInfo[],
90
+ selectedElement: Element,
91
+ selfDepth: number
92
+ ): void {
93
+ chain.push(toLayerInfo(selectedElement, selfDepth));
100
94
  const descendants = getInstrumentedDescendants(
101
95
  selectedElement,
102
96
  MAX_CHILD_DEPTH
103
97
  );
104
- // Assign visual depth: we need to track the instrumented nesting to set depth correctly
105
98
  assignDescendantDepths(selectedElement, descendants, selfDepth + 1);
106
-
107
99
  chain.push(...descendants);
100
+ }
101
+
102
+ /** Get the innermost instrumented parent's DOM element, or null if none. */
103
+ function getImmediateInstrParent(parents: LayerInfo[]): Element | null {
104
+ return parents.length > 0 ? parents[parents.length - 1]!.element : null;
105
+ }
106
+
107
+ /** Collect instrumented siblings of the selected element from its parent (DOM order). */
108
+ function collectSiblings(parent: Element, selectedElement: Element): LayerInfo[] {
109
+ const siblings = getInstrumentedDescendants(parent, 1);
110
+ if (!siblings.some((s) => s.element === selectedElement)) {
111
+ siblings.push(toLayerInfo(selectedElement));
112
+ }
113
+ return siblings;
114
+ }
115
+
116
+ /** Append siblings at selfDepth, expanding children only for the selected element. */
117
+ function appendSiblingsWithSelected(
118
+ chain: LayerInfo[],
119
+ siblings: LayerInfo[],
120
+ selectedElement: Element,
121
+ selfDepth: number
122
+ ): void {
123
+ for (const sibling of siblings) {
124
+ if (sibling.element === selectedElement) {
125
+ appendSelfAndDescendants(chain, selectedElement, selfDepth);
126
+ } else {
127
+ chain.push({ ...sibling, depth: selfDepth });
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Build the layer chain for the dropdown:
134
+ *
135
+ * Parents – up to MAX_PARENT_DEPTH instrumented ancestors, outer → inner.
136
+ * Siblings – instrumented children of the immediate parent, at the same depth.
137
+ * Current – the selected element (highlighted), with children expanded.
138
+ * Children – instrumented descendants within MAX_CHILD_DEPTH levels, DOM order.
139
+ *
140
+ * Each item carries a `depth` for visual indentation.
141
+ */
142
+ export function buildLayerChain(selectedElement: Element): LayerInfo[] {
143
+ const parents = collectInstrumentedParents(selectedElement);
144
+ const chain: LayerInfo[] = [];
145
+ const selfDepth = appendParentsWithDepth(chain, parents);
146
+
147
+ const instrParent = getImmediateInstrParent(parents);
148
+ if (instrParent) {
149
+ const siblings = collectSiblings(instrParent, selectedElement);
150
+ appendSiblingsWithSelected(chain, siblings, selectedElement, selfDepth);
151
+ } else {
152
+ appendSelfAndDescendants(chain, selectedElement, selfDepth);
153
+ }
154
+
108
155
  return chain;
109
156
  }
110
157
 
@@ -16,22 +16,6 @@ export function getElementSelectorId(element: Element): string | null {
16
16
  );
17
17
  }
18
18
 
19
- /** Find the nearest instrumented ancestor (not the element itself) */
20
- export function getImmediateInstrumentedParent(element: Element): Element | null {
21
- let current = element.parentElement;
22
- while (
23
- current &&
24
- current !== document.documentElement &&
25
- current !== document.body
26
- ) {
27
- if (isInstrumentedElement(current)) {
28
- return current;
29
- }
30
- current = current.parentElement;
31
- }
32
- return null;
33
- }
34
-
35
19
  /** Find elements by ID - first try data-source-location, fallback to data-visual-selector-id */
36
20
  export function findElementsById(id: string | null): Element[] {
37
21
  if (!id) return [];