@sudodevstudio/astro-ai 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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/agent/agent-fallback.d.ts +50 -0
  4. package/dist/agent/agent-fallback.d.ts.map +1 -0
  5. package/dist/agent/agent-fallback.js +31 -0
  6. package/dist/agent/agent-fallback.js.map +1 -0
  7. package/dist/agent/cli-agent-fallback.d.ts +79 -0
  8. package/dist/agent/cli-agent-fallback.d.ts.map +1 -0
  9. package/dist/agent/cli-agent-fallback.js +1056 -0
  10. package/dist/agent/cli-agent-fallback.js.map +1 -0
  11. package/dist/integration/index.d.ts +28 -0
  12. package/dist/integration/index.d.ts.map +1 -0
  13. package/dist/integration/index.js +389 -0
  14. package/dist/integration/index.js.map +1 -0
  15. package/dist/resolver/astro-resolver.d.ts +28 -0
  16. package/dist/resolver/astro-resolver.d.ts.map +1 -0
  17. package/dist/resolver/astro-resolver.js +835 -0
  18. package/dist/resolver/astro-resolver.js.map +1 -0
  19. package/dist/resolver/types.d.ts +39 -0
  20. package/dist/resolver/types.d.ts.map +1 -0
  21. package/dist/resolver/types.js +2 -0
  22. package/dist/resolver/types.js.map +1 -0
  23. package/dist/shared/protocol.d.ts +150 -0
  24. package/dist/shared/protocol.d.ts.map +1 -0
  25. package/dist/shared/protocol.js +28 -0
  26. package/dist/shared/protocol.js.map +1 -0
  27. package/dist/shared/selection-context.d.ts +75 -0
  28. package/dist/shared/selection-context.d.ts.map +1 -0
  29. package/dist/shared/selection-context.js +2 -0
  30. package/dist/shared/selection-context.js.map +1 -0
  31. package/dist/shared/visual-components.d.ts +15 -0
  32. package/dist/shared/visual-components.d.ts.map +1 -0
  33. package/dist/shared/visual-components.js +2 -0
  34. package/dist/shared/visual-components.js.map +1 -0
  35. package/dist/toolbar/action-model.d.ts +20 -0
  36. package/dist/toolbar/action-model.d.ts.map +1 -0
  37. package/dist/toolbar/action-model.js +49 -0
  38. package/dist/toolbar/action-model.js.map +1 -0
  39. package/dist/toolbar/app.d.ts +3 -0
  40. package/dist/toolbar/app.d.ts.map +1 -0
  41. package/dist/toolbar/app.js +274 -0
  42. package/dist/toolbar/app.js.map +1 -0
  43. package/dist/toolbar/chat-drawer.d.ts +111 -0
  44. package/dist/toolbar/chat-drawer.d.ts.map +1 -0
  45. package/dist/toolbar/chat-drawer.js +1575 -0
  46. package/dist/toolbar/chat-drawer.js.map +1 -0
  47. package/dist/toolbar/chat-windows.d.ts +62 -0
  48. package/dist/toolbar/chat-windows.d.ts.map +1 -0
  49. package/dist/toolbar/chat-windows.js +287 -0
  50. package/dist/toolbar/chat-windows.js.map +1 -0
  51. package/dist/toolbar/contextual-actions.d.ts +24 -0
  52. package/dist/toolbar/contextual-actions.d.ts.map +1 -0
  53. package/dist/toolbar/contextual-actions.js +371 -0
  54. package/dist/toolbar/contextual-actions.js.map +1 -0
  55. package/dist/toolbar/diagnostic-actions.d.ts +22 -0
  56. package/dist/toolbar/diagnostic-actions.d.ts.map +1 -0
  57. package/dist/toolbar/diagnostic-actions.js +127 -0
  58. package/dist/toolbar/diagnostic-actions.js.map +1 -0
  59. package/dist/toolbar/layers.d.ts +26 -0
  60. package/dist/toolbar/layers.d.ts.map +1 -0
  61. package/dist/toolbar/layers.js +31 -0
  62. package/dist/toolbar/layers.js.map +1 -0
  63. package/dist/toolbar/overlay.d.ts +40 -0
  64. package/dist/toolbar/overlay.d.ts.map +1 -0
  65. package/dist/toolbar/overlay.js +619 -0
  66. package/dist/toolbar/overlay.js.map +1 -0
  67. package/dist/visual/capability-resolver.d.ts +11 -0
  68. package/dist/visual/capability-resolver.d.ts.map +1 -0
  69. package/dist/visual/capability-resolver.js +69 -0
  70. package/dist/visual/capability-resolver.js.map +1 -0
  71. package/dist/visual/command-engine.d.ts +10 -0
  72. package/dist/visual/command-engine.d.ts.map +1 -0
  73. package/dist/visual/command-engine.js +199 -0
  74. package/dist/visual/command-engine.js.map +1 -0
  75. package/dist/visual/commands.d.ts +36 -0
  76. package/dist/visual/commands.d.ts.map +1 -0
  77. package/dist/visual/commands.js +2 -0
  78. package/dist/visual/commands.js.map +1 -0
  79. package/dist/visual/patch-transactions.d.ts +57 -0
  80. package/dist/visual/patch-transactions.d.ts.map +1 -0
  81. package/dist/visual/patch-transactions.js +259 -0
  82. package/dist/visual/patch-transactions.js.map +1 -0
  83. package/dist/vite/build-ai-plugin.d.ts +11 -0
  84. package/dist/vite/build-ai-plugin.d.ts.map +1 -0
  85. package/dist/vite/build-ai-plugin.js +45 -0
  86. package/dist/vite/build-ai-plugin.js.map +1 -0
  87. package/package.json +85 -0
  88. package/src/agent/agent-fallback.ts +95 -0
  89. package/src/agent/cli-agent-fallback.ts +1382 -0
  90. package/src/integration/index.ts +475 -0
  91. package/src/resolver/astro-resolver.ts +1024 -0
  92. package/src/resolver/types.ts +47 -0
  93. package/src/shared/protocol.ts +181 -0
  94. package/src/shared/selection-context.ts +91 -0
  95. package/src/shared/visual-components.ts +16 -0
  96. package/src/toolbar/action-model.ts +67 -0
  97. package/src/toolbar/app.ts +294 -0
  98. package/src/toolbar/chat-drawer.ts +1742 -0
  99. package/src/toolbar/chat-windows.ts +331 -0
  100. package/src/toolbar/contextual-actions.ts +401 -0
  101. package/src/toolbar/diagnostic-actions.ts +151 -0
  102. package/src/toolbar/layers.ts +32 -0
  103. package/src/toolbar/overlay.ts +638 -0
  104. package/src/visual/capability-resolver.ts +83 -0
  105. package/src/visual/command-engine.ts +250 -0
  106. package/src/visual/commands.ts +37 -0
  107. package/src/visual/patch-transactions.ts +270 -0
  108. package/src/vite/build-ai-plugin.ts +46 -0
