Spaces:
Sleeping
feat: migrate injected panel to Chrome Side Panel API
Browse filesThe injected-panel approach couldn't trigger page reflow because CSS
changes don't affect window.innerWidth (what media queries check).
The Chrome Side Panel API creates a real browser sidebar that actually
reduces the viewport, so LinkedIn/Naukri reflow exactly like a window
resize β full quality, no zoom, no clipping.
- manifest: add sidePanel permission + side_panel config (popup.html?mode=sidepanel)
- background: setPanelBehavior openPanelOnActionClick=true (toolbar click = panel)
- content: remove entire injected panel + page zoom/shift code
- popup: detect ?mode=sidepanel to skip the dock button in side panel context
Usage: click the extension icon in the Chrome toolbar to open/close the panel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- extension/background.js +5 -0
- extension/content.js +0 -131
- extension/manifest.json +5 -2
- extension/popup/popup.js +2 -1
|
@@ -5,6 +5,11 @@
|
|
| 5 |
// Run before configuring anything. importScripts works in a classic MV3 worker.
|
| 6 |
try { importScripts('default_resume.js'); } catch (_) { /* default optional */ }
|
| 7 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
function safeRespond(sendResponse, data) {
|
| 9 |
try { sendResponse(data); } catch (_) { /* popup closed before reply */ }
|
| 10 |
}
|
|
|
|
| 5 |
// Run before configuring anything. importScripts works in a classic MV3 worker.
|
| 6 |
try { importScripts('default_resume.js'); } catch (_) { /* default optional */ }
|
| 7 |
|
| 8 |
+
// Open the side panel when the user clicks the extension toolbar icon.
|
| 9 |
+
if (chrome.sidePanel?.setPanelBehavior) {
|
| 10 |
+
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }).catch(() => {});
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
function safeRespond(sendResponse, data) {
|
| 14 |
try { sendResponse(data); } catch (_) { /* popup closed before reply */ }
|
| 15 |
}
|
|
@@ -389,127 +389,6 @@ async function extractJD() {
|
|
| 389 |
return result;
|
| 390 |
}
|
| 391 |
|
| 392 |
-
// βββ Persistent left-docked side panel (R18) βββββββββββββββββββββββββββββββββ
|
| 393 |
-
|
| 394 |
-
const PANEL_UI_KEY = 'panel_ui'; // { collapsed, dismissed }
|
| 395 |
-
|
| 396 |
-
/** True when this origin can't host the panel (extension / internal pages). */
|
| 397 |
-
function isRestrictedPanelHost() {
|
| 398 |
-
const p = (location.protocol || '').toLowerCase();
|
| 399 |
-
return p === 'chrome:' || p === 'chrome-extension:' || p === 'about:' || p === 'edge:';
|
| 400 |
-
}
|
| 401 |
-
|
| 402 |
-
/**
|
| 403 |
-
* Inject a persistent, collapsible left-docked panel that hosts the existing
|
| 404 |
-
* popup UI (popup/popup.html) in an iframe. The panel stays visible while the
|
| 405 |
-
* user interacts with the page, so a background-owned Run (R15) never appears
|
| 406 |
-
* to "restart". Idempotent: re-running never double-injects. Only the narrow
|
| 407 |
-
* dock + tab capture pointer events, so the rest of the page stays clickable.
|
| 408 |
-
* @param {boolean} forceShow
|
| 409 |
-
*/
|
| 410 |
-
function injectSidePanel(forceShow) {
|
| 411 |
-
try {
|
| 412 |
-
if (isRestrictedPanelHost()) return;
|
| 413 |
-
if (window.top !== window) return; // never inject inside our own iframe
|
| 414 |
-
|
| 415 |
-
let root = document.getElementById('ats-side-panel-root');
|
| 416 |
-
if (root) {
|
| 417 |
-
if (forceShow) root.classList.remove('collapsed');
|
| 418 |
-
return;
|
| 419 |
-
}
|
| 420 |
-
|
| 421 |
-
if (!document.getElementById('ats-side-panel-style')) {
|
| 422 |
-
const style = document.createElement('style');
|
| 423 |
-
style.id = 'ats-side-panel-style';
|
| 424 |
-
style.textContent = [
|
| 425 |
-
'#ats-side-panel-root{position:fixed;top:0;left:0;height:100vh;',
|
| 426 |
-
'z-index:2147483646;display:flex;align-items:stretch;font:initial;}',
|
| 427 |
-
'#ats-side-panel-frame{width:380px;height:100%;border:0;background:#fff;',
|
| 428 |
-
'box-shadow:2px 0 12px rgba(0,0,0,.18);}',
|
| 429 |
-
'#ats-side-panel-root.collapsed #ats-side-panel-frame{width:0;box-shadow:none;}',
|
| 430 |
-
'#ats-side-panel-tab{align-self:center;display:flex;flex-direction:column;gap:6px;}',
|
| 431 |
-
'#ats-side-panel-toggle,#ats-side-panel-dismiss{cursor:pointer;border:0;color:#fff;',
|
| 432 |
-
'background:#2563eb;font:600 11px/1 system-ui,sans-serif;padding:10px 6px;',
|
| 433 |
-
'border-radius:0 6px 6px 0;writing-mode:vertical-rl;}',
|
| 434 |
-
'#ats-side-panel-dismiss{background:#64748b;}',
|
| 435 |
-
].join('');
|
| 436 |
-
(document.head || document.documentElement).appendChild(style);
|
| 437 |
-
}
|
| 438 |
-
|
| 439 |
-
root = document.createElement('div');
|
| 440 |
-
root.id = 'ats-side-panel-root';
|
| 441 |
-
|
| 442 |
-
const frame = document.createElement('iframe');
|
| 443 |
-
frame.id = 'ats-side-panel-frame';
|
| 444 |
-
frame.src = chrome.runtime.getURL('popup/popup.html');
|
| 445 |
-
root.appendChild(frame);
|
| 446 |
-
|
| 447 |
-
function persistPanelUi(dismissed) {
|
| 448 |
-
try {
|
| 449 |
-
chrome.storage.local.set({
|
| 450 |
-
[PANEL_UI_KEY]: {
|
| 451 |
-
collapsed: root.isConnected ? root.classList.contains('collapsed') : false,
|
| 452 |
-
dismissed: !!dismissed,
|
| 453 |
-
},
|
| 454 |
-
});
|
| 455 |
-
} catch (_) { /* quota β ignore */ }
|
| 456 |
-
}
|
| 457 |
-
|
| 458 |
-
const tab = document.createElement('div');
|
| 459 |
-
tab.id = 'ats-side-panel-tab';
|
| 460 |
-
const toggle = document.createElement('button');
|
| 461 |
-
toggle.id = 'ats-side-panel-toggle';
|
| 462 |
-
toggle.textContent = 'ATS';
|
| 463 |
-
toggle.title = 'Show / hide the ATS panel';
|
| 464 |
-
function _shiftPage(open) {
|
| 465 |
-
const body = document.body;
|
| 466 |
-
if (!body) return;
|
| 467 |
-
if (open) {
|
| 468 |
-
// zoom on body triggers a real layout reflow (same as resizing the window).
|
| 469 |
-
// Panel root lives outside body so it is unaffected by the zoom.
|
| 470 |
-
const ratio = ((window.innerWidth - 380) / window.innerWidth).toFixed(4);
|
| 471 |
-
body.style.setProperty('zoom', ratio, 'important');
|
| 472 |
-
body.style.setProperty('margin-left', '380px', 'important');
|
| 473 |
-
} else {
|
| 474 |
-
body.style.removeProperty('zoom');
|
| 475 |
-
body.style.removeProperty('margin-left');
|
| 476 |
-
}
|
| 477 |
-
}
|
| 478 |
-
|
| 479 |
-
toggle.addEventListener('click', () => {
|
| 480 |
-
root.classList.toggle('collapsed');
|
| 481 |
-
_shiftPage(!root.classList.contains('collapsed'));
|
| 482 |
-
persistPanelUi(false);
|
| 483 |
-
});
|
| 484 |
-
const dismiss = document.createElement('button');
|
| 485 |
-
dismiss.id = 'ats-side-panel-dismiss';
|
| 486 |
-
dismiss.textContent = '\u00d7'; // Γ
|
| 487 |
-
dismiss.title = 'Hide for this page';
|
| 488 |
-
dismiss.addEventListener('click', () => {
|
| 489 |
-
_shiftPage(false);
|
| 490 |
-
root.remove();
|
| 491 |
-
persistPanelUi(true);
|
| 492 |
-
});
|
| 493 |
-
tab.appendChild(toggle);
|
| 494 |
-
tab.appendChild(dismiss);
|
| 495 |
-
root.appendChild(tab);
|
| 496 |
-
|
| 497 |
-
// Insert as child of <html> (before <body>) so body zoom doesn't shrink the panel.
|
| 498 |
-
document.documentElement.insertBefore(root, document.body || null);
|
| 499 |
-
_shiftPage(true); // panel starts expanded β push page right immediately
|
| 500 |
-
|
| 501 |
-
// Restore the saved collapsed / dismissed preference (unless force-shown).
|
| 502 |
-
try {
|
| 503 |
-
chrome.storage.local.get([PANEL_UI_KEY], (res) => {
|
| 504 |
-
if (chrome.runtime.lastError) return;
|
| 505 |
-
const ui = (res && res[PANEL_UI_KEY]) || {};
|
| 506 |
-
if (!forceShow && ui.dismissed) { _shiftPage(false); root.remove(); return; }
|
| 507 |
-
if (!forceShow && ui.collapsed) { root.classList.add('collapsed'); _shiftPage(false); }
|
| 508 |
-
});
|
| 509 |
-
} catch (_) { /* ignore */ }
|
| 510 |
-
} catch (_) { /* never break the host page */ }
|
| 511 |
-
}
|
| 512 |
-
|
| 513 |
// βββ Non-job / recommendation-page detection βββββββββββββββββββββββββββββββββ
|
| 514 |
|
| 515 |
// Headers LinkedIn/Indeed/Naukri show on jobs HOME / search / collections pages
|
|
@@ -754,13 +633,6 @@ function applyAutofillAnswers(answers) {
|
|
| 754 |
// βββ Message listener βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 755 |
|
| 756 |
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
| 757 |
-
if (msg.type === 'TOGGLE_PANEL') {
|
| 758 |
-
const root = document.getElementById('ats-side-panel-root');
|
| 759 |
-
if (!root) injectSidePanel(true);
|
| 760 |
-
else root.classList.toggle('collapsed');
|
| 761 |
-
sendResponse({ ok: true });
|
| 762 |
-
return; // synchronous response
|
| 763 |
-
}
|
| 764 |
|
| 765 |
if (msg.type === 'SCAN_FORM') {
|
| 766 |
try {
|
|
@@ -821,6 +693,3 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
| 821 |
}
|
| 822 |
});
|
| 823 |
|
| 824 |
-
// Inject the persistent left-docked panel once the content script loads
|
| 825 |
-
// (idempotent; respects the saved collapsed/dismissed preference).
|
| 826 |
-
injectSidePanel(false);
|
|
|
|
| 389 |
return result;
|
| 390 |
}
|
| 391 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
// βββ Non-job / recommendation-page detection βββββββββββββββββββββββββββββββββ
|
| 393 |
|
| 394 |
// Headers LinkedIn/Indeed/Naukri show on jobs HOME / search / collections pages
|
|
|
|
| 633 |
// βββ Message listener βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 634 |
|
| 635 |
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 636 |
|
| 637 |
if (msg.type === 'SCAN_FORM') {
|
| 638 |
try {
|
|
|
|
| 693 |
}
|
| 694 |
});
|
| 695 |
|
|
|
|
|
|
|
|
|
|
@@ -7,7 +7,8 @@
|
|
| 7 |
"storage",
|
| 8 |
"downloads",
|
| 9 |
"activeTab",
|
| 10 |
-
"scripting"
|
|
|
|
| 11 |
],
|
| 12 |
"host_permissions": [
|
| 13 |
"<all_urls>"
|
|
@@ -15,8 +16,10 @@
|
|
| 15 |
"background": {
|
| 16 |
"service_worker": "background.js"
|
| 17 |
},
|
|
|
|
|
|
|
|
|
|
| 18 |
"action": {
|
| 19 |
-
"default_popup": "popup/popup.html",
|
| 20 |
"default_icon": {
|
| 21 |
"16": "icons/icon16.png",
|
| 22 |
"48": "icons/icon48.png",
|
|
|
|
| 7 |
"storage",
|
| 8 |
"downloads",
|
| 9 |
"activeTab",
|
| 10 |
+
"scripting",
|
| 11 |
+
"sidePanel"
|
| 12 |
],
|
| 13 |
"host_permissions": [
|
| 14 |
"<all_urls>"
|
|
|
|
| 16 |
"background": {
|
| 17 |
"service_worker": "background.js"
|
| 18 |
},
|
| 19 |
+
"side_panel": {
|
| 20 |
+
"default_path": "popup/popup.html?mode=sidepanel"
|
| 21 |
+
},
|
| 22 |
"action": {
|
|
|
|
| 23 |
"default_icon": {
|
| 24 |
"16": "icons/icon16.png",
|
| 25 |
"48": "icons/icon48.png",
|
|
@@ -46,7 +46,8 @@ const manualJDEl = document.getElementById('manual-jd');
|
|
| 46 |
// a button docks the persistent in-page panel and closes the popup. When this
|
| 47 |
// page runs EMBEDDED inside the content-script iframe (window.top !== window),
|
| 48 |
// the control is hidden because the UI is already docked.
|
| 49 |
-
const
|
|
|
|
| 50 |
function setupPanelLauncher() {
|
| 51 |
if (PANEL_EMBEDDED) return;
|
| 52 |
const bar = document.createElement('button');
|
|
|
|
| 46 |
// a button docks the persistent in-page panel and closes the popup. When this
|
| 47 |
// page runs EMBEDDED inside the content-script iframe (window.top !== window),
|
| 48 |
// the control is hidden because the UI is already docked.
|
| 49 |
+
const IS_SIDE_PANEL = new URLSearchParams(location.search).get('mode') === 'sidepanel';
|
| 50 |
+
const PANEL_EMBEDDED = window.top !== window || IS_SIDE_PANEL;
|
| 51 |
function setupPanelLauncher() {
|
| 52 |
if (PANEL_EMBEDDED) return;
|
| 53 |
const bar = document.createElement('button');
|