@depup/base44__vite-plugin 1.0.4-depup.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 (167) hide show
  1. package/README.md +34 -0
  2. package/changes.json +22 -0
  3. package/compat/agents.cjs +13 -0
  4. package/compat/base44Client.cjs +6 -0
  5. package/compat/entities.cjs +25 -0
  6. package/compat/functions.cjs +9 -0
  7. package/compat/integrations.cjs +9 -0
  8. package/dist/ErrorOverlay.d.ts +12 -0
  9. package/dist/ErrorOverlay.d.ts.map +1 -0
  10. package/dist/ErrorOverlay.js +51 -0
  11. package/dist/ErrorOverlay.js.map +1 -0
  12. package/dist/bridge.d.ts +8 -0
  13. package/dist/bridge.d.ts.map +1 -0
  14. package/dist/bridge.js +8 -0
  15. package/dist/bridge.js.map +1 -0
  16. package/dist/capabilities/inline-edit/controller.d.ts +3 -0
  17. package/dist/capabilities/inline-edit/controller.d.ts.map +1 -0
  18. package/dist/capabilities/inline-edit/controller.js +203 -0
  19. package/dist/capabilities/inline-edit/controller.js.map +1 -0
  20. package/dist/capabilities/inline-edit/dom-utils.d.ts +7 -0
  21. package/dist/capabilities/inline-edit/dom-utils.d.ts.map +1 -0
  22. package/dist/capabilities/inline-edit/dom-utils.js +59 -0
  23. package/dist/capabilities/inline-edit/dom-utils.js.map +1 -0
  24. package/dist/capabilities/inline-edit/index.d.ts +3 -0
  25. package/dist/capabilities/inline-edit/index.d.ts.map +1 -0
  26. package/dist/capabilities/inline-edit/index.js +2 -0
  27. package/dist/capabilities/inline-edit/index.js.map +1 -0
  28. package/dist/capabilities/inline-edit/types.d.ts +29 -0
  29. package/dist/capabilities/inline-edit/types.d.ts.map +1 -0
  30. package/dist/capabilities/inline-edit/types.js +2 -0
  31. package/dist/capabilities/inline-edit/types.js.map +1 -0
  32. package/dist/consts.d.ts +11 -0
  33. package/dist/consts.d.ts.map +1 -0
  34. package/dist/consts.js +11 -0
  35. package/dist/consts.js.map +1 -0
  36. package/dist/error-overlay-plugin.d.ts +3 -0
  37. package/dist/error-overlay-plugin.d.ts.map +1 -0
  38. package/dist/error-overlay-plugin.js +15 -0
  39. package/dist/error-overlay-plugin.js.map +1 -0
  40. package/dist/html-injections-plugin.d.ts +8 -0
  41. package/dist/html-injections-plugin.d.ts.map +1 -0
  42. package/dist/html-injections-plugin.js +132 -0
  43. package/dist/html-injections-plugin.js.map +1 -0
  44. package/dist/index.d.ts +9 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +158 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/injections/layer-dropdown/consts.d.ts +20 -0
  49. package/dist/injections/layer-dropdown/consts.d.ts.map +1 -0
  50. package/dist/injections/layer-dropdown/consts.js +41 -0
  51. package/dist/injections/layer-dropdown/consts.js.map +1 -0
  52. package/dist/injections/layer-dropdown/controller.d.ts +4 -0
  53. package/dist/injections/layer-dropdown/controller.d.ts.map +1 -0
  54. package/dist/injections/layer-dropdown/controller.js +88 -0
  55. package/dist/injections/layer-dropdown/controller.js.map +1 -0
  56. package/dist/injections/layer-dropdown/dropdown-ui.d.ts +13 -0
  57. package/dist/injections/layer-dropdown/dropdown-ui.d.ts.map +1 -0
  58. package/dist/injections/layer-dropdown/dropdown-ui.js +186 -0
  59. package/dist/injections/layer-dropdown/dropdown-ui.js.map +1 -0
  60. package/dist/injections/layer-dropdown/types.d.ts +26 -0
  61. package/dist/injections/layer-dropdown/types.d.ts.map +1 -0
  62. package/dist/injections/layer-dropdown/types.js +3 -0
  63. package/dist/injections/layer-dropdown/types.js.map +1 -0
  64. package/dist/injections/layer-dropdown/utils.d.ts +25 -0
  65. package/dist/injections/layer-dropdown/utils.d.ts.map +1 -0
  66. package/dist/injections/layer-dropdown/utils.js +143 -0
  67. package/dist/injections/layer-dropdown/utils.js.map +1 -0
  68. package/dist/injections/navigation-notifier.d.ts +2 -0
  69. package/dist/injections/navigation-notifier.d.ts.map +1 -0
  70. package/dist/injections/navigation-notifier.js +34 -0
  71. package/dist/injections/navigation-notifier.js.map +1 -0
  72. package/dist/injections/sandbox-hmr-notifier.d.ts +2 -0
  73. package/dist/injections/sandbox-hmr-notifier.d.ts.map +1 -0
  74. package/dist/injections/sandbox-hmr-notifier.js +10 -0
  75. package/dist/injections/sandbox-hmr-notifier.js.map +1 -0
  76. package/dist/injections/sandbox-mount-observer.d.ts +2 -0
  77. package/dist/injections/sandbox-mount-observer.d.ts.map +1 -0
  78. package/dist/injections/sandbox-mount-observer.js +18 -0
  79. package/dist/injections/sandbox-mount-observer.js.map +1 -0
  80. package/dist/injections/unhandled-errors-handlers.d.ts +2 -0
  81. package/dist/injections/unhandled-errors-handlers.d.ts.map +1 -0
  82. package/dist/injections/unhandled-errors-handlers.js +93 -0
  83. package/dist/injections/unhandled-errors-handlers.js.map +1 -0
  84. package/dist/injections/utils.d.ts +65 -0
  85. package/dist/injections/utils.d.ts.map +1 -0
  86. package/dist/injections/utils.js +186 -0
  87. package/dist/injections/utils.js.map +1 -0
  88. package/dist/injections/visual-edit-agent.d.ts +2 -0
  89. package/dist/injections/visual-edit-agent.d.ts.map +1 -0
  90. package/dist/injections/visual-edit-agent.js +583 -0
  91. package/dist/injections/visual-edit-agent.js.map +1 -0
  92. package/dist/jsx-processor.d.ts +17 -0
  93. package/dist/jsx-processor.d.ts.map +1 -0
  94. package/dist/jsx-processor.js +129 -0
  95. package/dist/jsx-processor.js.map +1 -0
  96. package/dist/jsx-utils.d.ts +16 -0
  97. package/dist/jsx-utils.d.ts.map +1 -0
  98. package/dist/jsx-utils.js +98 -0
  99. package/dist/jsx-utils.js.map +1 -0
  100. package/dist/processors/collection-id-processor.d.ts +20 -0
  101. package/dist/processors/collection-id-processor.d.ts.map +1 -0
  102. package/dist/processors/collection-id-processor.js +182 -0
  103. package/dist/processors/collection-id-processor.js.map +1 -0
  104. package/dist/processors/collection-item-field-processor.d.ts +39 -0
  105. package/dist/processors/collection-item-field-processor.d.ts.map +1 -0
  106. package/dist/processors/collection-item-field-processor.js +289 -0
  107. package/dist/processors/collection-item-field-processor.js.map +1 -0
  108. package/dist/processors/collection-item-id-processor.d.ts +12 -0
  109. package/dist/processors/collection-item-id-processor.d.ts.map +1 -0
  110. package/dist/processors/collection-item-id-processor.js +50 -0
  111. package/dist/processors/collection-item-id-processor.js.map +1 -0
  112. package/dist/processors/static-array-processor.d.ts +28 -0
  113. package/dist/processors/static-array-processor.d.ts.map +1 -0
  114. package/dist/processors/static-array-processor.js +173 -0
  115. package/dist/processors/static-array-processor.js.map +1 -0
  116. package/dist/processors/utils/collection-tracing-utils.d.ts +36 -0
  117. package/dist/processors/utils/collection-tracing-utils.d.ts.map +1 -0
  118. package/dist/processors/utils/collection-tracing-utils.js +390 -0
  119. package/dist/processors/utils/collection-tracing-utils.js.map +1 -0
  120. package/dist/processors/utils/shared-utils.d.ts +96 -0
  121. package/dist/processors/utils/shared-utils.d.ts.map +1 -0
  122. package/dist/processors/utils/shared-utils.js +600 -0
  123. package/dist/processors/utils/shared-utils.js.map +1 -0
  124. package/dist/statics/index.mjs +16 -0
  125. package/dist/statics/index.mjs.map +1 -0
  126. package/dist/utils.d.ts +2 -0
  127. package/dist/utils.d.ts.map +1 -0
  128. package/dist/utils.js +22 -0
  129. package/dist/utils.js.map +1 -0
  130. package/dist/visual-edit-plugin.d.ts +3 -0
  131. package/dist/visual-edit-plugin.d.ts.map +1 -0
  132. package/dist/visual-edit-plugin.js +100 -0
  133. package/dist/visual-edit-plugin.js.map +1 -0
  134. package/package.json +75 -0
  135. package/src/ErrorOverlay.ts +71 -0
  136. package/src/bridge.ts +8 -0
  137. package/src/capabilities/inline-edit/controller.ts +254 -0
  138. package/src/capabilities/inline-edit/dom-utils.ts +58 -0
  139. package/src/capabilities/inline-edit/index.ts +2 -0
  140. package/src/capabilities/inline-edit/types.ts +35 -0
  141. package/src/consts.ts +11 -0
  142. package/src/error-overlay-plugin.ts +19 -0
  143. package/src/html-injections-plugin.ts +166 -0
  144. package/src/index.ts +225 -0
  145. package/src/injections/layer-dropdown/LAYERS.md +258 -0
  146. package/src/injections/layer-dropdown/consts.ts +51 -0
  147. package/src/injections/layer-dropdown/controller.ts +109 -0
  148. package/src/injections/layer-dropdown/dropdown-ui.ts +242 -0
  149. package/src/injections/layer-dropdown/types.ts +30 -0
  150. package/src/injections/layer-dropdown/utils.ts +175 -0
  151. package/src/injections/navigation-notifier.ts +43 -0
  152. package/src/injections/sandbox-hmr-notifier.ts +8 -0
  153. package/src/injections/sandbox-mount-observer.ts +25 -0
  154. package/src/injections/unhandled-errors-handlers.ts +114 -0
  155. package/src/injections/utils.ts +208 -0
  156. package/src/injections/visual-edit-agent.ts +706 -0
  157. package/src/jsx-processor.ts +169 -0
  158. package/src/jsx-utils.ts +131 -0
  159. package/src/processors/collection-id-processor.ts +261 -0
  160. package/src/processors/collection-item-field-processor.ts +439 -0
  161. package/src/processors/collection-item-id-processor.ts +69 -0
  162. package/src/processors/static-array-processor.ts +260 -0
  163. package/src/processors/utils/collection-tracing-utils.ts +507 -0
  164. package/src/processors/utils/shared-utils.ts +785 -0
  165. package/src/utils.ts +27 -0
  166. package/src/visual-edit-plugin.md +358 -0
  167. package/src/visual-edit-plugin.ts +110 -0