@@ -0,0 +1,40 @@
1
+ import type { SelectionContext, SourceInsertionZone } from '../shared/selection-context.js';
2
+ import type { DeterministicVisualCommand } from '../visual/commands.js';
3
+ type AnchorPoint = {
4
+ x: number;
5
+ y: number;
6
+ };
7
+ export type SelectionOverlayCallbacks = {
8
+ onInspect(nodeId: string): void;
9
+ onActiveChange(active: boolean): void;
10
+ onCommand(command: DeterministicVisualCommand): void;
11
+ onAskAI(contexts: SelectionContext[], elements: HTMLElement[]): void;
12
+ onClear(): void;
13
+ onSelectionChange(contexts: SelectionContext[], elements: HTMLElement[]): void;
14
+ onSelectionAnchorChange(rect?: DOMRect): void;
15
+ };
16
+ export declare class SelectionOverlay {
17
+ #private;
18
+ constructor(callbacks: SelectionOverlayCallbacks);
19
+ get active(): boolean;
20
+ enable(): void;
21
+ disable(): void;
22
+ start(): void;
23
+ stop(): void;
24
+ setInsertionZones(zones: SourceInsertionZone[]): void;
25
+ setSelection(context: SelectionContext): void;
26
+ clearSelection(): void;
27
+ rejectSelection(nodeId: string): void;
28
+ destroy(): void;
29
+ /**
30
+ * Delivers a real click to the page. Selection mode intercepts clicks to
31
+ * pick elements, so interception is lifted for the dispatch and restored
32
+ * afterwards — without this there is no way to open a menu or switch a tab
33
+ * to reach the state you actually want to edit.
34
+ */
35
+ clickThrough(target: HTMLElement): boolean;
36
+ }
37
+ export declare function normalizedRect(start: AnchorPoint, end: AnchorPoint): DOMRect;
38
+ export declare function rectanglesIntersect(first: Pick<DOMRect, 'left' | 'right' | 'top' | 'bottom'>, second: Pick<DOMRect, 'left' | 'right' | 'top' | 'bottom'>): boolean;
39
+ export {};
40
+ //# sourceMappingURL=overlay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../src/toolbar/overlay.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC5F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AASxE,KAAK,WAAW,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAW5C,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACtC,SAAS,CAAC,OAAO,EAAE,0BAA0B,GAAG,IAAI,CAAC;IACrD,OAAO,CAAC,QAAQ,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IACrE,OAAO,IAAI,IAAI,CAAC;IAChB,iBAAiB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI,CAAC;IAC/E,uBAAuB,CAAC,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/C,CAAC;AAEF,qBAAa,gBAAgB;;gBAuBf,SAAS,EAAE,yBAAyB;IAahD,IAAI,MAAM,IAAI,OAAO,CAEpB;IAcD,MAAM,IAAI,IAAI;IAad,OAAO,IAAI,IAAI;IAsBf,KAAK,IAAI,IAAI;IAab,IAAI,IAAI,IAAI;IAcZ,iBAAiB,CAAC,KAAK,EAAE,mBAAmB,EAAE,GAAG,IAAI;IA6BrD,YAAY,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI;IAmC7C,cAAc,IAAI,IAAI;IAQtB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAMrC,OAAO,IAAI,IAAI;IA2Ef;;;;;OAKG;IACH,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO;CAgP3C;AA+CD,wBAAgB,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAI5E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC,GAAG,OAAO,CAElK"}
@@ -0,0 +1,619 @@
1
+ import { middleTruncatePath } from './action-model.js';
2
+ import { ContextualActionBar } from './contextual-actions.js';
3
+ import { EDITOR_LAYERS } from './layers.js';
4
+ const NODE_SELECTOR = '[data-astro-ai-id]';
5
+ const MARQUEE_THRESHOLD = 6;
6
+ const MAX_MARQUEE_SELECTION = 50;
7
+ export class SelectionOverlay {
8
+ #hover;
9
+ #hoverLabel;
10
+ #hoverTooltip;
11
+ #marquee;
12
+ #actions;
13
+ #callbacks;
14
+ #selected = new Map();
15
+ #pending = new Map();
16
+ #insertionControls = new Map();
17
+ #active = false;
18
+ #enabled = false;
19
+ #primaryNodeId;
20
+ #tooltipTimer;
21
+ #mutationTimer;
22
+ #observer;
23
+ #hoveredElement;
24
+ #marqueeOrigin;
25
+ #marqueeActive = false;
26
+ #suppressClick = false;
27
+ #viewportFrame;
28
+ #tabIndexes = new Map();
29
+ constructor(callbacks) {
30
+ this.#callbacks = callbacks;
31
+ [this.#hover, this.#hoverLabel, this.#hoverTooltip] = createHighlight(false);
32
+ this.#marquee = createMarquee();
33
+ this.#actions = new ContextualActionBar({
34
+ onCommand: callbacks.onCommand,
35
+ onAskAI: (contexts) => callbacks.onAskAI(contexts, this.#elementsFor(contexts)),
36
+ onClear: () => this.clearSelection(),
37
+ onClickThrough: (target) => this.clickThrough(target),
38
+ });
39
+ document.documentElement.append(this.#hover, this.#marquee);
40
+ }
41
+ get active() {
42
+ return this.#active;
43
+ }
44
+ /**
45
+ * Resolves the exact elements behind a selection. A source node rendered in a
46
+ * loop yields many DOM nodes sharing one id, so only the overlay knows which
47
+ * repetition was actually picked.
48
+ */
49
+ #elementsFor(contexts) {
50
+ return contexts.flatMap(({ selectedNode }) => {
51
+ const element = this.#selected.get(selectedNode.nodeId)?.element;
52
+ return element === undefined ? [] : [element];
53
+ });
54
+ }
55
+ enable() {
56
+ if (this.#enabled)
57
+ return;
58
+ this.#enabled = true;
59
+ document.addEventListener('contextmenu', this.#onContextMenu, true);
60
+ document.addEventListener('scroll', this.#onViewportChange, true);
61
+ window.addEventListener('resize', this.#onViewportChange);
62
+ this.#observer = new MutationObserver(this.#onMutations);
63
+ this.#observer.observe(document.documentElement, { childList: true, subtree: true });
64
+ this.#makeKeyboardReachable();
65
+ document.addEventListener('focusin', this.#onFocusIn, true);
66
+ document.addEventListener('keydown', this.#onSelectionKeyDown, true);
67
+ }
68
+ disable() {
69
+ if (!this.#enabled)
70
+ return;
71
+ this.stop();
72
+ this.clearSelection();
73
+ this.#enabled = false;
74
+ document.removeEventListener('contextmenu', this.#onContextMenu, true);
75
+ document.removeEventListener('scroll', this.#onViewportChange, true);
76
+ window.removeEventListener('resize', this.#onViewportChange);
77
+ this.#observer?.disconnect();
78
+ this.#observer = undefined;
79
+ document.removeEventListener('focusin', this.#onFocusIn, true);
80
+ document.removeEventListener('keydown', this.#onSelectionKeyDown, true);
81
+ for (const [element, previous] of this.#tabIndexes) {
82
+ if (previous === null)
83
+ element.removeAttribute('tabindex');
84
+ else
85
+ element.setAttribute('tabindex', previous);
86
+ }
87
+ this.#tabIndexes.clear();
88
+ if (this.#viewportFrame !== undefined)
89
+ window.cancelAnimationFrame(this.#viewportFrame);
90
+ this.#viewportFrame = undefined;
91
+ if (this.#mutationTimer !== undefined)
92
+ window.clearTimeout(this.#mutationTimer);
93
+ }
94
+ start() {
95
+ if (!this.#enabled)
96
+ this.enable();
97
+ if (this.#active)
98
+ return;
99
+ this.#active = true;
100
+ this.#callbacks.onActiveChange(true);
101
+ document.addEventListener('pointerdown', this.#onPointerDown, true);
102
+ document.addEventListener('pointermove', this.#onPointerMove, true);
103
+ document.addEventListener('pointerup', this.#onPointerUp, true);
104
+ document.addEventListener('pointerleave', this.#onPointerLeave, true);
105
+ document.addEventListener('click', this.#onClick, true);
106
+ this.#setInsertionControlsVisible(true);
107
+ }
108
+ stop() {
109
+ if (!this.#active)
110
+ return;
111
+ this.#active = false;
112
+ this.#callbacks.onActiveChange(false);
113
+ this.#hideHover();
114
+ this.#resetMarquee();
115
+ document.removeEventListener('pointerdown', this.#onPointerDown, true);
116
+ document.removeEventListener('pointermove', this.#onPointerMove, true);
117
+ document.removeEventListener('pointerup', this.#onPointerUp, true);
118
+ document.removeEventListener('pointerleave', this.#onPointerLeave, true);
119
+ document.removeEventListener('click', this.#onClick, true);
120
+ this.#setInsertionControlsVisible(false);
121
+ }
122
+ setInsertionZones(zones) {
123
+ for (const { button } of this.#insertionControls.values())
124
+ button.remove();
125
+ this.#insertionControls.clear();
126
+ for (const zone of zones) {
127
+ const button = document.createElement('button');
128
+ button.type = 'button';
129
+ button.dataset.astroAiUi = 'insertion-zone';
130
+ button.textContent = zone.slot === undefined ? '+ Add to page' : `+ Add to ${zone.slot}`;
131
+ button.title = `Insert a native Astro element into ${zone.file}`;
132
+ Object.assign(button.style, {
133
+ position: 'fixed',
134
+ zIndex: String(EDITOR_LAYERS.insertionControl),
135
+ border: '1px solid #8b5cf6',
136
+ borderRadius: '999px',
137
+ background: '#181b23',
138
+ color: '#ede9fe',
139
+ padding: '5px 9px',
140
+ font: '600 11px/1.2 ui-sans-serif, system-ui, sans-serif',
141
+ cursor: 'pointer',
142
+ boxShadow: '0 4px 14px rgb(0 0 0 / .3)',
143
+ display: this.#active ? 'block' : 'none',
144
+ });
145
+ button.addEventListener('click', () => this.#insertAtZone(zone));
146
+ document.documentElement.append(button);
147
+ this.#insertionControls.set(zone.id, { zone, button });
148
+ }
149
+ this.#positionInsertionControls();
150
+ }
151
+ setSelection(context) {
152
+ const nodeId = context.selectedNode.nodeId;
153
+ const pending = this.#pending.get(nodeId);
154
+ this.#pending.delete(nodeId);
155
+ let existing = this.#selected.get(nodeId);
156
+ if (pending === undefined && existing === undefined)
157
+ return;
158
+ if (pending?.mode === 'replace') {
159
+ this.#clearItems();
160
+ existing = undefined;
161
+ }
162
+ const target = pending?.element ?? existing?.element ?? findNodeById(nodeId);
163
+ if (target === undefined || !target.isConnected) {
164
+ this.#removeItem(nodeId);
165
+ this.#syncSelection();
166
+ return;
167
+ }
168
+ if (existing === undefined) {
169
+ const [highlight, label] = createHighlight(true);
170
+ document.documentElement.append(highlight);
171
+ existing = { element: target, context, highlight, label };
172
+ this.#selected.set(nodeId, existing);
173
+ }
174
+ else {
175
+ existing.element = target;
176
+ existing.context = context;
177
+ }
178
+ this.#primaryNodeId = nodeId;
179
+ this.#updateItem(existing);
180
+ if (pending?.point !== undefined && this.#selected.size === 1) {
181
+ this.#actions.show(context, target, pending.point);
182
+ this.#syncSelection(false);
183
+ }
184
+ else {
185
+ this.#syncSelection();
186
+ }
187
+ }
188
+ clearSelection() {
189
+ const hadSelection = this.#selected.size > 0 || this.#pending.size > 0;
190
+ this.#clearItems();
191
+ this.#pending.clear();
192
+ this.#syncSelection();
193
+ if (hadSelection)
194
+ this.#callbacks.onClear();
195
+ }
196
+ rejectSelection(nodeId) {
197
+ this.#pending.delete(nodeId);
198
+ if (this.#selected.has(nodeId))
199
+ this.#removeItem(nodeId);
200
+ this.#syncSelection();
201
+ }
202
+ destroy() {
203
+ this.disable();
204
+ this.#actions.destroy();
205
+ this.#hover.remove();
206
+ this.#marquee.remove();
207
+ for (const { button } of this.#insertionControls.values())
208
+ button.remove();
209
+ this.#insertionControls.clear();
210
+ }
211
+ #onPointerDown = (event) => {
212
+ if (event.button !== 0 || isEditorEventTarget(event.target))
213
+ return;
214
+ this.#marqueeOrigin = { x: event.clientX, y: event.clientY, additive: event.shiftKey };
215
+ };
216
+ #onPointerMove = (event) => {
217
+ const origin = this.#marqueeOrigin;
218
+ if (origin !== undefined && (event.buttons & 1) === 1) {
219
+ const distance = Math.hypot(event.clientX - origin.x, event.clientY - origin.y);
220
+ if (this.#marqueeActive || distance >= MARQUEE_THRESHOLD) {
221
+ this.#marqueeActive = true;
222
+ event.preventDefault();
223
+ event.stopImmediatePropagation();
224
+ this.#hideHover();
225
+ positionMarquee(this.#marquee, origin, { x: event.clientX, y: event.clientY });
226
+ this.#marquee.style.display = 'block';
227
+ return;
228
+ }
229
+ }
230
+ const source = findSourceElement(event.target);
231
+ if (source === undefined) {
232
+ this.#hideHover();
233
+ return;
234
+ }
235
+ positionElement(this.#hover, source);
236
+ this.#hover.style.display = 'block';
237
+ if (this.#hoveredElement === source)
238
+ return;
239
+ this.#hoveredElement = source;
240
+ this.#hoverLabel.textContent = source.dataset.astroAiName ?? source.tagName.toLowerCase();
241
+ this.#hoverTooltip.style.display = 'none';
242
+ if (this.#tooltipTimer !== undefined)
243
+ window.clearTimeout(this.#tooltipTimer);
244
+ const hint = source.dataset.astroAiSource;
245
+ if (hint !== undefined) {
246
+ this.#tooltipTimer = window.setTimeout(() => {
247
+ this.#hoverTooltip.textContent = hint;
248
+ this.#hoverTooltip.style.display = 'block';
249
+ }, 500);
250
+ }
251
+ };
252
+ #onPointerUp = (event) => {
253
+ const origin = this.#marqueeOrigin;
254
+ if (origin === undefined)
255
+ return;
256
+ if (!this.#marqueeActive) {
257
+ this.#marqueeOrigin = undefined;
258
+ return;
259
+ }
260
+ event.preventDefault();
261
+ event.stopImmediatePropagation();
262
+ const candidates = marqueeCandidates(normalizedRect(origin, { x: event.clientX, y: event.clientY }));
263
+ if (!origin.additive)
264
+ this.#clearItems();
265
+ for (const target of candidates) {
266
+ const nodeId = target.dataset.astroAiId;
267
+ if (nodeId === undefined || this.#selected.has(nodeId) || this.#pending.has(nodeId))
268
+ continue;
269
+ this.#inspect(target, nodeId, 'add');
270
+ }
271
+ if (candidates.length === 0)
272
+ this.#syncSelection();
273
+ this.#suppressClick = true;
274
+ window.setTimeout(() => { this.#suppressClick = false; }, 0);
275
+ this.#resetMarquee();
276
+ };
277
+ #onPointerLeave = () => {
278
+ if (!this.#marqueeActive)
279
+ this.#hideHover();
280
+ };
281
+ /**
282
+ * Delivers a real click to the page. Selection mode intercepts clicks to
283
+ * pick elements, so interception is lifted for the dispatch and restored
284
+ * afterwards — without this there is no way to open a menu or switch a tab
285
+ * to reach the state you actually want to edit.
286
+ */
287
+ clickThrough(target) {
288
+ if (!target.isConnected)
289
+ return false;
290
+ const wasActive = this.#active;
291
+ if (wasActive)
292
+ this.stop();
293
+ try {
294
+ target.click();
295
+ }
296
+ finally {
297
+ // Restore on the next task so the page's own handlers and any resulting
298
+ // re-render settle before clicks are captured again.
299
+ if (wasActive) {
300
+ window.setTimeout(() => {
301
+ if (this.#enabled)
302
+ this.start();
303
+ }, 0);
304
+ }
305
+ }
306
+ return true;
307
+ }
308
+ #onClick = (event) => {
309
+ if (this.#suppressClick) {
310
+ event.preventDefault();
311
+ event.stopImmediatePropagation();
312
+ return;
313
+ }
314
+ const target = findSourceElement(event.target);
315
+ const nodeId = target?.dataset.astroAiId;
316
+ if (target === undefined || nodeId === undefined)
317
+ return;
318
+ event.preventDefault();
319
+ event.stopImmediatePropagation();
320
+ if (event.shiftKey && this.#selected.has(nodeId)) {
321
+ this.#removeItem(nodeId);
322
+ this.#syncSelection();
323
+ return;
324
+ }
325
+ this.#inspect(target, nodeId, event.shiftKey ? 'add' : 'replace');
326
+ };
327
+ #onContextMenu = (event) => {
328
+ if (!this.#enabled || event.shiftKey)
329
+ return;
330
+ const target = findSourceElement(event.target);
331
+ const nodeId = target?.dataset.astroAiId;
332
+ if (target === undefined || nodeId === undefined)
333
+ return;
334
+ event.preventDefault();
335
+ event.stopImmediatePropagation();
336
+ this.#inspect(target, nodeId, 'replace', { x: event.clientX, y: event.clientY });
337
+ };
338
+ #onViewportChange = () => {
339
+ if (this.#viewportFrame !== undefined)
340
+ return;
341
+ this.#viewportFrame = window.requestAnimationFrame(() => {
342
+ this.#viewportFrame = undefined;
343
+ const rects = [];
344
+ for (const [nodeId, item] of this.#selected) {
345
+ if (!item.element.isConnected)
346
+ this.#removeItem(nodeId);
347
+ else {
348
+ const rect = item.element.getBoundingClientRect();
349
+ rects.push(rect);
350
+ this.#updateItem(item, rect);
351
+ }
352
+ }
353
+ this.#actions.reposition();
354
+ this.#positionInsertionControls();
355
+ this.#emitAnchor(rects);
356
+ });
357
+ };
358
+ #onMutations = (records) => {
359
+ if (records.every(({ target }) => isEditorUiTarget(target)))
360
+ return;
361
+ this.#makeKeyboardReachable();
362
+ if (this.#selected.size === 0 || this.#mutationTimer !== undefined)
363
+ return;
364
+ this.#mutationTimer = window.setTimeout(() => {
365
+ this.#mutationTimer = undefined;
366
+ for (const [nodeId, item] of this.#selected) {
367
+ // A node id identifies a source location, not a DOM node: one rendered
368
+ // in a loop repeats its id on every instance. Re-resolving by id would
369
+ // silently rebind the selection to the first repetition, so the element
370
+ // that is still on the page is kept as-is.
371
+ const relocated = item.element.isConnected
372
+ ? item.element
373
+ : findNodeById(nodeId);
374
+ if (relocated === undefined) {
375
+ this.#removeItem(nodeId);
376
+ continue;
377
+ }
378
+ item.element = relocated;
379
+ this.#updateItem(item);
380
+ this.#pending.set(nodeId, { element: relocated, mode: 'refresh' });
381
+ this.#callbacks.onInspect(nodeId);
382
+ }
383
+ this.#syncSelection();
384
+ }, 60);
385
+ };
386
+ #inspect(element, nodeId, mode, point) {
387
+ if (mode === 'replace')
388
+ this.#pending.clear();
389
+ this.#pending.set(nodeId, { element, mode, ...(point === undefined ? {} : { point }) });
390
+ this.#callbacks.onInspect(nodeId);
391
+ }
392
+ #syncSelection(renderActions = true) {
393
+ const items = [...this.#selected.values()];
394
+ if (items.length === 0) {
395
+ this.#primaryNodeId = undefined;
396
+ this.#actions.hide();
397
+ this.#callbacks.onSelectionChange([], []);
398
+ this.#callbacks.onSelectionAnchorChange();
399
+ return;
400
+ }
401
+ const primary = (this.#primaryNodeId === undefined ? undefined : this.#selected.get(this.#primaryNodeId)) ?? items.at(-1);
402
+ if (primary === undefined)
403
+ return;
404
+ this.#primaryNodeId = primary.context.selectedNode.nodeId;
405
+ if (renderActions) {
406
+ if (items.length === 1)
407
+ this.#actions.show(primary.context, primary.element);
408
+ else
409
+ this.#actions.showMultiple(items.map(({ context }) => context), primary.element);
410
+ }
411
+ this.#callbacks.onSelectionChange(items.map(({ context }) => context), items.map(({ element }) => element));
412
+ this.#emitAnchor();
413
+ }
414
+ #emitAnchor(measured) {
415
+ const rects = measured ?? [...this.#selected.values()].filter(({ element }) => element.isConnected).map(({ element }) => element.getBoundingClientRect());
416
+ if (rects.length === 0) {
417
+ this.#callbacks.onSelectionAnchorChange();
418
+ return;
419
+ }
420
+ const left = Math.min(...rects.map(({ left }) => left));
421
+ const right = Math.max(...rects.map(({ right }) => right));
422
+ const top = Math.min(...rects.map(({ top }) => top));
423
+ const bottom = Math.max(...rects.map(({ bottom }) => bottom));
424
+ this.#callbacks.onSelectionAnchorChange(new DOMRect(left, top, right - left, bottom - top));
425
+ }
426
+ #updateItem(item, rect) {
427
+ const source = item.context.selectedNode.source;
428
+ item.label.textContent = `${middleTruncatePath(source.file, 38)}:${source.start.line}`;
429
+ item.label.title = source.file;
430
+ item.highlight.style.display = 'block';
431
+ positionElement(item.highlight, item.element, rect);
432
+ }
433
+ #makeKeyboardReachable() {
434
+ for (const element of document.querySelectorAll(NODE_SELECTOR)) {
435
+ if (this.#tabIndexes.has(element))
436
+ continue;
437
+ this.#tabIndexes.set(element, element.getAttribute('tabindex'));
438
+ if (!element.hasAttribute('tabindex'))
439
+ element.tabIndex = 0;
440
+ }
441
+ }
442
+ #onFocusIn = (event) => {
443
+ if (!this.#active)
444
+ return;
445
+ const source = findSourceElement(event.target);
446
+ if (source === undefined)
447
+ return;
448
+ positionElement(this.#hover, source);
449
+ this.#hoverLabel.textContent = source.dataset.astroAiName ?? source.tagName.toLowerCase();
450
+ this.#hover.style.display = 'block';
451
+ };
452
+ #onSelectionKeyDown = (event) => {
453
+ if (!this.#active || (event.key !== 'Enter' && event.key !== ' '))
454
+ return;
455
+ const target = findSourceElement(event.target);
456
+ const nodeId = target?.dataset.astroAiId;
457
+ if (target === undefined || nodeId === undefined)
458
+ return;
459
+ event.preventDefault();
460
+ event.stopImmediatePropagation();
461
+ this.#inspect(target, nodeId, event.shiftKey ? 'add' : 'replace');
462
+ };
463
+ #removeItem(nodeId) {
464
+ this.#selected.get(nodeId)?.highlight.remove();
465
+ this.#selected.delete(nodeId);
466
+ this.#pending.delete(nodeId);
467
+ if (this.#primaryNodeId === nodeId)
468
+ this.#primaryNodeId = undefined;
469
+ }
470
+ #clearItems() {
471
+ for (const { highlight } of this.#selected.values())
472
+ highlight.remove();
473
+ this.#selected.clear();
474
+ this.#primaryNodeId = undefined;
475
+ }
476
+ #hideHover() {
477
+ if (this.#tooltipTimer !== undefined)
478
+ window.clearTimeout(this.#tooltipTimer);
479
+ this.#tooltipTimer = undefined;
480
+ this.#hoveredElement = undefined;
481
+ this.#hover.style.display = 'none';
482
+ this.#hoverTooltip.style.display = 'none';
483
+ }
484
+ #resetMarquee() {
485
+ this.#marqueeOrigin = undefined;
486
+ this.#marqueeActive = false;
487
+ this.#marquee.style.display = 'none';
488
+ }
489
+ #setInsertionControlsVisible(visible) {
490
+ for (const { button } of this.#insertionControls.values()) {
491
+ button.style.display = visible ? 'block' : 'none';
492
+ }
493
+ if (visible)
494
+ this.#positionInsertionControls();
495
+ }
496
+ #positionInsertionControls() {
497
+ let rootIndex = 0;
498
+ for (const { zone, button } of this.#insertionControls.values()) {
499
+ if (zone.parentNodeId === undefined) {
500
+ Object.assign(button.style, { left: '16px', top: `${16 + rootIndex * 34}px` });
501
+ rootIndex += 1;
502
+ continue;
503
+ }
504
+ const parent = findNodeById(zone.parentNodeId);
505
+ if (parent === undefined) {
506
+ button.style.display = 'none';
507
+ continue;
508
+ }
509
+ const rect = parent.getBoundingClientRect();
510
+ Object.assign(button.style, {
511
+ display: this.#active ? 'block' : 'none',
512
+ left: `${Math.max(8, rect.left + 8)}px`,
513
+ top: `${Math.max(8, Math.min(window.innerHeight - 32, rect.bottom - 28))}px`,
514
+ });
515
+ }
516
+ }
517
+ #insertAtZone(zone) {
518
+ const tag = window.prompt('Native element tag', 'section')?.trim();
519
+ if (tag === undefined || tag === '')
520
+ return;
521
+ const text = window.prompt('Literal text', '') ?? undefined;
522
+ if (text === undefined)
523
+ return;
524
+ this.#callbacks.onCommand({
525
+ kind: 'insert-literal-element',
526
+ file: zone.file,
527
+ ...(zone.parentNodeId === undefined ? {} : { parentNodeId: zone.parentNodeId }),
528
+ ...(zone.slot === undefined ? {} : { slot: zone.slot }),
529
+ tag,
530
+ text,
531
+ });
532
+ }
533
+ }
534
+ function createHighlight(selected) {
535
+ const highlight = document.createElement('div');
536
+ highlight.dataset.astroAiUi = selected ? 'selection' : 'hover';
537
+ Object.assign(highlight.style, {
538
+ position: 'fixed', zIndex: String(selected ? EDITOR_LAYERS.selectionOutline : EDITOR_LAYERS.hoverOutline), pointerEvents: 'none',
539
+ border: `2px ${selected ? 'solid' : 'dashed'} ${selected ? '#8b5cf6' : '#a78bfa'}`,
540
+ borderRadius: '4px', background: selected ? 'rgb(139 92 246 / 0.08)' : 'rgb(167 139 250 / 0.04)',
541
+ boxSizing: 'border-box', display: 'none',
542
+ });
543
+ const label = document.createElement('span');
544
+ Object.assign(label.style, {
545
+ position: 'absolute', bottom: '100%', left: '-2px', maxWidth: 'min(440px, 80vw)', overflow: 'hidden',
546
+ padding: '3px 7px', borderRadius: '4px 4px 0 0', background: selected ? '#6d28d9' : '#7c3aed',
547
+ color: 'white', font: '11px/1.4 ui-monospace, SFMono-Regular, monospace', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
548
+ });
549
+ const tooltip = document.createElement('span');
550
+ Object.assign(tooltip.style, {
551
+ position: 'absolute', left: '0', top: '6px', transform: 'translateY(100%)', background: '#111318',
552
+ border: '1px solid #454b59', borderRadius: '4px', color: '#e2e8f0', display: 'none',
553
+ font: '11px/1.4 ui-monospace, SFMono-Regular, monospace', padding: '4px 7px', whiteSpace: 'nowrap',
554
+ });
555
+ highlight.append(label, tooltip);
556
+ return [highlight, label, tooltip];
557
+ }
558
+ function createMarquee() {
559
+ const marquee = document.createElement('div');
560
+ marquee.dataset.astroAiUi = 'marquee';
561
+ Object.assign(marquee.style, {
562
+ position: 'fixed', zIndex: String(EDITOR_LAYERS.marquee), pointerEvents: 'none', border: '1px solid #c4b5fd',
563
+ borderRadius: '3px', background: 'rgb(139 92 246 / 0.16)', boxShadow: '0 0 0 1px rgb(76 29 149 / .35) inset', display: 'none',
564
+ });
565
+ return marquee;
566
+ }
567
+ function positionElement(overlay, target, measured) {
568
+ const rect = measured ?? target.getBoundingClientRect();
569
+ Object.assign(overlay.style, { left: `${rect.left}px`, top: `${rect.top}px`, width: `${rect.width}px`, height: `${rect.height}px` });
570
+ }
571
+ function positionMarquee(element, start, end) {
572
+ const rect = normalizedRect(start, end);
573
+ Object.assign(element.style, { left: `${rect.left}px`, top: `${rect.top}px`, width: `${rect.width}px`, height: `${rect.height}px` });
574
+ }
575
+ export function normalizedRect(start, end) {
576
+ const left = Math.min(start.x, end.x);
577
+ const top = Math.min(start.y, end.y);
578
+ return new DOMRect(left, top, Math.abs(end.x - start.x), Math.abs(end.y - start.y));
579
+ }
580
+ export function rectanglesIntersect(first, second) {
581
+ return first.left <= second.right && first.right >= second.left && first.top <= second.bottom && first.bottom >= second.top;
582
+ }
583
+ function marqueeCandidates(selectionRect) {
584
+ const intersecting = [...document.querySelectorAll(NODE_SELECTOR)].filter((element) => {
585
+ const rect = element.getBoundingClientRect();
586
+ return rect.width > 0 && rect.height > 0 && rectanglesIntersect(selectionRect, rect);
587
+ });
588
+ const leafMost = intersecting.filter((element) => !intersecting.some((candidate) => candidate !== element && element.contains(candidate)));
589
+ const unique = new Map();
590
+ for (const element of leafMost) {
591
+ const nodeId = element.dataset.astroAiId;
592
+ if (nodeId !== undefined && !unique.has(nodeId))
593
+ unique.set(nodeId, element);
594
+ if (unique.size >= MAX_MARQUEE_SELECTION)
595
+ break;
596
+ }
597
+ return [...unique.values()];
598
+ }
599
+ function findSourceElement(target) {
600
+ if (!(target instanceof Element))
601
+ return undefined;
602
+ if (target.closest('astro-dev-toolbar, [data-astro-ai-ui]') !== null)
603
+ return undefined;
604
+ return target.closest(NODE_SELECTOR) ?? undefined;
605
+ }
606
+ function findNodeById(nodeId) {
607
+ return document.querySelector(`[data-astro-ai-id="${cssEscape(nodeId)}"]`) ?? undefined;
608
+ }
609
+ function isEditorEventTarget(target) {
610
+ return target instanceof Element && target.closest('astro-dev-toolbar, [data-astro-ai-ui]') !== null;
611
+ }
612
+ function isEditorUiTarget(target) {
613
+ const element = target instanceof Element ? target : target.parentElement;
614
+ return element !== null && element.closest('[data-astro-ai-ui]') !== null;
615
+ }
616
+ function cssEscape(value) {
617
+ return globalThis.CSS?.escape(value) ?? value.replaceAll('"', '\\"');
618
+ }
619
+ //# sourceMappingURL=overlay.js.map