saitejatirunagari Claude Sonnet 4.6 commited on
Commit
6884f35
Β·
1 Parent(s): 08033a0

feat: migrate injected panel to Chrome Side Panel API

Browse files

The 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 CHANGED
@@ -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
  }
extension/content.js CHANGED
@@ -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
 
 
 
 
extension/manifest.json CHANGED
@@ -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",
extension/popup/popup.js CHANGED
@@ -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 PANEL_EMBEDDED = window.top !== window;
 
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');