Legal Acts Finder — Polished Slide Sidebar

Detect legal acts/rules, dedupe mentions, find PDFs (or fallback to Google). Smooth slide-out sidebar, draggable (vertical), batch open PDFs, keyboard toggle (Alt+Shift+L). Theme-aware.

// ==UserScript==
// @name         Legal Acts Finder — Polished Slide Sidebar
// @namespace    http://tampermonkey.net/
// @version      1.9.2
// @description  Detect legal acts/rules, dedupe mentions, find PDFs (or fallback to Google). Smooth slide-out sidebar, draggable (vertical), batch open PDFs, keyboard toggle (Alt+Shift+L). Theme-aware.
// @author       iamnobody
// @license      MIT
// @match        *://*/*
// @exclude      *://www.google.*/*
// @exclude      *://search.yahoo.com/*
// @exclude      *://www.bing.com/*
// @exclude      *://duckduckgo.com/*
// @exclude      *://search.brave.com/*
// @exclude      *://*.yandex.*/*
// @grant        GM_xmlhttpRequest
// @icon         https://greasyfork.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTg5MTc1LCJwdXIiOiJibG9iX2lkIn19--c218824699773e9e6d58fe11cc76cdbb165a2e65/1000031087.jpg?locale=en
// @banner       https://greasyfork.org/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsiZGF0YSI6MTg5MTczLCJwdXIiOiJibG9iX2lkIn19--77a89502797ffc05cd152a04c877a3b3de4c24be/1000031086.jpg?locale=en
// ==/UserScript==