package/src/index.ts ADDED
@@ -0,0 +1,225 @@
1
+ import type { Plugin, UserConfig } from "vite";
2
+ import { loadEnv } from "vite";
3
+ import { errorOverlayPlugin } from "./error-overlay-plugin.js";
4
+ import { visualEditPlugin } from "./visual-edit-plugin.js";
5
+ import { filterPackagesInProject } from "./utils.js";
6
+ import { htmlInjectionsPlugin } from "./html-injections-plugin.js";
7
+
8
+ const isRunningInSandbox = !!process.env.MODAL_SANDBOX_ID;
9
+
10
+ export default function vitePlugin(
11
+ opts: {
12
+ legacySDKImports?: boolean;
13
+ hmrNotifier?: boolean;
14
+ navigationNotifier?: boolean;
15
+ visualEditAgent?: boolean;
16
+ analyticsTracker?: boolean;
17
+ } = {}
18
+ ) {
19
+ const {
20
+ legacySDKImports = false,
21
+ hmrNotifier = false,
22
+ navigationNotifier = false,
23
+ visualEditAgent = false,
24
+ analyticsTracker = false,
25
+ } = opts;
26
+
27
+ return [
28
+ {
29
+ name: "base44",
30
+ config: ({ mode, root = process.cwd() }) => {
31
+ const env = loadEnv(mode ?? "development", root, "");
32
+
33
+ return {
34
+ resolve: {
35
+ alias: {
36
+ "@/": "/src/",
37
+ },
38
+ },
39
+ ...(legacySDKImports
40
+ ? {
41
+ define: {
42
+ "process.env.VITE_BASE44_APP_ID": JSON.stringify(
43
+ env.VITE_BASE44_APP_ID
44
+ ),
45
+ "process.env.VITE_BASE44_BACKEND_URL": JSON.stringify(
46
+ env.VITE_BASE44_BACKEND_URL
47
+ ),
48
+ },
49
+ }
50
+ : {}),
51
+ ...(isRunningInSandbox
52
+ ? ({
53
+ server: {
54
+ cors: true,
55
+ host: "0.0.0.0", // Bind to all interfaces for container access
56
+ port: 5173,
57
+ strictPort: true,
58
+ // Allow all hosts - essential for Modal tunnel URLs
59
+ allowedHosts: true,
60
+ watch: {
61
+ // Enable polling for better file change detection in containers
62
+ usePolling: true,
63
+ interval: 100, // Check every 100ms for responsive HMR
64
+ // Wait for file writes to complete before triggering HMR
65
+ awaitWriteFinish: {
66
+ stabilityThreshold: 150,
67
+ pollInterval: 50,
68
+ },
69
+ },
70
+ },
71
+ build: {
72
+ rollupOptions: {
73
+ onwarn(warning, warn) {
74
+ // Treat import errors as fatal errors
75
+ if (
76
+ warning.code === "UNRESOLVED_IMPORT" ||
77
+ warning.code === "MISSING_EXPORT"
78
+ ) {
79
+ throw new Error(`Build failed: ${warning.message}`);
80
+ }
81
+ // Use default for other warnings
82
+ warn(warning);
83
+ },
84
+ },
85
+ },
86
+ } as Partial<UserConfig>)
87
+ : (() => {
88
+ if (env.VITE_BASE44_APP_BASE_URL) {
89
+ console.log(
90
+ `[base44] Proxy enabled: /api -> ${env.VITE_BASE44_APP_BASE_URL}`
91
+ );
92
+ return {
93
+ server: {
94
+ proxy: {
95
+ "/api": {
96
+ target: env.VITE_BASE44_APP_BASE_URL,
97
+ changeOrigin: true,
98
+ },
99
+ },
100
+ },
101
+ };
102
+ }
103
+ console.log(
104
+ "[base44] Proxy not enabled (VITE_BASE44_APP_BASE_URL not set)"
105
+ );
106
+ return {};
107
+ })()),
108
+ optimizeDeps: {
109
+ ...(isRunningInSandbox
110
+ ? {
111
+ include: filterPackagesInProject(
112
+ [
113
+ "react",
114
+ "react-dom",
115
+ "framer-motion",
116
+ "lodash",
117
+ "moment",
118
+ "react-quill",
119
+ ],
120
+ root
121
+ ),
122
+ }
123
+ : {}),
124
+ esbuildOptions: {
125
+ loader: {
126
+ ".js": "jsx",
127
+ },
128
+ },
129
+ },
130
+ };
131
+ },
132
+ async resolveId(source, importer, options) {
133
+ // only resolve imports in the legacy SDK if they are not in an HTML file
134
+ // because we might have pages that include /integrations, /functions, etc.
135
+ if (legacySDKImports && !importer?.endsWith(".html")) {
136
+ const existingResolution = await this.resolve(
137
+ source,
138
+ importer,
139
+ options
140
+ );
141
+
142
+ if (existingResolution) {
143
+ return existingResolution;
144
+ }
145
+
146
+ // in legacy apps, the AI sometimes imports components from the Layout with `../`
147
+ // which breaks when monving to the vite template, so this solved it by
148
+ // resolving the path to the components folder
149
+ if (
150
+ importer?.endsWith("/src/Layout.jsx") &&
151
+ source.startsWith("../components")
152
+ ) {
153
+ return this.resolve(
154
+ source.replace(/^..\/components/, "@/components"),
155
+ importer,
156
+ options
157
+ );
158
+ }
159
+
160
+ // Handle imports of Layout.js or Layout.jsx from pages directory
161
+ if (
162
+ importer?.includes("/pages") &&
163
+ (source.toLowerCase().endsWith("layout.jsx") ||
164
+ source.toLowerCase().endsWith("layout.js"))
165
+ ) {
166
+ return this.resolve("@/Layout.jsx", importer, options);
167
+ }
168
+
169
+ if (source.includes("/entities")) {
170
+ return this.resolve(
171
+ "@base44/vite-plugin/compat/entities.cjs",
172
+ importer,
173
+ options
174
+ );
175
+ }
176
+
177
+ if (source.includes("/functions")) {
178
+ return this.resolve(
179
+ "@base44/vite-plugin/compat/functions.cjs",
180
+ importer,
181
+ options
182
+ );
183
+ }
184
+
185
+ if (source.includes("/integrations")) {
186
+ return this.resolve(
187
+ "@base44/vite-plugin/compat/integrations.cjs",
188
+ importer,
189
+ options
190
+ );
191
+ }
192
+
193
+ if (source.includes("/agents")) {
194
+ return this.resolve(
195
+ "@base44/vite-plugin/compat/agents.cjs",
196
+ importer,
197
+ options
198
+ );
199
+ }
200
+ }
201
+
202
+ return null;
203
+ },
204
+ } as Plugin,
205
+ ...(isRunningInSandbox
206
+ ? [
207
+ {
208
+ name: "iframe-hmr",
209
+ configureServer(server) {
210
+ server.middlewares.use((req, res, next) => {
211
+ // Allow iframe embedding
212
+ res.setHeader("X-Frame-Options", "ALLOWALL");
213
+ res.setHeader("Content-Security-Policy", "frame-ancestors *;");
214
+ next();
215
+ });
216
+ },
217
+ } as Plugin,
218
+ errorOverlayPlugin(),
219
+ visualEditPlugin(),
220
+ ]
221
+ : []),
222
+ // HTML injections - handles both dev (sandbox) and production injections
223
+ htmlInjectionsPlugin({ hmrNotifier, navigationNotifier, visualEditAgent, analyticsTracker }),
224
+ ];
225
+ }
@@ -0,0 +1,258 @@
1
+ # Layer Dropdown Feature
2
+
3
+ ## Overview
4
+
5
+ The layer dropdown lets users navigate the instrumented DOM hierarchy around a selected element. Clicking the chevron (`▾`) on an element's label reveals a dropdown showing **ancestors**, **siblings**, **the selected element**, and its **children** — all with depth-based indentation.
6
+
7
+ ```
8
+ grandparent (depth 0)
9
+ parent (depth 1)
10
+ sibling-1 (depth 2)
11
+ ★ selected (depth 2) ← highlighted in blue
12
+ child-a (depth 3)
13
+ child-b (depth 3)
14
+ sibling-2 (depth 2)
15
+ ```
16
+
17
+ Children are only expanded for the selected element, not for siblings.
18
+
19
+ ---
20
+
21
+ ## Architecture
22
+
23
+ ```
24
+ ┌─────────────┐ ┌────────────────┐ ┌────────────────┐
25
+ │ types.ts │◄────│ controller.ts │────►│ dropdown-ui.ts│
26
+ │ consts.ts │◄────│ (orchestrator)│ │ (rendering) │
27
+ └─────────────┘ └───────┬────────┘ └────────────────┘
28
+
29
+ ┌─────▼──────┐
30
+ │ utils.ts │
31
+ │ (DOM walk) │
32
+ └────────────┘
33
+ ```
34
+
35
+ | File | Role |
36
+ |------|------|
37
+ | **types.ts** | `LayerInfo`, callback types, `LayerControllerDeps`, `LayerController` |
38
+ | **consts.ts** | Style maps, depth limits, chevron character, attribute name |
39
+ | **utils.ts** | DOM traversal — parent walking, sibling/descendant collection, depth assignment |
40
+ | **dropdown-ui.ts** | Creates the dropdown DOM, positions it, handles hover/click/keyboard |
41
+ | **controller.ts** | Stateful factory that wires everything together: build chain, show dropdown, handle selection |
42
+
43
+ ---
44
+
45
+ ## Data Model
46
+
47
+ ### `LayerInfo`
48
+
49
+ ```typescript
50
+ interface LayerInfo {
51
+ element: Element; // DOM reference
52
+ tagName: string; // lowercase tag name ("div", "button")
53
+ selectorId: string | null; // data-source-location or data-visual-selector-id
54
+ depth?: number; // visual indentation level (set during chain building)
55
+ }
56
+ ```
57
+
58
+ An element is **instrumented** if it has `dataset.sourceLocation` or `dataset.visualSelectorId`. Only instrumented elements appear in the dropdown.
59
+
60
+ ### Key Constants
61
+
62
+ | Constant | Value | Purpose |
63
+ |----------|-------|---------|
64
+ | `MAX_PARENT_DEPTH` | `2` | Max ancestor levels shown above selected |
65
+ | `MAX_CHILD_DEPTH` | `2` | Max descendant levels shown below selected |
66
+ | `DEPTH_INDENT_PX` | `10` | Extra left-padding per depth level |
67
+ | `LABEL_CHEVRON` | `" ▾"` | Appended to label text when dropdown is available |
68
+
69
+ ---
70
+
71
+ ## How the Chain Is Built (`utils.ts`)
72
+
73
+ `buildLayerChain(selectedElement)` is the entry point. It delegates to focused helper functions:
74
+
75
+ ```typescript
76
+ export function buildLayerChain(selectedElement: Element): LayerInfo[] {
77
+ const parents = collectInstrumentedParents(selectedElement);
78
+ const chain: LayerInfo[] = [];
79
+ const selfDepth = appendParentsWithDepth(chain, parents);
80
+
81
+ const instrParent = getImmediateInstrParent(parents);
82
+ if (instrParent) {
83
+ const siblings = collectSiblings(instrParent, selectedElement);
84
+ appendSiblingsWithSelected(chain, siblings, selectedElement, selfDepth);
85
+ } else {
86
+ appendSelfAndDescendants(chain, selectedElement, selfDepth);
87
+ }
88
+
89
+ return chain;
90
+ }
91
+ ```
92
+
93
+ ### Helper Functions
94
+
95
+ | Function | Purpose |
96
+ |----------|---------|
97
+ | `toLayerInfo(element, depth?)` | Convert a DOM element to a `LayerInfo` object |
98
+ | `collectInstrumentedParents(el)` | Walk up the DOM, collect up to `MAX_PARENT_DEPTH` instrumented ancestors, return outermost-first |
99
+ | `appendParentsWithDepth(chain, parents)` | Push parents into chain with `depth = index` (0, 1, ...), return count as `selfDepth` |
100
+ | `getImmediateInstrParent(parents)` | Return the innermost parent's DOM element, or `null` if `parents` is empty |
101
+ | `collectSiblings(parent, selectedEl)` | Call `getInstrumentedDescendants(parent, 1)` to get all first-level instrumented children in DOM order; safety-guard ensures `selectedEl` is always included |
102
+ | `appendSiblingsWithSelected(chain, siblings, selectedEl, depth)` | Iterate siblings at `selfDepth`; for the selected element, expand children via `appendSelfAndDescendants`; for others, just add at `selfDepth` |
103
+ | `appendSelfAndDescendants(chain, el, depth)` | Push element + its descendants (up to `MAX_CHILD_DEPTH` levels) with assigned depths |
104
+ | `getInstrumentedDescendants(parent, maxDepth)` | Recursive DOM walk — instrumented children increment depth, non-instrumented wrappers are transparent. Results in DOM order |
105
+ | `assignDescendantDepths(root, descendants, startDepth)` | Second-pass walk assigning `depth = startDepth + instrDepth - 1` using a `Set`/`Map` for O(1) lookups |
106
+
107
+ ### Chain-Building Steps
108
+
109
+ **Step 1 — `collectInstrumentedParents`**: Walk up from `selectedElement.parentElement`, skipping `document.body` and `document.documentElement`. Collect up to `MAX_PARENT_DEPTH` instrumented ancestors, then reverse so outermost comes first.
110
+
111
+ **Step 2 — `appendParentsWithDepth`**: Each parent gets `depth = index` (0, 1, ...). The count becomes `selfDepth`.
112
+
113
+ **Step 3 — `getImmediateInstrParent`**: Extract the innermost instrumented parent's element. If `parents` is empty, returns `null` (root-level element).
114
+
115
+ **Step 4 — `collectSiblings`** (when parent exists): Call `getInstrumentedDescendants(instrParent, 1)` to get all first-level instrumented children of the parent in DOM order — this naturally includes the selected element among its siblings. A safety guard ensures the selected element is always present.
116
+
117
+ **Step 5 — `appendSiblingsWithSelected`**: Iterate siblings in DOM order, all at `selfDepth`. For the selected element only, delegate to `appendSelfAndDescendants` which also collects and appends children. For other siblings, just push at `selfDepth` with no child expansion.
118
+
119
+ **Fallback** (no parent): If no instrumented parent exists (root-level element), skip sibling collection and call `appendSelfAndDescendants` directly — just self + children.
120
+
121
+ ---
122
+
123
+ ## Controller Lifecycle (`controller.ts`)
124
+
125
+ `createLayerController(deps)` returns `{ attachToOverlay, cleanup }`.
126
+
127
+ ### Dependency Injection
128
+
129
+ | Dep | Called When |
130
+ |-----|------------|
131
+ | `createPreviewOverlay(el)` | User hovers a dropdown item |
132
+ | `getSelectedElementId()` | Checking if hovered item is already selected |
133
+ | `selectElement(el)` | User clicks a dropdown item — performs selection, returns overlay |
134
+ | `onDeselect()` | Dropdown opens — temporarily clears selection for hover previews |
135
+
136
+ ### `attachToOverlay(overlay, element)`
137
+
138
+ 1. Extract the label `<div>` from the overlay.
139
+ 2. Call `buildLayerChain(element)`.
140
+ 3. **Guard**: if `layers.length <= 1`, return early — no chevron, no click handler. This is how the chevron is hidden when there's nothing to navigate to.
141
+ 4. Append chevron via `enhanceLabelWithChevron(label)`.
142
+ 5. Bind click handler to label (toggle behavior).
143
+
144
+ ### Click Handler Flow
145
+
146
+ ```
147
+ Label clicked
148
+ ├─ Dropdown already open → close + reselect source element
149
+ └─ Dropdown closed →
150
+ 1. Save source element (for Escape restore)
151
+ 2. deps.onDeselect() (clear selection visual)
152
+ 3. Register Escape key listener (capture phase)
153
+ 4. showDropdown(label, layers, callbacks...)
154
+ ```
155
+
156
+ ### State Transitions
157
+
158
+ ```
159
+ CLOSED ──[click chevron]──► OPEN
160
+ OPEN ──[click chevron]──► CLOSED (reselect original)
161
+ OPEN ──[Escape]──────────► CLOSED (reselect original)
162
+ OPEN ──[click item]─────► CLOSED → NEW ELEMENT SELECTED → reattach
163
+ OPEN ──[click outside]──► CLOSED
164
+ ```
165
+
166
+ Selection is recursive — `selectElementFromLayer` calls `deps.selectElement()`, gets a new overlay, and calls `attachToOverlay()` again on it.
167
+
168
+ ---
169
+
170
+ ## Dropdown UI (`dropdown-ui.ts`)
171
+
172
+ ### Global State (singleton)
173
+
174
+ Only one dropdown can be active at a time:
175
+
176
+ ```typescript
177
+ let activeDropdown: HTMLDivElement | null = null;
178
+ let outsideMousedownHandler: ((e: MouseEvent) => void) | null = null;
179
+ let activeOnHoverEnd: OnLayerHoverEnd | null = null;
180
+ let activeKeydownHandler: ((e: KeyboardEvent) => void) | null = null;
181
+ ```
182
+
183
+ ### `showDropdown(label, layers, currentSelectorId, onSelect, onHover, onHoverEnd)`
184
+
185
+ 1. Close any existing dropdown.
186
+ 2. Create dropdown element with all layer items.
187
+ 3. Position it below the label (`offsetTop + offsetHeight + 2px` gap).
188
+ 4. Append to the overlay's parent element.
189
+ 5. Set up keyboard navigation and outside-click handler.
190
+
191
+ ### Dropdown Item Rendering
192
+
193
+ Each `LayerInfo` becomes a `<div>` row:
194
+
195
+ - **Display name**: `layer.tagName` (e.g., "div", "section")
196
+ - **Indentation**: `paddingLeft = 12 + depth * 10` px
197
+ - **Active (selected) item**: blue text (`#526cff`), light blue bg (`#DBEAFE`), semi-bold
198
+ - **Hover state**: light gray bg (`#f1f5f9`)
199
+ - **Events**: `mouseenter` → preview, `mouseleave` → clear preview, `click` → select
200
+
201
+ ### Keyboard Navigation
202
+
203
+ - **Arrow Down/Up**: Circular focus movement through items
204
+ - **Enter**: Select focused item
205
+ - All listeners use capture phase to intercept before bubbling
206
+
207
+ ### Outside Click
208
+
209
+ Registered via `setTimeout(..., 0)` to prevent the opening click from immediately closing. Uses `mousedown` in capture phase for responsive dismissal.
210
+
211
+ ---
212
+
213
+ ## Edge Cases
214
+
215
+ | Scenario | Behavior |
216
+ |----------|----------|
217
+ | No instrumented parent (root element) | Falls back to self + children only (no siblings) |
218
+ | Only child (no other siblings) | Siblings list contains just self — same result as before |
219
+ | Parent is `document.body`/`html` | Excluded by parent-walk guard, so `parents` is empty |
220
+ | `layers.length <= 1` | No chevron appended, no click handler attached |
221
+ | Element not in siblings list | Safety guard appends it |
222
+ | Non-instrumented wrapper elements | Walked through transparently during descendant collection |
223
+
224
+ ---
225
+
226
+ ## Data Flow Summary
227
+
228
+ ```
229
+ User selects element
230
+
231
+ attachToOverlay(overlay, element)
232
+
233
+ buildLayerChain(element)
234
+ ├── collectInstrumentedParents() → parents[] (outermost first)
235
+ ├── appendParentsWithDepth() → chain gets parents at depth 0,1,...
236
+ ├── getImmediateInstrParent() → instrParent or null
237
+ ├── collectSiblings(instrParent) → siblings[] (DOM order)
238
+ └── appendSiblingsWithSelected()
239
+ ├── sibling → chain at selfDepth (no expansion)
240
+ ├── ★ selected → appendSelfAndDescendants()
241
+ │ ├── self at selfDepth
242
+ │ ├── getInstrumentedDescendants(self, 2) → children[]
243
+ │ └── assignDescendantDepths() → children get depth values
244
+ └── sibling → chain at selfDepth (no expansion)
245
+
246
+ LayerInfo[] with depth values
247
+
248
+ layers.length > 1 ? enhanceLabelWithChevron() : return
249
+
250
+ User clicks chevron → showDropdown()
251
+
252
+ createDropdownElement(layers, currentId, callbacks)
253
+ └── createDropdownItem() for each layer (indented by depth)
254
+
255
+ User hovers item → showLayerPreview() → preview overlay
256
+ User clicks item → selectElementFromLayer() → new selection → reattach
257
+ User presses Escape → closeDropdown() → reselect original
258
+ ```
@@ -0,0 +1,51 @@
1
+ /** Style constants for the layer dropdown UI */
2
+
3
+ export const DROPDOWN_CONTAINER_STYLES: Record<string, string> = {
4
+ position: "absolute",
5
+ backgroundColor: "#ffffff",
6
+ border: "1px solid #e2e8f0",
7
+ borderRadius: "6px",
8
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
9
+ fontSize: "12px",
10
+ minWidth: "120px",
11
+ maxHeight: "200px",
12
+ overflowY: "auto",
13
+ zIndex: "10001",
14
+ padding: "4px 0",
15
+ pointerEvents: "auto",
16
+ };
17
+
18
+ export const DROPDOWN_ITEM_BASE_STYLES: Record<string, string> = {
19
+ padding: "4px 12px",
20
+ cursor: "pointer",
21
+ color: "#334155",
22
+ backgroundColor: "transparent",
23
+ whiteSpace: "nowrap",
24
+ lineHeight: "1.5",
25
+ fontWeight: "400",
26
+ };
27
+
28
+ export const DROPDOWN_ITEM_ACTIVE_COLOR = "#526cff";
29
+ export const DROPDOWN_ITEM_ACTIVE_BG = "#DBEAFE";
30
+ export const DROPDOWN_ITEM_ACTIVE_FONT_WEIGHT = "600";
31
+
32
+ export const DROPDOWN_ITEM_HOVER_BG = "#f1f5f9";
33
+
34
+ export const DEPTH_INDENT_PX = 10;
35
+
36
+ /** SVG chevron shown when dropdown is collapsed (click to expand) */
37
+ export const CHEVRON_COLLAPSED = `<svg width="12" height="12" viewBox="0 0 24 24" style="vertical-align:middle;margin-left:4px"><path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" fill="none"/></svg>`;
38
+ /** SVG chevron shown when dropdown is expanded (click to collapse) */
39
+ export const CHEVRON_EXPANDED = `<svg width="12" height="12" viewBox="0 0 24 24" style="vertical-align:middle;margin-left:4px"><path d="M18 15l-6-6-6 6" stroke="currentColor" stroke-width="2" fill="none"/></svg>`;
40
+
41
+ export const CHEVRON_ATTR = "data-chevron";
42
+
43
+ export const BASE_PADDING_PX = 12;
44
+
45
+ export const LAYER_DROPDOWN_ATTR = "data-layer-dropdown";
46
+
47
+ /** Max instrumented ancestors to show above the selected element */
48
+ export const MAX_PARENT_DEPTH = 2;
49
+
50
+ /** Max instrumented depth levels to show below the selected element */
51
+ export const MAX_CHILD_DEPTH = 2;
@@ -0,0 +1,109 @@
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, LayerControllerConfig, LayerController } from "./types.js";
12
+
13
+ export function createLayerController(config: LayerControllerConfig): LayerController {
14
+ let layerPreviewOverlay: HTMLDivElement | null = null;
15
+ let escapeHandler: ((e: KeyboardEvent) => void) | null = null;
16
+ let dropdownSourceLayer: LayerInfo | 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) === config.getSelectedElementId()) return;
28
+
29
+ layerPreviewOverlay = config.createPreviewOverlay(layer.element);
30
+ };
31
+
32
+ const selectLayer = (layer: LayerInfo) => {
33
+ clearLayerPreview();
34
+ closeDropdown();
35
+ if (escapeHandler) {
36
+ document.removeEventListener("keydown", escapeHandler, true);
37
+ escapeHandler = null;
38
+ }
39
+ dropdownSourceLayer = null;
40
+
41
+ const firstOverlay = config.selectElement(layer.element);
42
+ attachToOverlay(firstOverlay, layer.element);
43
+ };
44
+
45
+ const restoreSelection = () => {
46
+ if (escapeHandler) {
47
+ document.removeEventListener("keydown", escapeHandler, true);
48
+ escapeHandler = null;
49
+ }
50
+ if (dropdownSourceLayer) {
51
+ selectLayer(dropdownSourceLayer);
52
+ dropdownSourceLayer = null;
53
+ }
54
+ };
55
+
56
+ const handleLabelClick = (e: MouseEvent, label: HTMLDivElement, element: Element, layers: LayerInfo[], currentId: string | null) => {
57
+ e.stopPropagation();
58
+ e.preventDefault();
59
+ if (isDropdownOpen()) {
60
+ closeDropdown();
61
+ restoreSelection();
62
+ } else {
63
+ dropdownSourceLayer = {
64
+ element,
65
+ tagName: element.tagName.toLowerCase(),
66
+ selectorId: currentId,
67
+ };
68
+ config.onDeselect();
69
+
70
+ escapeHandler = (ev: KeyboardEvent) => {
71
+ if (ev.key === "Escape") {
72
+ ev.stopPropagation();
73
+ closeDropdown();
74
+ restoreSelection();
75
+ }
76
+ };
77
+ document.addEventListener("keydown", escapeHandler, true);
78
+
79
+ showDropdown(label, layers, element, { onSelect: selectLayer, onHover: showLayerPreview, onHoverEnd: clearLayerPreview });
80
+ }
81
+ };
82
+
83
+ const attachToOverlay = (
84
+ overlay: HTMLDivElement | undefined,
85
+ element: Element
86
+ ) => {
87
+ if (!overlay) return;
88
+
89
+ const label = overlay.querySelector("div") as HTMLDivElement | null;
90
+ if (!label) return;
91
+
92
+ const layers = buildLayerChain(element);
93
+ if (layers.length <= 1) return;
94
+
95
+ const currentId = getElementSelectorId(element);
96
+ enhanceLabelWithChevron(label);
97
+
98
+ label.addEventListener("click", (e: MouseEvent) => {
99
+ handleLabelClick(e, label, element, layers, currentId);
100
+ });
101
+ };
102
+
103
+ const cleanup = () => {
104
+ clearLayerPreview();
105
+ closeDropdown();
106
+ };
107
+
108
+ return { attachToOverlay, cleanup };
109
+ }