@@ -9,7 +9,20 @@ export function buildActionOverlayScript(): string {
99 if (window.__pqaOverlay) return;
1010
1111 const ROOT_ID = "pqa-action-overlay-root";
12+ const PANEL_ID = "pqa-overlay-panel";
13+ const PANEL_BODY_ID = "pqa-overlay-panel-body";
1214 const HIGHLIGHT_CLASS = "pqa-overlay-highlight";
15+ const CURSOR_CLASS = "pqa-overlay-cursor";
16+ const MAX_ENTRIES = 3;
17+ const PANEL_WIDTH = 320;
18+ const PANEL_HEIGHT = 280;
19+ const PANEL_MARGIN = 12;
20+
21+ let panelLeft = null;
22+ let panelTop = null;
23+ let dragActive = false;
24+ let dragOffsetX = 0;
25+ let dragOffsetY = 0;
1326
1427 function ensureRoot() {
1528 let root = document.getElementById(ROOT_ID);
@@ -33,27 +46,46 @@ export function buildActionOverlayScript(): string {
3346 const style = document.createElement("style");
3447 style.id = "pqa-overlay-styles";
3548 style.textContent = [
36- "#pqa-action-overlay-root .pqa-overlay-hud {",
37- " position: fixed; top: 12px; left: 50%; transform: translateX(-50%);",
38- " max-width: min(90vw, 640px); padding: 10px 16px;",
39- " background: rgba(15, 23, 42, 0.92); color: #f8fafc;",
40- " font: 600 14px/1.35 system-ui, -apple-system, sans-serif;",
49+ "#pqa-action-overlay-root .pqa-overlay-panel {",
50+ " position: fixed; width: 320px; height: 280px;",
51+ " display: flex; flex-direction: column;",
4152 " border-radius: 8px; box-shadow: 0 4px 24px rgba(0,0,0,0.35);",
42- " text-align: center;",
53+ " overflow: hidden; pointer-events: auto;",
54+ " background: rgba(15, 23, 42, 0.7); color: #f8fafc;",
55+ " font: 500 13px/1.4 system-ui, -apple-system, sans-serif;",
56+ " transition: background 0.15s ease;",
57+ "}",
58+ "#pqa-action-overlay-root .pqa-overlay-panel:hover {",
59+ " background: rgba(15, 23, 42, 1);",
60+ "}",
61+ "#pqa-action-overlay-root .pqa-overlay-panel-handle {",
62+ " height: 24px; flex-shrink: 0; cursor: grab;",
63+ " background: rgba(255,255,255,0.06);",
64+ " border-bottom: 1px solid rgba(148, 163, 184, 0.25);",
65+ "}",
66+ "#pqa-action-overlay-root .pqa-overlay-panel-handle:active { cursor: grabbing; }",
67+ "#pqa-action-overlay-root .pqa-overlay-panel-body {",
68+ " flex: 1; overflow-y: auto; padding: 8px 12px; text-align: left;",
4369 "}",
44- "#pqa-action-overlay-root .pqa-overlay-hud-title {",
45- " display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;",
70+ "#pqa-action-overlay-root .pqa-overlay-entry {",
71+ " padding: 8px 0;",
72+ " border-bottom: 1px solid rgba(148, 163, 184, 0.2);",
4673 "}",
47- "#pqa-action-overlay-root .pqa-overlay-hud-detail {",
48- " display: block; margin-top: 2px;",
49- " font-size: 12px; font-weight: 500; color: #94a3b8;",
50- " white-space: nowrap; overflow: hidden; text-overflow: ellipsis;",
74+ "#pqa-action-overlay-root .pqa-overlay-entry:last-child { border-bottom: none; }",
75+ "#pqa-action-overlay-root .pqa-overlay-entry-intent {",
76+ " display: block; font-size: 14px; font-weight: 600; line-height: 1.35;",
77+ " word-break: break-word; white-space: pre-wrap;",
78+ "}",
79+ "#pqa-action-overlay-root .pqa-overlay-entry-command {",
80+ " display: block; margin-top: 4px; font-size: 12px; font-weight: 500;",
81+ " color: #94a3b8; line-height: 1.45; word-break: break-word;",
5182 "}",
5283 "#pqa-action-overlay-root .pqa-overlay-highlight {",
5384 " position: fixed; border: 3px solid #f59e0b;",
5485 " background: rgba(245, 158, 11, 0.15); border-radius: 4px;",
5586 " box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.25);",
5687 " transition: top 0.75s ease, left 0.75s ease, width 0.75s ease, height 0.75s ease;",
88+ " pointer-events: none;",
5789 "}",
5890 "#pqa-action-overlay-root .pqa-overlay-cursor {",
5991 " position: fixed; width: 32px; height: 32px;",
@@ -66,94 +98,225 @@ export function buildActionOverlayScript(): string {
6698 document.documentElement.appendChild(style);
6799 }
68100
101+ function defaultPanelPosition() {
102+ return {
103+ left: Math.max(PANEL_MARGIN, window.innerWidth - PANEL_WIDTH - PANEL_MARGIN),
104+ top: PANEL_MARGIN,
105+ };
106+ }
107+
108+ function applyPanelPosition(panel) {
109+ const left = panelLeft ?? defaultPanelPosition().left;
110+ const top = panelTop ?? defaultPanelPosition().top;
111+ Object.assign(panel.style, {
112+ left: left + "px",
113+ top: top + "px",
114+ right: "auto",
115+ transform: "none",
116+ });
117+ }
118+
119+ function onDragMove(event) {
120+ if (!dragActive) return;
121+ panelLeft = event.clientX - dragOffsetX;
122+ panelTop = event.clientY - dragOffsetY;
123+ const panel = document.getElementById(PANEL_ID);
124+ if (panel) applyPanelPosition(panel);
125+ }
126+
127+ function onDragEnd() {
128+ dragActive = false;
129+ }
130+
131+ function ensurePanel(root) {
132+ let panel = document.getElementById(PANEL_ID);
133+ if (panel) return panel;
134+
135+ panel = document.createElement("div");
136+ panel.id = PANEL_ID;
137+ panel.className = "pqa-overlay-panel";
138+
139+ const handle = document.createElement("div");
140+ handle.className = "pqa-overlay-panel-handle";
141+ handle.setAttribute("aria-hidden", "true");
142+ handle.addEventListener("mousedown", (event) => {
143+ if (event.button !== 0) return;
144+ dragActive = true;
145+ const rect = panel.getBoundingClientRect();
146+ dragOffsetX = event.clientX - rect.left;
147+ dragOffsetY = event.clientY - rect.top;
148+ panelLeft = rect.left;
149+ panelTop = rect.top;
150+ event.preventDefault();
151+ });
152+
153+ const body = document.createElement("div");
154+ body.id = PANEL_BODY_ID;
155+ body.className = "pqa-overlay-panel-body";
156+
157+ panel.appendChild(handle);
158+ panel.appendChild(body);
159+ root.appendChild(panel);
160+ applyPanelPosition(panel);
161+
162+ window.addEventListener("mousemove", onDragMove);
163+ window.addEventListener("mouseup", onDragEnd);
164+
165+ return panel;
166+ }
167+
168+ function clearAnimation(root) {
169+ root.querySelectorAll("." + HIGHLIGHT_CLASS + ", ." + CURSOR_CLASS).forEach((el) => el.remove());
170+ }
171+
69172 function clearOverlay() {
70173 const root = document.getElementById(ROOT_ID);
71174 if (root) root.replaceChildren();
72175 }
73176
74- function normalizeHudPayload (input) {
177+ function normalizeEntryPayload (input) {
75178 if (typeof input === "string") {
76- return { intent: "", detail: input };
179+ return { command: input, intent: "" };
77180 }
78181 if (input && typeof input === "object") {
182+ const command =
183+ input.command != null && input.command !== ""
184+ ? String(input.command)
185+ : input.detail != null && input.detail !== ""
186+ ? String(input.detail)
187+ : input.label != null && input.label !== ""
188+ ? String(input.label)
189+ : "";
79190 return {
191+ command,
80192 intent: input.intent ? String(input.intent) : "",
81- detail: input.detail ? String(input.detail) : input.label ? String(input.label) : "",
82193 };
83194 }
84- return { intent: "", detail: "" };
195+ return { command: "", intent: "" };
196+ }
197+
198+ function trimEntryStack(body) {
199+ while (body.children.length >= MAX_ENTRIES) {
200+ body.firstElementChild?.remove();
201+ }
202+ }
203+
204+ function scrollPanelToBottom(body) {
205+ body.scrollTop = body.scrollHeight;
85206 }
86207
87- function appendHud(root, payload) {
88- const { intent, detail } = normalizeHudPayload(payload);
89- const hud = document.createElement("div");
90- hud.className = "pqa-overlay-hud";
208+ function appendEntry(root, payload) {
209+ const { command, intent } = normalizeEntryPayload(payload);
210+ if (!command && !intent) return;
211+
212+ const panel = ensurePanel(root);
213+ const body = panel.querySelector("#" + PANEL_BODY_ID);
214+ if (!body) return;
215+
216+ trimEntryStack(body);
217+
218+ const entry = document.createElement("div");
219+ entry.className = "pqa-overlay-entry";
220+
91221 if (intent) {
92- const title = document.createElement("span");
93- title .className = "pqa-overlay-hud-title ";
94- title .textContent = intent;
95- hud .appendChild(title );
222+ const text = document.createElement("span");
223+ text .className = "pqa-overlay-entry-intent ";
224+ text .textContent = intent;
225+ entry .appendChild(text );
96226 }
97- if (detail ) {
98- const sub = document.createElement("span");
99- sub .className = intent ? "pqa-overlay-hud-detail" : "pqa-overlay-hud-title ";
100- sub .textContent = detail ;
101- hud .appendChild(sub );
227+ if (command ) {
228+ const cmd = document.createElement("span");
229+ cmd .className = "pqa-overlay-entry-command ";
230+ cmd .textContent = command ;
231+ entry .appendChild(cmd );
102232 }
103- root.appendChild(hud);
233+
234+ body.appendChild(entry);
235+ scrollPanelToBottom(body);
104236 }
105237
106238 function showHud(payload) {
107239 ensureStyles();
108240 const root = ensureRoot();
109- clearOverlay( );
110- appendHud (root, payload);
241+ clearAnimation(root );
242+ appendEntry (root, payload);
111243 }
112244
113- function showMutation(payload) {
114- ensureStyles();
115- const root = ensureRoot();
116- clearOverlay();
117-
118- appendHud(root, payload);
119- const box = payload && payload.box ? payload.box : null;
120-
121- if (box && typeof box.x === "number" && typeof box.y === "number") {
122- const w = typeof box.width === "number" ? box.width : 0;
123- const h = typeof box.height === "number" ? box.height : 0;
124- const cx = box.x + w / 2;
125- const cy = box.y + h / 2;
126-
127- const highlight = document.createElement("div");
128- highlight.className = HIGHLIGHT_CLASS;
129- Object.assign(highlight.style, {
130- top: box.y + "px",
131- left: box.x + "px",
132- width: Math.max(w, 4) + "px",
133- height: Math.max(h, 4) + "px",
134- });
135- root.appendChild(highlight);
136-
137- const cursor = document.createElement("div");
138- cursor.className = "pqa-overlay-cursor";
139- cursor.innerHTML =
140- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32">' +
141- '<path fill="#f59e0b" stroke="#0f172a" stroke-width="1.2" d="M4 2l14 14h-6l-4 8z"/></svg>';
142- const startX = Math.max(0, cx - 100);
143- const startY = Math.max(0, cy - 80);
144- Object.assign(cursor.style, {
145- top: startY + "px",
146- left: startX + "px",
147- });
148- root.appendChild(cursor);
245+ function scrollBoxIntoView(box) {
246+ const w = typeof box.width === "number" ? box.width : 0;
247+ const h = typeof box.height === "number" ? box.height : 0;
248+ let x = box.x;
249+ let y = box.y;
250+ const margin = 72;
251+ const vw = window.innerWidth;
252+ const vh = window.innerHeight;
253+ const cx = x + w / 2;
254+ const cy = y + h / 2;
255+
256+ let scrollX = 0;
257+ let scrollY = 0;
258+
259+ if (cy < margin) scrollY = cy - margin - (vh - margin) / 2;
260+ else if (cy > vh - margin) scrollY = cy - (vh + margin) / 2;
261+
262+ if (cx < margin) scrollX = cx - margin - (vw - margin) / 2;
263+ else if (cx > vw - margin) scrollX = cx - (vw + margin) / 2;
264+
265+ if (scrollX !== 0 || scrollY !== 0) {
266+ window.scrollBy({ top: scrollY, left: scrollX, behavior: "instant" });
267+ x -= scrollX;
268+ y -= scrollY;
269+ }
149270
271+ return { x, y, width: w, height: h };
272+ }
273+
274+ function showMutationAnimation(root, rawBox) {
275+ if (!rawBox || typeof rawBox.x !== "number" || typeof rawBox.y !== "number") return;
276+
277+ const box = scrollBoxIntoView(rawBox);
278+ const w = box.width;
279+ const h = box.height;
280+ const cx = box.x + w / 2;
281+ const cy = box.y + h / 2;
282+
283+ const highlight = document.createElement("div");
284+ highlight.className = HIGHLIGHT_CLASS;
285+ Object.assign(highlight.style, {
286+ top: box.y + "px",
287+ left: box.x + "px",
288+ width: Math.max(w, 4) + "px",
289+ height: Math.max(h, 4) + "px",
290+ });
291+ root.appendChild(highlight);
292+
293+ const cursor = document.createElement("div");
294+ cursor.className = CURSOR_CLASS;
295+ cursor.innerHTML =
296+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="32" height="32">' +
297+ '<path fill="#f59e0b" stroke="#0f172a" stroke-width="1.2" d="M4 2l14 14h-6l-4 8z"/></svg>';
298+ const startX = Math.max(0, cx - 100);
299+ const startY = Math.max(0, cy - 80);
300+ Object.assign(cursor.style, {
301+ top: startY + "px",
302+ left: startX + "px",
303+ });
304+ root.appendChild(cursor);
305+
306+ requestAnimationFrame(() => {
150307 requestAnimationFrame(() => {
151- requestAnimationFrame(() => {
152- cursor.style.top = cy + "px";
153- cursor.style.left = cx + "px";
154- });
308+ cursor.style.top = cy + "px";
309+ cursor.style.left = cx + "px";
155310 });
156- }
311+ });
312+ }
313+
314+ function showMutation(payload) {
315+ ensureStyles();
316+ const root = ensureRoot();
317+ clearAnimation(root);
318+ appendEntry(root, payload);
319+ showMutationAnimation(root, payload && payload.box ? payload.box : null);
157320 }
158321
159322 window.__pqaOverlay = {
0 commit comments