(() => {
  'use strict';

  /* ---------- Early exit if site is excluded ---------- */
  const HOST = location.hostname;
  const EX_KEY = 'la_excluded_sites';
  const excluded = JSON.parse(localStorage.getItem(EX_KEY) || '[]');
  if (excluded.includes(HOST)) return;

  /* ---------- Config ---------- */
  const MIN_WIDTH_PX = 250;
  const MAX_WIDTH_PCT = 30;  // sidebar width cap (% of viewport)
  const TRANS_MS      = 320; // animation
  const FETCH_CONCURRENCY = 4;
  const OPEN_DELAY_MS = 350; // ms between tab opens
  const TOGGLE_SHORTCUT = { altKey:true, shiftKey:true, key:'L' }; // Alt+Shift+L

  /* ---------- Regexes ---------- */
  const actRegex  = /(\b[A-Z]?[a-zA-Z&\-\s]{2,}?\s+act\s+of\s+\d{4}\b)|(\b[A-Z]?[a-zA-Z&\-\s]{2,}?\s+act,\s+\d{4}\b)|(\b[A-Z]?[a-zA-Z&\-\s]{2,}?\s+act\s+of\s+year\s+\d{4}\b)/gi;
  const ruleRegex = /\bsection\s+\w+\s+of\s+\w+\s+act,\s+\d{4}\b/gi;

  /* ---------- Helpers ---------- */
  const clamp       = (v,a,b)=>Math.max(a,Math.min(b,v));
  const escapeHtml  = s=>String(s).replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));
  const queuedFetch = createQueue(FETCH_CONCURRENCY);

  /* ---------- Extract & dedupe ---------- */
  function extractUniqueMatches(){
    const text=(document.body.innerText||'').replace(/\s+/g,' ');
    const acts = [...text.matchAll(actRegex)].map(m=>(m[0]||'').trim());
    const rules= [...text.matchAll(ruleRegex)].map(m=>(m[0]||'').trim());
    const seen=new Map();
    [...acts,...rules].forEach(r=>{
      const k=r.toLowerCase();
      if(!seen.has(k)) seen.set(k,r);
    });
    return [...seen.values()];
  }

  /* ---------- Limited-concurrency queue ---------- */
  function createQueue(max){
    const q=[]; let running=0;
    const next=()=>{
      if(running>=max || !q.length) return;
      const {query,res} = q.shift(); running++;
      fetchPdf(query).then(r=>res(r)).finally(()=>{running--; next();});
    };
    return query=>new Promise(res=>{q.push({query,res}); next();});
  }

  function fetchPdf(query){
    return new Promise(resolve=>{
      const g = `https://www.google.com/search?q=${encodeURIComponent(query+' pdf')}`;
      GM_xmlhttpRequest({
        method:'GET', url:g, headers:{'User-Agent':navigator.userAgent},
        onload:r=>{
          const html=r.responseText||'';
          const m=html.match(/https?:\/\/[^"'>\s]+?\.pdf\b/i);
          resolve(m?{url:m[0].replace(/\u0026/g,'&'),type:'pdf'}:{url:g,type:'search'});
        },
        onerror: ()=>resolve({url:g,type:'search'}),
        timeout:15000
      });
    });
  }

  /* ---------- UI ---------- */
  function createSidebar(matches){
    /* container */
    const box=document.createElement('div');
    Object.assign(box.style,{position:'fixed',top:'50%',right:'0',transform:'translateY(-50%)',zIndex:2147483647});
    document.body.appendChild(box);

    /* style */
    const css=document.createElement('style');
    css.textContent=`
:root{--bg:#fff;--fg:#0b1220;--accent:#ff8a00;--muted:rgba(11,18,32,.6);--shadow:rgba(12,16,20,.12)}
@media(prefers-color-scheme:dark){:root{--bg:#07101a;--fg:#e6eef8;--accent:#ffb86b;--muted:rgba(230,238,248,.7);--shadow:rgba(0,0,0,.6)}}
#la-sidebar{position:fixed;right:0;top:50%;transform:translate(100%,-50%);opacity:0;
  transition:transform ${TRANS_MS}ms cubic-bezier(.2,.9,.2,1),opacity ${TRANS_MS}ms;
  width:min(${MAX_WIDTH_PCT}vw,100%);min-width:${MIN_WIDTH_PX}px;max-height:80vh;
  background:var(--bg);color:var(--fg);border-radius:12px 0 0 12px;box-shadow:0 12px 30px var(--shadow);
  display:flex;flex-direction:column;overflow:hidden}
#la-sidebar.open{transform:translate(0,-50%);opacity:1}
#la-header{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;border-bottom:1px solid rgba(0,0,0,.06)}
#la-title{display:flex;gap:8px;align-items:center;font-weight:600;font-size:15px}
.la-dot{width:10px;height:10px;border-radius:50%;background:var(--accent)}
#la-controls{display:flex;gap:8px;align-items:center}
.btn{background:transparent;border:1px solid rgba(0,0,0,.12);padding:6px 10px;border-radius:8px;color:var(--fg);cursor:pointer}
.btn[disabled]{opacity:.45;cursor:not-allowed}
.btn-red{border-color:rgba(255,0,0,.25)}
#la-list{flex:1 1 auto;overflow-y:auto;padding:10px}
.la-item{padding:8px 10px;border-radius:8px;margin-bottom:8px}
.la-item:hover{background:rgba(0,0,0,.03)}
.la-item a{color:var(--accent);text-decoration:none;font-weight:600}
.la-meta{font-size:12px;color:var(--muted);margin-top:6px}
#la-footer{padding:8px 12px;border-top:1px solid rgba(0,0,0,.05);font-size:12px;color:var(--muted);display:flex;justify-content:space-between}
#la-tab{position:absolute;right:0;top:50%;transform:translateY(-50%);width:36px;height:76px;border-radius:8px 0 0 8px;
  background:var(--accent);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:800;cursor:pointer;box-shadow:0 8px 20px var(--shadow)}
#la-accordion{padding:10px;border-top:1px solid rgba(0,0,0,.04)}
#la-accordion-toggle{display:flex;gap:8px;cursor:pointer;user-select:none}
#la-accordion-content{overflow:hidden;max-height:0;transition:max-height ${TRANS_MS}ms cubic-bezier(.2,.9,.2,1);padding-top:8px}
`;
    document.head.appendChild(css);

    /* sidebar */
    const panel=document.createElement('aside'); panel.id='la-sidebar'; box.appendChild(panel);

    /* header */
    const header=document.createElement('div'); header.id='la-header'; panel.appendChild(header);
    header.innerHTML=`<div id="la-title"><span class="la-dot"></span><span>Acts & Rules</span></div>`;
    const ctr=document.createElement('div'); ctr.id='la-controls'; header.appendChild(ctr);

    const openAll=document.createElement('button'); openAll.className='btn'; openAll.textContent='Open All PDFs (0)'; openAll.disabled=true; ctr.appendChild(openAll);
    const closeBtn=document.createElement('button'); closeBtn.className='btn'; closeBtn.textContent='✕'; ctr.appendChild(closeBtn);

    /* exclude button */
    const exBtn=document.createElement('button'); exBtn.className='btn btn-red'; ctr.appendChild(exBtn);
    let excludedSites=[...excluded]; let isExcluded=excludedSites.includes(HOST);
    updateExBtn();
    function updateExBtn(){ exBtn.textContent=isExcluded?'Re-enable Site':'Exclude Site'; }
    exBtn.addEventListener('click',()=>{
      if(isExcluded){ excludedSites=excludedSites.filter(h=>h!==HOST); }
      else           { excludedSites.push(HOST); }
      localStorage.setItem(EX_KEY,JSON.stringify(excludedSites));
      isExcluded=!isExcluded; updateExBtn(); alert('Reload to apply.'); });

    /* list, accordion, footer, tab */
    const list=document.createElement('div'); list.id='la-list'; panel.appendChild(list);
    const accWrap=document.createElement('div'); accWrap.id='la-accordion'; panel.appendChild(accWrap);
    accWrap.innerHTML=`<div id="la-accordion-toggle"><span id="la-arrow">►</span><span>View individual PDFs</span></div><div id="la-accordion-content"></div>`;
    const accToggle=accWrap.firstElementChild, accContent=accWrap.lastElementChild;
    const foot=document.createElement('div'); foot.id='la-footer'; foot.innerHTML='<span>Alt+Shift+L to toggle</span><span style="font-size:11px;opacity:.6">Polite queue</span>'; panel.appendChild(foot);
    const tab=document.createElement('div'); tab.id='la-tab'; tab.textContent='‹'; box.appendChild(tab);

    /* PDF tracking */
    const pdfList=[]; const nodes=[];

    function showNo(){
      list.innerHTML='<div class="la-item"><div class="title">No acts/rules found.</div></div>';
    }

    async function fill(matches){
      list.innerHTML='';
      if(!matches.length) return showNo();
      matches.forEach(m=>{
        const n=document.createElement('div'); n.className='la-item';
        n.innerHTML=`<div class="title">${escapeHtml(m)}</div><div class="la-meta">Looking up PDF…</div>`;
        list.appendChild(n); nodes.push({q:m,el:n});
      });
      for(const it of nodes){
        const res=await queuedFetch(it.q);
        it.el.innerHTML='';
        it.el.innerHTML=`<a href="${res.url}" target="_blank" rel="noopener">${escapeHtml(it.q)}</a><div class="la-meta">${
          res.type==='pdf'?'Direct PDF':'Google search'}</div>`;
        if(res.type==='pdf') pdfList.push({q:it.q,url:res.url});
        updateBatch(); }
      buildAcc();
    }

    function updateBatch(){
      openAll.textContent=`Open All PDFs (${pdfList.length})`;
      openAll.disabled=!pdfList.length;
    }

    function buildAcc(){
      accContent.innerHTML='';
      if(!pdfList.length) return accContent.textContent='No direct PDFs found.';
      pdfList.forEach(p=>{
        const r=document.createElement('div'); r.className='la-item';
        r.innerHTML=`<a href="${p.url}" target="_blank" rel="noopener">${escapeHtml(p.q)}</a>`;
        accContent.appendChild(r); });
    }

    /* panel toggle */
    let open=false;
    function show(){ open=true; panel.classList.add('open'); tab.style.display='none'; }
    function hide(){ open=false; panel.classList.remove('open'); tab.style.display='flex'; }
    tab.onclick=show; closeBtn.onclick=hide;

    /* accordion */
    let accOpen=false; accToggle.onclick=()=>{
      accOpen=!accOpen;
      accToggle.firstElementChild.style.transform=accOpen?'rotate(90deg)':'none';
      accContent.style.maxHeight=accOpen?accContent.scrollHeight+'px':'0';
    };

    /* batch open */
    openAll.onclick=async()=>{
      if(!pdfList.length) return;
      if(!confirm(`Open ${pdfList.length} PDF(s) in new tabs?`)) return;
      for(const p of pdfList){
        window.open(p.url,'_blank','noopener');
        await new Promise(r=>setTimeout(r,OPEN_DELAY_MS));
      }
    };

    /* responsive width 
    function setW(){
      const vw=Math.max(document.documentElement.clientWidth,window.innerWidth);
      panel.style.width=Math.max(MIN_WIDTH_PX,Math.floor(vw*MAX_WIDTH_PCT/100))+'px';
    }
    setW(); window.addEventListener('resize',setW);*/

    /* kick off */
    fill(matches);
  }

  /* ---------- boot ---------- */
  const matches=extractUniqueMatches();
  (document.readyState==='complete'||document.readyState==='interactive')
    ? setTimeout(()=>createSidebar(matches),400)
    : window.addEventListener('load',()=>setTimeout(()=>createSidebar(matches),300));

})();