Skip to content

Instantly share code, notes, and snippets.

@minanagehsalalma
Last active March 25, 2026 17:24
Show Gist options
  • Select an option

  • Save minanagehsalalma/22178e2b2dc914235b848b357f3b5b1e to your computer and use it in GitHub Desktop.

Select an option

Save minanagehsalalma/22178e2b2dc914235b848b357f3b5b1e to your computer and use it in GitHub Desktop.
a bookmarklet that will extract all values from dropdown menus on a page.
javascript:(function(){
const delay = ms => new Promise(r => setTimeout(r, ms));
// ── Selectors ──────────────────────────────────────────────────────────────
const triggerSelectors = [
'[aria-haspopup="listbox"]','[aria-haspopup="true"]','[aria-expanded]',
'button[class*="select"]','div[class*="select"]',
'[class*="dropdown"] > button','[class*="trigger"]','select',
];
const optionSelectors = [
'[role="option"]','[role="menuitem"]','[role="listbox"] li','[data-value]','[aria-selected]',
];
// ── Helpers ────────────────────────────────────────────────────────────────
function getLabel(el) {
if (el.id) {
const lbl = document.querySelector(`label[for="${el.id}"]`);
if (lbl) return lbl.textContent.trim();
}
return el.getAttribute('aria-label') || el.getAttribute('placeholder') ||
el.getAttribute('name') ||
el.querySelector('span,label,p')?.textContent?.trim() ||
el.textContent.trim().replace(/\s+/g,' ').slice(0,40) ||
`Dropdown #${Math.random().toString(36).slice(2,6)}`;
}
function readNativeOptions(sel) {
return Array.from(sel.options).map(o => ({ text: o.textContent.trim(), value: o.value }));
}
function isPlaceholder(opt) {
return opt.value === '' || opt.value === '-1' || opt.value === '0' || opt.text === '--' || opt.text === '-';
}
// Trigger a native select change (works with Vue/Angular/React-ish)
function setNativeValue(sel, value) {
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'value');
if (nativeSetter && nativeSetter.set) nativeSetter.set.call(sel, value);
else sel.value = value;
sel.dispatchEvent(new Event('input', { bubbles: true }));
sel.dispatchEvent(new Event('change', { bubbles: true }));
}
// Collect all visible triggers once
const allTriggers = [];
const seenEls = new Set();
triggerSelectors.forEach(sel => {
document.querySelectorAll(sel).forEach(el => {
if (!seenEls.has(el) && el.offsetParent !== null) { seenEls.add(el); allTriggers.push(el); }
});
});
const nativeSelects = allTriggers.filter(el => el.tagName === 'SELECT');
const jsDropdowns = allTriggers.filter(el => el.tagName !== 'SELECT');
// ── Highlight helpers ──────────────────────────────────────────────────────
const ORIG = new WeakMap();
function hl(el, color) {
if (!ORIG.has(el)) ORIG.set(el, { outline: el.style.outline, outlineOffset: el.style.outlineOffset, boxShadow: el.style.boxShadow });
el.style.outline = `2px solid ${color}`; el.style.outlineOffset = '2px'; el.style.boxShadow = `0 0 0 4px ${color}33`;
}
function unhl(el) {
if (ORIG.has(el)) { const s = ORIG.get(el); el.style.outline = s.outline; el.style.outlineOffset = s.outlineOffset; el.style.boxShadow = s.boxShadow; }
}
function refreshHL(selected) {
allTriggers.forEach(el => selected.has(el) ? hl(el, '#22c55e') : unhl(el));
}
// ── Core export logic ──────────────────────────────────────────────────────
async function exportTargets(targets, statusEl, exportBtn) {
const allFound = {};
const nativeTargets = targets.filter(el => el.tagName === 'SELECT');
const jsTargets = targets.filter(el => el.tagName !== 'SELECT');
// --- Pass 1: snapshot all native selects directly ---
for (const sel of nativeTargets) {
const label = getLabel(sel);
const opts = readNativeOptions(sel);
const realOpts = opts.filter(o => !isPlaceholder(o));
if (realOpts.length > 0) {
allFound[label] = realOpts.map(o => `${o.text} [value="${o.value}"]`);
} else {
// Seems dependent — mark for pass 2
allFound[label] = null;
}
}
// --- Pass 2: detect & expand dependent native selects ---
// For each "empty" select, try every other native select as the potential parent
const emptySelects = nativeTargets.filter(s => allFound[getLabel(s)] === null);
for (const depSel of emptySelects) {
const depLabel = getLabel(depSel);
const mapping = {}; // parentOptionText -> [child options]
let foundParent = false;
for (const parentSel of nativeTargets) {
if (parentSel === depSel) continue;
const parentOpts = readNativeOptions(parentSel).filter(o => !isPlaceholder(o));
if (parentOpts.length === 0) continue;
const savedParentVal = parentSel.value;
let anyChildOpts = false;
for (const pOpt of parentOpts) {
statusEl.textContent = `Probing "${getLabel(depSel)}" ← "${pOpt.text}"…`;
setNativeValue(parentSel, pOpt.value);
await delay(350);
const childOpts = readNativeOptions(depSel).filter(o => !isPlaceholder(o));
if (childOpts.length > 0) {
mapping[pOpt.text] = childOpts.map(o => `${o.text} [value="${o.value}"]`);
anyChildOpts = true;
}
}
// Restore parent
setNativeValue(parentSel, savedParentVal);
await delay(200);
if (anyChildOpts) { foundParent = true; break; }
}
if (foundParent && Object.keys(mapping).length > 0) {
allFound[depLabel] = mapping; // store as object = dependent map
} else {
allFound[depLabel] = []; // truly empty
}
}
// --- Pass 3: JS dropdowns ---
for (let i = 0; i < jsTargets.length; i++) {
const trigger = jsTargets[i];
const label = getLabel(trigger);
statusEl.textContent = `Scanning JS dropdown ${i+1}/${jsTargets.length}: "${label.slice(0,22)}"…`;
try {
const before = new Set(
[...document.querySelectorAll(optionSelectors.join(','))]
.filter(e => e.offsetParent !== null).map(e => e.textContent.trim())
);
trigger.click();
trigger.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
trigger.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
await delay(450);
const seen = new Set(); const opts = [];
optionSelectors.forEach(sel => {
document.querySelectorAll(sel).forEach(el => {
if (!el.offsetParent) return;
const text = el.textContent.trim().replace(/\s+/g,' ');
if (text && text.length > 1 && !seen.has(text) && !before.has(text)) { seen.add(text); opts.push(text); }
});
});
if (!opts.length) {
optionSelectors.forEach(sel => {
document.querySelectorAll(sel).forEach(el => {
if (!el.offsetParent) return;
const text = el.textContent.trim().replace(/\s+/g,' ');
if (text && text.length > 1 && !seen.has(text)) { seen.add(text); opts.push(text); }
});
});
}
if (opts.length) allFound[label] = opts;
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
trigger.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
document.body.click();
await delay(200);
} catch(e) {}
}
return allFound;
}
// ── Format output ──────────────────────────────────────────────────────────
function formatOutput(allFound, scannedCount) {
let totalItems = 0;
Object.values(allFound).forEach(v => {
if (!v) return;
if (Array.isArray(v)) totalItems += v.length;
else Object.values(v).forEach(arr => totalItems += arr.length);
});
let out = `=== DROPDOWN EXPORT ===\nScanned: ${scannedCount} | Found: ${totalItems} options\n${'='.repeat(40)}\n\n`;
Object.entries(allFound).forEach(([label, val]) => {
if (!val) return;
out += `📋 ${label}\n`;
if (Array.isArray(val)) {
if (val.length === 0) { out += ` (no options)\n`; }
else val.forEach((item, i) => out += ` ${i+1}. ${item}\n`);
} else {
// dependent map
out += ` ⚠️ Options depend on parent selection:\n`;
Object.entries(val).forEach(([parentOpt, items]) => {
out += ` ▸ When parent = "${parentOpt}":\n`;
items.forEach((item, i) => out += ` ${i+1}. ${item}\n`);
});
}
out += '\n';
});
return { out, totalItems };
}
async function runExport(targets, statusEl, exportBtn) {
const allFound = await exportTargets(targets, statusEl, exportBtn);
const { out, totalItems } = formatOutput(allFound, targets.length);
exportBtn.textContent = exportBtn.dataset.label;
exportBtn.disabled = false;
navigator.clipboard.writeText(out).then(() => {
statusEl.textContent = `✅ Copied ${totalItems} options to clipboard!`;
statusEl.style.color = '#22c55e';
}).catch(() => {
// Fallback textarea
cleanup();
const ta = document.createElement('textarea');
ta.value = out;
ta.style.cssText = 'position:fixed;top:5%;left:5%;width:90%;height:90%;z-index:9999999;padding:12px;font:13px monospace;background:#1e1e1e;color:#d4d4d4;border:2px solid #444;border-radius:8px;';
document.body.appendChild(ta); ta.select();
const cb = document.createElement('button');
cb.textContent = '✕ Close';
cb.style.cssText = 'position:fixed;top:6%;right:6%;z-index:9999999;padding:6px 14px;background:#c0392b;color:white;border:none;border-radius:6px;cursor:pointer;';
cb.onclick = () => { ta.remove(); cb.remove(); };
document.body.appendChild(cb);
});
}
// ── Build UI ───────────────────────────────────────────────────────────────
const overlay = document.createElement('div');
overlay.style.cssText = `position:fixed;top:16px;right:16px;z-index:2147483647;width:330px;max-height:82vh;background:#1a1a2e;color:#e0e0e0;border:1px solid #444;border-radius:12px;font:13px/1.5 system-ui,sans-serif;box-shadow:0 8px 32px rgba(0,0,0,.6);display:flex;flex-direction:column;user-select:none;`;
overlay.innerHTML = `
<div id="__ddh__" style="padding:12px 16px;background:#16213e;border-radius:12px 12px 0 0;cursor:move;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #333;">
<span style="font-weight:700;font-size:14px;">🗂️ Dropdown Exporter</span>
<button id="__ddc__" style="background:none;border:none;color:#aaa;font-size:18px;cursor:pointer;line-height:1;padding:0 2px;">&times;</button>
</div>
<!-- Mode switcher -->
<div style="display:flex;border-bottom:1px solid #2a2a3e;">
<button id="__mode_all__" data-mode="all" style="flex:1;padding:9px 0;border:none;background:#22c55e;color:#000;font-weight:700;cursor:pointer;font-size:12px;border-radius:0;">⚡ Export All</button>
<button id="__mode_sel__" data-mode="sel" style="flex:1;padding:9px 0;border:none;background:#2d2d4e;color:#9ca3af;font-weight:600;cursor:pointer;font-size:12px;border-radius:0;">🖱️ Select Mode</button>
</div>
<!-- Export All panel -->
<div id="__panel_all__" style="padding:14px;display:flex;flex-direction:column;gap:10px;">
<div style="font-size:12px;color:#9ca3af;line-height:1.6;">
Exports <b style="color:#e0e0e0;">all ${allTriggers.length} dropdowns</b> found on this page.<br>
Dependent sub-menus are automatically probed.
</div>
<button id="__dde_all__" data-label="⬇️ Export All Dropdowns" style="padding:10px;border-radius:8px;border:none;background:#22c55e;color:#000;font-weight:700;cursor:pointer;font-size:13px;">⬇️ Export All Dropdowns</button>
</div>
<!-- Select Mode panel -->
<div id="__panel_sel__" style="display:none;flex-direction:column;flex:1;overflow:hidden;">
<div style="padding:8px 14px;background:#0f3460;font-size:12px;color:#93c5fd;"><b>Hover</b> to highlight · <b>Click</b> to toggle</div>
<div style="padding:8px 14px;display:flex;gap:8px;border-bottom:1px solid #2a2a3e;">
<button id="__dda__" style="flex:1;padding:5px;border-radius:6px;border:1px solid #555;background:#2d2d4e;color:#e0e0e0;cursor:pointer;font-size:11px;">✅ All</button>
<button id="__ddn__" style="flex:1;padding:5px;border-radius:6px;border:1px solid #555;background:#2d2d4e;color:#e0e0e0;cursor:pointer;font-size:11px;">❌ Clear</button>
</div>
<div id="__ddl__" style="overflow-y:auto;flex:1;padding:8px 6px;min-height:80px;max-height:calc(80vh - 260px);"></div>
<div style="padding:10px 14px;border-top:1px solid #2a2a3e;">
<button id="__dde_sel__" data-label="⬇️ Export Selected" style="width:100%;padding:9px;border-radius:8px;border:none;background:#22c55e;color:#000;font-weight:700;cursor:pointer;font-size:13px;">⬇️ Export Selected</button>
</div>
</div>
<div id="__dds__" style="padding:4px 14px 10px;font-size:11px;color:#6b7280;text-align:center;min-height:20px;"></div>
`;
document.body.appendChild(overlay);
const statusEl = overlay.querySelector('#__dds__');
const listEl = overlay.querySelector('#__ddl__');
const panelAll = overlay.querySelector('#__panel_all__');
const panelSel = overlay.querySelector('#__panel_sel__');
const btnModeAll = overlay.querySelector('#__mode_all__');
const btnModeSel = overlay.querySelector('#__mode_sel__');
const exportAll = overlay.querySelector('#__dde_all__');
const exportSel = overlay.querySelector('#__dde_sel__');
const selectedTriggers = new Set();
// Mode switching
function setMode(mode) {
const isAll = mode === 'all';
panelAll.style.display = isAll ? 'flex' : 'none';
panelSel.style.display = isAll ? 'none' : 'flex';
btnModeAll.style.background = isAll ? '#22c55e' : '#2d2d4e';
btnModeAll.style.color = isAll ? '#000' : '#9ca3af';
btnModeSel.style.background = isAll ? '#2d2d4e' : '#22c55e';
btnModeSel.style.color = isAll ? '#9ca3af' : '#000';
statusEl.textContent = '';
statusEl.style.color = '#6b7280';
if (!isAll) buildList();
}
btnModeAll.addEventListener('click', () => setMode('all'));
btnModeSel.addEventListener('click', () => setMode('sel'));
// ── Select mode list ───────────────────────────────────────────────────────
function updateStatus() {
statusEl.textContent = `${selectedTriggers.size} / ${allTriggers.length} selected`;
exportSel.style.opacity = selectedTriggers.size > 0 ? '1' : '0.45';
exportSel.disabled = selectedTriggers.size === 0;
}
function buildList() {
listEl.innerHTML = '';
if (!allTriggers.length) { listEl.innerHTML = '<div style="padding:16px;color:#6b7280;text-align:center;">No dropdowns detected.</div>'; return; }
allTriggers.forEach(el => {
const label = getLabel(el);
const isNative = el.tagName === 'SELECT';
const row = document.createElement('div');
row.style.cssText = `display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:7px;cursor:pointer;transition:background .15s;margin-bottom:2px;`;
const cb = document.createElement('input');
cb.type = 'checkbox'; cb.style.cssText = 'width:15px;height:15px;accent-color:#22c55e;cursor:pointer;flex-shrink:0;';
const lbl = document.createElement('span');
lbl.textContent = label.length > 28 ? label.slice(0,28)+'…' : label;
lbl.style.cssText = 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;';
const badge = document.createElement('span');
badge.textContent = isNative ? 'native' : 'js';
badge.style.cssText = `font-size:10px;padding:1px 5px;border-radius:4px;flex-shrink:0;background:${isNative?'#1d4ed8':'#7c3aed'};color:${isNative?'#bfdbfe':'#ddd6fe'};`;
row.append(cb, lbl, badge);
listEl.appendChild(row);
function syncRow() {
const on = selectedTriggers.has(el);
cb.checked = on; row.style.background = on ? '#14532d55' : 'transparent';
refreshHL(selectedTriggers); updateStatus();
}
row.addEventListener('click', () => { selectedTriggers.has(el)?selectedTriggers.delete(el):selectedTriggers.add(el); syncRow(); });
row.addEventListener('mouseenter', () => { row.style.background = selectedTriggers.has(el)?'#14532d88':'#2d2d4e'; if(!selectedTriggers.has(el)){hl(el,'#3b82f6');el.scrollIntoView({behavior:'smooth',block:'center'});} });
row.addEventListener('mouseleave', () => { row.style.background = selectedTriggers.has(el)?'#14532d55':'transparent'; if(!selectedTriggers.has(el))unhl(el); });
syncRow();
});
updateStatus();
}
overlay.querySelector('#__dda__').addEventListener('click', () => { allTriggers.forEach(el=>selectedTriggers.add(el)); buildList(); refreshHL(selectedTriggers); });
overlay.querySelector('#__ddn__').addEventListener('click', () => { selectedTriggers.clear(); allTriggers.forEach(el=>unhl(el)); buildList(); });
// Page-level interception (select mode only)
let hoverTarget = null;
function onPageMouseover(e) {
if (overlay.contains(e.target)) return;
const el = allTriggers.find(t => t===e.target||t.contains(e.target));
if (!el) { if(hoverTarget&&!selectedTriggers.has(hoverTarget))unhl(hoverTarget); hoverTarget=null; return; }
if(hoverTarget&&hoverTarget!==el&&!selectedTriggers.has(hoverTarget))unhl(hoverTarget);
hoverTarget=el; if(!selectedTriggers.has(el))hl(el,'#3b82f6');
}
function onPageClick(e) {
if(overlay.contains(e.target))return;
const el=allTriggers.find(t=>t===e.target||t.contains(e.target));
if(!el)return;
e.preventDefault(); e.stopImmediatePropagation();
selectedTriggers.has(el)?selectedTriggers.delete(el):selectedTriggers.add(el);
refreshHL(selectedTriggers); buildList(); updateStatus();
}
document.addEventListener('mouseover', onPageMouseover, true);
document.addEventListener('click', onPageClick, true);
// ── Export buttons ─────────────────────────────────────────────────────────
exportAll.addEventListener('click', async () => {
exportAll.textContent = '⏳ Scanning…'; exportAll.disabled = true;
statusEl.style.color = '#6b7280';
await runExport(allTriggers, statusEl, exportAll);
});
exportSel.addEventListener('click', async () => {
const targets = [...selectedTriggers];
if (!targets.length) return;
exportSel.textContent = '⏳ Scanning…'; exportSel.disabled = true;
statusEl.style.color = '#6b7280';
await runExport(targets, statusEl, exportSel);
});
// ── Close ──────────────────────────────────────────────────────────────────
function cleanup() {
document.removeEventListener('mouseover', onPageMouseover, true);
document.removeEventListener('click', onPageClick, true);
allTriggers.forEach(el => unhl(el));
overlay.remove();
}
overlay.querySelector('#__ddc__').addEventListener('click', cleanup);
// ── Draggable ──────────────────────────────────────────────────────────────
const hdr = overlay.querySelector('#__ddh__');
let drag=false,ox=0,oy=0;
hdr.addEventListener('mousedown', e=>{drag=true;const r=overlay.getBoundingClientRect();ox=e.clientX-r.left;oy=e.clientY-r.top;e.preventDefault();});
document.addEventListener('mousemove', e=>{if(!drag)return;overlay.style.right='auto';overlay.style.left=(e.clientX-ox)+'px';overlay.style.top=(e.clientY-oy)+'px';});
document.addEventListener('mouseup', ()=>drag=false);
})();
@minanagehsalalma
Copy link
Copy Markdown
Author

🗂️ Dropdown Exporter — Bookmarklet

  • Two modesExport All (default, one click) and Select Mode for cherry-picking specific dropdowns
  • Floating draggable panel — stays out of your way, works on any page layout
  • Native <select> support — reads options directly without clicking, zero side effects
  • JS/custom dropdowns — click-to-open with before/after diffing to avoid capturing unrelated options
  • Dependent sub-menu probing — automatically detects dropdowns whose options are empty until a parent is selected, iterates every parent value and maps child options accordingly
  • Smart label resolution — uses <label for="">, aria-label, name, or visible text, in that priority order
  • Visual feedback — blue outline on hover, green outline on selected, progress status during export
  • Select Mode controls — checkbox list with Select All / Clear, plus direct click-on-page toggle
  • native / js badges — instantly see which dropdowns are standard HTML vs framework-rendered
  • Clipboard export — copies structured plain-text output; falls back to an in-page textarea if clipboard access is denied
  • Dependent output formatting — maps child options per parent value with clear ▸ When parent = "..." labeling
  • No dependencies — pure vanilla JS, works as a bookmarklet or pasted directly into DevTools console
image image

@minanagehsalalma
Copy link
Copy Markdown
Author

minanagehsalalma commented Mar 12, 2026

A more versatile version that handles complex react dropmenus :

@minanagehsalalma
Copy link
Copy Markdown
Author

minanagehsalalma commented Mar 12, 2026

(function(){
  const OID = '__ddx__';
  const VERSION = '2.3.0';
  const SHOW_VALUES = true;
  const MAX_VIRTUAL_SCROLL_STEPS = 28;
  const SCAN_STATUS_TRIM = 34;

  const existing = document.getElementById(OID);
  if (existing && typeof existing.__cleanup === 'function') {
    existing.__cleanup();
    return;
  }

  const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
  const norm = value => String(value || '').replace(/\s+/g, ' ').trim();
  const quoted = value => JSON.stringify(String(value));
  const escAttr = value => String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
  const cssEsc = value => {
    try {
      if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(String(value));
    } catch (_) {}
    return String(value).replace(/[^a-zA-Z0-9_-]/g, ch => `\\${ch}`);
  };

  let box = null;
  let scanCache = { triggers: [], inputs: [] };
  let labelCache = new WeakMap();
  let docOrder = new WeakMap();

  const TRIG = [
    'select',
    '[role="combobox"]',
    'button[aria-haspopup]',
    'button[aria-expanded]',
    'button[data-bs-toggle="dropdown"]',
    'button[class*="dropdown-toggle"]',
    '[aria-controls][aria-expanded]',
    '[role="button"][aria-haspopup]',
    '[role="button"][aria-expanded]',
    '[class*="dropdown-toggle"]',
    '[class*="select"]',
    '[class*="combobox"]',
    '[data-slot*="trigger"]',
    '[data-slot*="select"]',
    '[data-testid*="select"]',
    '[data-testid*="dropdown-trigger"]'
  ];

  const POP = [
    '[role="listbox"]',
    '[role="menu"]',
    '[data-radix-popper-content-wrapper]',
    '[data-state="open"]',
    '[data-headlessui-state="open"]',
    '[data-slot*="content"]',
    '[class*="popover"]',
    '[class*="dropdown-menu"]',
    '[class*="select-menu"]'
  ].join(',');

  const OPT = [
    '[role="option"]',
    '[role="menuitem"]',
    '[data-value]',
    'li[aria-selected]',
    '[data-radix-collection-item]',
    '[data-slot*="option"]',
    '[data-testid*="option"]'
  ].join(',');

  const APP_DATA_KEYS = ['__NEXT_DATA__', '__INITIAL_STATE__', '__PRELOADED_STATE__', '__APOLLO_STATE__', '__NUXT__'];
  const LABEL_KEYS = /^(label|question|title|name|text|prompt|fieldLabel|displayName|legend)$/i;
  const OPTION_KEYS = /^(options|choices|answers|values|items|allowedValues|enum|selections)$/i;
  const STOP_WORDS = new Set(['a', 'an', 'and', 'are', 'at', 'be', 'by', 'do', 'else', 'for', 'how', 'if', 'in', 'is', 'it', 'of', 'on', 'or', 'per', 'the', 'to', 'us', 'we', 'what', 'when', 'which', 'with', 'would', 'you', 'your']);
  const appOptionCache = new Map();

  const ql = (sel, root = document) => {
    try {
      return Array.from(root.querySelectorAll(sel));
    } catch (_) {
      return [];
    }
  };

  function walkRoots(visitor) {
    const seen = new Set();
    const walk = root => {
      if (!root || seen.has(root)) return;
      seen.add(root);
      visitor(root);
      const base = root instanceof Document ? root.documentElement : root;
      if (!base) return;
      const nodes = [base, ...ql('*', base)];
      for (const el of nodes) {
        if (!el || el === box || (box && box.contains(el))) continue;
        if (el.shadowRoot) walk(el.shadowRoot);
        if (el.tagName === 'IFRAME') {
          try {
            const frameDoc = el.contentDocument;
            if (frameDoc && frameDoc.documentElement) walk(frameDoc);
          } catch (_) {}
        }
      }
    };
    walk(document);
  }

  function q(sel, root) {
    if (root) return ql(sel, root);
    const out = [];
    const seen = new Set();
    walkRoots(searchRoot => {
      ql(sel, searchRoot).forEach(el => {
        if (!seen.has(el)) {
          seen.add(el);
          out.push(el);
        }
      });
    });
    return out;
  }

  function ownerDoc(el) {
    return (el && el.ownerDocument) || document;
  }

  function ownerWin(el) {
    const doc = ownerDoc(el);
    return doc.defaultView || window;
  }

  function refreshDocOrder() {
    docOrder = new WeakMap();
    let index = 0;
    walkRoots(root => {
      const doc = root instanceof Document ? root : root.ownerDocument;
      if (doc && !docOrder.has(doc)) docOrder.set(doc, index++);
    });
  }

  function order(a, b) {
    if (a === b) return 0;
    const da = docOrder.get(ownerDoc(a)) || 0;
    const db = docOrder.get(ownerDoc(b)) || 0;
    if (da !== db) return da - db;
    try {
      return a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
    } catch (_) {
      return 0;
    }
  }

  function vis(el) {
    if (!el || !el.isConnected) return false;
    try {
      const styles = ownerWin(el).getComputedStyle(el);
      const rect = el.getBoundingClientRect();
      return styles.display !== 'none' && styles.visibility !== 'hidden' && +styles.opacity !== 0 && rect.width > 0 && rect.height > 0 && el.getClientRects().length > 0;
    } catch (_) {
      return false;
    }
  }

  function txt(el) {
    return norm(el && (el.innerText || el.textContent));
  }

  function area(el) {
    const rect = el.getBoundingClientRect();
    return rect.width * rect.height;
  }

  function parentLike(node) {
    if (!node) return null;
    if (node.parentElement) return node.parentElement;
    try {
      const root = node.getRootNode && node.getRootNode();
      if (root instanceof ShadowRoot) return root.host || null;
    } catch (_) {}
    return null;
  }

  function ancestorChain(el, depth = 6) {
    const out = [];
    let cur = el;
    for (let i = 0; i < depth; i += 1) {
      cur = parentLike(cur);
      if (!cur) break;
      out.push(cur);
    }
    return out;
  }

  function closestMatch(el, selector) {
    let cur = el;
    while (cur) {
      try {
        if (cur.matches && cur.matches(selector)) return cur;
      } catch (_) {}
      cur = parentLike(cur);
    }
    return null;
  }

  function deepGetById(id, preferredDoc) {
    if (!id) return null;
    const findInRoot = root => {
      try {
        if (root instanceof Document) return root.getElementById(id);
        return root.querySelector(`#${cssEsc(id)}`) || root.querySelector(`[id="${escAttr(id)}"]`);
      } catch (_) {
        return null;
      }
    };
    if (preferredDoc) {
      const direct = findInRoot(preferredDoc);
      if (direct) return direct;
    }
    let found = null;
    walkRoots(root => {
      if (found) return;
      const hit = findInRoot(root);
      if (hit) found = hit;
    });
    return found;
  }

  function fallbackLabel(el) {
    const rect = el.getBoundingClientRect();
    const name = norm(el.getAttribute && (el.getAttribute('name') || el.getAttribute('id') || ''));
    const tag = (el.tagName || 'Field').toLowerCase();
    return name || `${tag} @ ${Math.round(rect.top)},${Math.round(rect.left)}`;
  }

  function reactLike(el) {
    if (!el) return false;
    const raw = `${el.className || ''} ${el.id || ''}`.toLowerCase();
    return raw.includes('react-select') || /^react-select-\d+/.test(String(el.id || ''));
  }

  function badTrigger(el) {
    if (!el) return true;
    const role = String(el.getAttribute('role') || '').toLowerCase();
    const raw = `${el.className || ''} ${el.id || ''}`.toLowerCase();
    if (role === 'option' || role === 'menuitem') return true;
    if (/^react-select-\d+-(option|group|listbox|placeholder|input|value)/.test(String(el.id || ''))) return true;
    if (reactLike(el) && /(placeholder|value-container|single-value|input-container|menu-list|indicator|group)/.test(raw) && !/control/.test(raw)) return true;
    if (/(^|\s)(option|menu-item|dropdown-item)(\s|$)/.test(raw) && !/control|trigger/.test(raw)) return true;
    return false;
  }

  function normalizeTrigger(el) {
    if (!el) return el;
    if (el.tagName === 'SELECT') return el;
    const shell = closestMatch(el, '.react-select,[class*="react-select"],[id^="dropdown"]');
    if (badTrigger(el) || shell) {
      const control = closestMatch(el, '.react-select__control,[class*="__control"],[class*="-control"]');
      if (control && vis(control)) return control;
      if (shell) {
        const pick = ql('.react-select__control,[class*="__control"],[class*="-control"],[role="combobox"],button[aria-haspopup],button[aria-expanded]', shell).find(vis);
        if (pick) return pick;
        if (vis(shell)) return shell;
      }
    }
    return el;
  }

  function labelFor(el) {
    const cached = labelCache.get(el);
    if (cached) return cached;

    const doc = ownerDoc(el);
    const aria = norm(el.getAttribute('aria-label'));
    if (aria) {
      labelCache.set(el, aria);
      return aria;
    }

    const labelledBy = norm(el.getAttribute('aria-labelledby'));
    if (labelledBy) {
      const text = labelledBy
        .split(/\s+/)
        .map(id => doc.getElementById(id) || doc.querySelector(`[id="${escAttr(id)}"]`))
        .filter(Boolean)
        .map(txt)
        .filter(Boolean)
        .join(' ');
      if (text) {
        labelCache.set(el, text);
        return text;
      }
    }

    if (el.labels && el.labels[0] && txt(el.labels[0])) {
      const label = txt(el.labels[0]);
      labelCache.set(el, label);
      return label;
    }

    if (el.id) {
      const explicit = ql('label', doc).find(node => node.htmlFor === el.id);
      if (explicit && txt(explicit)) {
        const label = txt(explicit);
        labelCache.set(el, label);
        return label;
      }
    }

    const wrap = closestMatch(el, 'label');
    if (wrap) {
      const label = txt(wrap).replace(txt(el), '').trim();
      if (label) {
        labelCache.set(el, label);
        return label;
      }
    }

    const rect = el.getBoundingClientRect();
    let best = '';
    let bestScore = -1;
    let parent = parentLike(el);
    for (let depth = 0; depth < 5 && parent; depth += 1, parent = parentLike(parent)) {
      ql('label,legend,p,span,div,strong,h1,h2,h3,h4,h5,h6', parent).forEach(candidate => {
        if (candidate === el || candidate.contains(el) || el.contains(candidate) || !vis(candidate)) return;
        const text = txt(candidate);
        if (!text || text.length > 160 || /^(select|choose|submit|continue|next|back)$/i.test(text)) return;
        const cRect = candidate.getBoundingClientRect();
        const overlap = Math.min(rect.right, cRect.right) - Math.max(rect.left, cRect.left);
        const gap = rect.top - cRect.bottom;
        const beside = cRect.left < rect.left && Math.abs(cRect.top - rect.top) < 22;
        const above = gap >= -8 && gap < 96 && overlap > 18;
        if (!(above || beside)) return;
        let score = 80 - depth * 10 - Math.min(Math.abs(gap), 60) + Math.min(overlap, 180) / 8;
        if (/LABEL|LEGEND|H\d/.test(candidate.tagName)) score += 18;
        if (text.endsWith('?')) score += 8;
        if (score > bestScore) {
          best = text;
          bestScore = score;
        }
      });
      if (best) break;
    }

    const raw = rawAttrs(el);
    const selfText = txt(el);
    if (/\bdropdown-toggle-split\b/.test(raw) && !selfText) {
      const group = parentLike(el);
      const siblingText = group
        ? ql('button,a,[role="button"]', group)
            .filter(node => node !== el)
            .map(txt)
            .find(text => text && text.length <= 120 && !/^(toggle dropdown|copy|search|result|live editor)$/i.test(text))
        : '';
      if (siblingText) {
        const label = `${siblingText} (toggle)`;
        labelCache.set(el, label);
        return label;
      }
    }
    if ((/\bdropdown-toggle\b/.test(raw) || (el.getAttribute('data-bs-toggle') || '').toLowerCase() === 'dropdown') && selfText && !placeholderLike(selfText) && selfText.length <= 120) {
      labelCache.set(el, selfText);
      return selfText;
    }

    const label = best || norm(el.getAttribute('placeholder')) || norm(el.getAttribute('name')) || norm(el.id) || selfText.slice(0, 80) || fallbackLabel(el);
    labelCache.set(el, label);
    return label;
  }

  function nearbySelect(el) {
    if (!el) return null;
    if (el.tagName === 'SELECT') return el;
    const target = el.getBoundingClientRect();
    const scopes = [el, ...ancestorChain(el, 5)];
    for (const scope of scopes) {
      const picks = ql('select', scope)
        .filter(node => node.options)
        .sort((a, b) => Math.abs(a.getBoundingClientRect().top - target.top) - Math.abs(b.getBoundingClientRect().top - target.top));
      if (picks[0]) return picks[0];
    }
    return null;
  }

  function rawAttrs(el) {
    return `${el.className || ''} ${el.id || ''} ${Array.from(el.attributes || []).map(attr => `${attr.name} ${attr.value}`).join(' ')}`.toLowerCase();
  }

  function classHints(el) {
    const raw = rawAttrs(el);
    let score = 0;
    if (/dropdown-toggle|react-select|select|combobox|listbox|trigger|menu-button/.test(raw)) score += 18;
    if (/option|item|dropdown-menu|menu__item/.test(raw)) score -= 8;
    return score;
  }

  function disabledish(el) {
    if (!el) return false;
    const raw = `${el.className || ''} ${el.getAttribute('aria-disabled') || ''} ${el.getAttribute('data-disabled') || ''}`.toLowerCase();
    return !!el.disabled || el.getAttribute('aria-disabled') === 'true' || raw.includes('is-disabled') || raw.includes('disabled');
  }

  function popupTarget(el) {
    const ids = [el.getAttribute('aria-controls'), el.getAttribute('aria-owns')]
      .filter(Boolean)
      .flatMap(value => norm(value).split(/\s+/).filter(Boolean));
    for (const id of ids) {
      const hit = deepGetById(id, ownerDoc(el));
      if (!hit) continue;
      const raw = rawAttrs(hit);
      try {
        if ((hit.matches && hit.matches(POP)) || /dropdown-menu|listbox|popover|menu/.test(raw)) return hit;
      } catch (_) {
        if (/dropdown-menu|listbox|popover|menu/.test(raw)) return hit;
      }
    }
    return null;
  }

  function ancestorRaw(el, depth = 7) {
    return [el, ...ancestorChain(el, depth)].map(node => rawAttrs(node)).join(' ');
  }

  function chromeContext(el) {
    if (!el) return false;
    if (closestMatch(el, 'nav,[role="navigation"],header,footer,aside,.navbar,.theme-doc-sidebar-container,.tableOfContents_bqdL,.DocSearch,.breadcrumbs,.pagination')) return true;
    const raw = ancestorRaw(el, 8);
    return /navbar|theme-doc-sidebar|docsidebar|sidebar|tableofcontents|table-of-contents|docsearch|breadcrumb|pagination|skiptocontent|backtotop|colormode|navbarsearchcontainer|menu__|footer|header/.test(raw);
  }

  function editorContext(el) {
    if (!el) return false;
    if (closestMatch(el, 'pre,code,.theme-code-block,[class*="codeBlock"],[class*="playgroundEditor"],[class*="editorToolbar"],[class*="copyButton"],.buttonGroup_wSGZ,.npm__react-simple-code-editor__textarea,.CodeMirror,.monaco-editor,.ace_editor')) return true;
    const raw = ancestorRaw(el, 8);
    return /playgroundeditor|editortoolbar|copybutton|codeblock|theme-code-block|simple-code-editor|codemirror|monaco|ace_editor|buttongroup_wsgz/.test(raw);
  }

  function insidePopupContent(el) {
    const popup = closestMatch(el, '[role="menu"],[role="listbox"],.dropdown-menu,[class*="dropdown-menu"],[class*="popover"],[class*="select-menu"]');
    return !!popup && popup !== el;
  }

  function weakButtonText(text) {
    return /^(search(?:\s*\(.*\))?|copy(?:\s+code(?:\s+to\s+clipboard)?)?|result|live editor|toggle navigation bar|switch between dark and light mode.*|back to top)$/i.test(norm(text));
  }

  function strongTriggerSignal(el) {
    if (!el) return false;
    const raw = rawAttrs(el);
    const role = norm(el.getAttribute('role')).toLowerCase();
    const dataToggle = String(el.getAttribute('data-bs-toggle') || '').toLowerCase();
    if (el.tagName === 'SELECT') return true;
    if (role === 'combobox') return true;
    if (el.hasAttribute('aria-haspopup')) return true;
    if (dataToggle === 'dropdown') return true;
    if (/\bdropdown-toggle\b/.test(raw)) return true;
    if (reactLike(el)) return true;
    if (!!nearbySelect(el)) return true;
    if (popupTarget(el)) return true;
    if (el.hasAttribute('aria-expanded') && el.tagName !== 'A' && (el.matches('button,[role="button"],[role="combobox"],input[readonly],div[tabindex]') || classHints(el) > 12)) return true;
    return false;
  }

  function selectish(el) {
    if (!el) return false;
    const role = norm(el.getAttribute('role')).toLowerCase();
    return el.tagName === 'SELECT' || role === 'combobox' || strongTriggerSignal(el) || classHints(el) > 0 || reactLike(el);
  }

  function placeholderLike(text) {
    return /^(select|choose|pick)(\.\.\.|\u2026)?$/i.test(text) || /^(select|choose|pick)\b/i.test(text);
  }

  function fieldish(el, text) {
    const rect = el.getBoundingClientRect();
    const styles = ownerWin(el).getComputedStyle(el);
    const border = ['Top', 'Right', 'Bottom', 'Left'].some(side => parseFloat(styles[`border${side}Width`]) > 0);
    const hint = classHints(el);
    const hasChevron = !!el.querySelector('svg,[class*="chevron"],[class*="caret"],[data-icon],[data-testid*="chevron"]');
    const hasNative = !!nearbySelect(el);
    const hasHidden = !!el.querySelector('input[type="hidden"],select');
    const signal = (placeholderLike(text) ? 1 : 0) + (hasChevron ? 1 : 0) + (hasNative ? 1 : 0) + (hasHidden ? 1 : 0) + (hint > 0 ? 1 : 0);
    let score = 0;

    if (rect.width >= 180 && rect.width <= 1200 && rect.height >= 30 && rect.height <= 72) score += 28;
    else if (rect.width >= 140 && rect.height >= 28 && rect.height <= 96) score += 12;
    else if (rect.height > 120) score -= 40;

    if (border) score += 12;
    if (parseFloat(styles.borderRadius || '0') >= 4) score += 6;
    if (styles.cursor === 'pointer') score += 8;
    if (hasChevron) score += 14;
    if (placeholderLike(text)) score += 30;
    if (hasNative) score += 16;
    if (hasHidden) score += 10;
    const label = labelFor(el);
    if (label && label !== text) score += 12;
    if (el.children.length > 0 && el.children.length <= 6) score += 4;
    if (ql('button,a,input:not([type="hidden"]),textarea,[contenteditable="true"]', el).length > 1) score -= 24;
    if (text && text.length > 80) score -= 16;

    return score + hint - (signal ? 0 : 35);
  }

  function score(el) {
    el = normalizeTrigger(el);
    if (!el || !vis(el) || badTrigger(el) || closestMatch(el, `#${OID}`)) return -1;
    const disabled = disabledish(el);
    const role = norm(el.getAttribute('role')).toLowerCase();
    const text = txt(el);
    const raw = rawAttrs(el);
    const strong = strongTriggerSignal(el);
    if (disabled && !selectish(el)) return -1;
    if (insidePopupContent(el) && !strong) return -1;
    if (editorContext(el) && !strong) return -1;
    if (chromeContext(el) && !strong) return -1;
    if (el.tagName === 'A' && role === 'button' && !strong) return -1;
    if ((weakButtonText(text) || weakButtonText(el.getAttribute('aria-label') || '')) && !strong) return -1;
    if (el.tagName === 'SELECT') return 220 + (disabled ? 12 : 0);
    if (el.tagName === 'INPUT' && !el.readOnly && el.type !== 'hidden') return -1;
    const rect = el.getBoundingClientRect();
    if (rect.height > 140) return -1;
    let s = fieldish(el, text);
    if (role === 'combobox') s += 85;
    if (role === 'button') s += 18;
    if (el.hasAttribute('aria-haspopup')) s += 35;
    if (el.hasAttribute('aria-expanded')) s += 25;
    if (popupTarget(el)) s += 20;
    if (el.getAttribute('data-bs-toggle') === 'dropdown') s += 28;
    if (/\bdropdown-toggle\b/.test(raw)) s += 26;
    if (el.hasAttribute('aria-controls') || el.hasAttribute('aria-owns')) s += 16;
    if (el.tagName === 'BUTTON') s += 20;
    if (el.tagName === 'INPUT' && el.readOnly) s += 16;
    if (el.tagName === 'DIV' && el.tabIndex >= 0) s += 10;
    if (rect.width < 90 || rect.height < 24) s -= 18;
    if (placeholderLike(text)) s += 18;
    if (disabled) s += 14;
    if (!strong) s -= 18;
    if (/^(submit|continue|next|back|close|cancel|save)$/i.test(text)) s -= 70;
    return s;
  }

  function collectTriggers() {
    const candidates = [];
    const seenRaw = new Set();
    const seenNormalized = new Set();
    const strictSel = TRIG.join(',');
    const broadSel = [...TRIG, '[class*="dropdown"]', '[data-testid*="dropdown"]', 'div[tabindex]', 'input[readonly]', 'button', '[role="button"]'].join(',');
    const passes = [
      { sel: strictSel, min: 54 },
      { sel: broadSel, min: 72, gate: () => candidates.length < 8 }
    ];

    passes.forEach(pass => {
      if (pass.gate && !pass.gate()) return;
      q(pass.sel).forEach(node => {
        if (seenRaw.has(node)) return;
        seenRaw.add(node);
        const el = normalizeTrigger(node);
        if (!el || seenNormalized.has(el)) return;
        const s = score(el);
        if (s >= pass.min) {
          seenNormalized.add(el);
          candidates.push({ el, s });
        }
      });
    });

    candidates.sort((a, b) => b.s - a.s || area(b.el) - area(a.el));
    const keep = [];
    candidates.forEach(candidate => {
      const nested = keep.findIndex(item => item.el === candidate.el || item.el.contains(candidate.el) || candidate.el.contains(item.el));
      if (nested === -1) {
        keep.push(candidate);
        return;
      }
      const current = keep[nested];
      const preferNew = candidate.s > current.s + 6 || (candidate.s >= current.s - 2 && area(candidate.el) > area(current.el) * 1.1);
      if (preferNew) keep[nested] = candidate;
    });

    return keep.map(item => normalizeTrigger(item.el)).filter((el, idx, arr) => el && arr.indexOf(el) === idx).sort(order);
  }

  function isSensitiveInput(el) {
    if (!el || el.tagName !== 'INPUT') return false;
    const type = String(el.type || 'text').toLowerCase();
    if (type === 'password') return true;
    const autocomplete = String(el.getAttribute('autocomplete') || '').toLowerCase();
    return autocomplete.includes('password');
  }

  function isTextField(el) {
    if (!el || !vis(el) || closestMatch(el, `#${OID}`)) return false;
    if (el.getAttribute('role') === 'combobox' || el.hasAttribute('aria-haspopup')) return false;
    if (closestMatch(el, '.react-select,[class*="react-select"]')) return false;
    if (editorContext(el)) return false;
    if (el.tagName === 'TEXTAREA') return true;
    if (el.tagName !== 'INPUT') return false;
    if (isSensitiveInput(el)) return false;
    const type = String(el.type || 'text').toLowerCase();
    if (['hidden', 'button', 'submit', 'reset', 'checkbox', 'radio', 'file', 'image', 'range', 'color'].includes(type)) return false;
    return true;
  }

  function inputDetails(el) {
    const type = el.tagName === 'TEXTAREA' ? 'textarea' : String(el.getAttribute('type') || 'text').toLowerCase();
    const placeholder = norm(el.getAttribute('placeholder')) || '(none)';
    const value = norm(el.value) || '(empty)';
    const maxLength = Number.isFinite(el.maxLength) && el.maxLength >= 0 ? String(el.maxLength) : '(none)';
    return [
      `type: ${quoted(type)}`,
      `placeholder: ${quoted(placeholder)}`,
      `value: ${quoted(value)}`,
      `maxlength: ${maxLength}`
    ];
  }

  function collectInputs() {
    return q('input,textarea').filter(isTextField).sort(order);
  }

  function formatOption(label, value) {
    const text = norm(label);
    const val = norm(value);
    if (!text) return '';
    if (!SHOW_VALUES || !val || text === val) return text;
    return `${text} [value="${escAttr(val)}"]`;
  }

  function nativeOptions(sel) {
    return Array.from(sel.options || []).map(opt => ({ text: norm(opt.textContent || opt.label), value: String(opt.value ?? '') }));
  }

  function isPlaceholderOpt(opt) {
    return !opt || !opt.text || opt.value === '' || opt.value === '-1' || opt.text === '--' || opt.text === '-' || placeholderLike(opt.text);
  }

  function optionish(item) {
    return !!item && typeof item === 'object' && (Array.isArray(item.options) || item.label != null || item.value != null || item.name != null || item.text != null);
  }

  function scalarish(value) {
    return typeof value === 'string' || typeof value === 'number';
  }

  function flattenOptionData(items, out = []) {
    (items || []).forEach(item => {
      if (!item) return;
      if (Array.isArray(item.options)) {
        flattenOptionData(item.options, out);
        return;
      }
      if (typeof item !== 'object') return;
      const label = norm(item.label ?? item.text ?? item.name ?? item.value);
      const value = item.value ?? item.id ?? item.key ?? '';
      if (!label || placeholderLike(label)) return;
      out.push(formatOption(label, value));
    });
    return [...new Set(out)];
  }

  function optionArrayValue(value) {
    if (!Array.isArray(value) || !value.length) return [];
    if (value.every(optionish)) return flattenOptionData(value);
    if (value.every(scalarish)) return flattenOptionData(value.map(item => ({ label: String(item), value: String(item) })));
    return [];
  }

  function optionItem(el) {
    const text = txt(el);
    if (!text || text.length > 220 || placeholderLike(text) || /^(search|type|loading|no options)(\.\.\.|\u2026)?$/i.test(text)) return '';
    const value = norm(el.getAttribute('data-value') || el.getAttribute('value'));
    return formatOption(text, value);
  }

  function looseOpts(root) {
    const out = [];
    const seen = new Set();
    ql(`${OPT},li,[class*="option"],[class*="menu-item"],[class*="dropdown-item"]`, root).forEach(el => {
      if (!vis(el) || closestMatch(el, `#${OID}`)) return;
      const item = optionItem(el);
      if (!item || seen.has(item)) return;
      seen.add(item);
      out.push(item);
    });
    return out;
  }

  function labelWords(text) {
    return norm(text).toLowerCase().replace(/[^a-z0-9\s]+/g, ' ').split(/\s+/).filter(word => word && !STOP_WORDS.has(word));
  }

  function labelScore(a, b) {
    const aa = norm(a).toLowerCase();
    const bb = norm(b).toLowerCase();
    if (!aa || !bb) return 0;
    if (aa === bb) return 140;
    if (aa.includes(bb) || bb.includes(aa)) return 105;
    const aw = new Set(labelWords(aa));
    const bw = new Set(labelWords(bb));
    if (!aw.size || !bw.size) return 0;
    let shared = 0;
    aw.forEach(word => {
      if (bw.has(word)) shared += 1;
    });
    if (!shared) return 0;
    return Math.round((shared / Math.max(aw.size, bw.size)) * 90 + (shared / Math.min(aw.size, bw.size)) * 30);
  }

  function appDataRoots() {
    const roots = [];
    const add = value => {
      if (value && typeof value === 'object' && !roots.includes(value)) roots.push(value);
    };
    APP_DATA_KEYS.forEach(key => {
      try {
        add(window[key]);
      } catch (_) {}
    });
    const next = document.getElementById('__NEXT_DATA__');
    if (next && !roots.length) {
      try {
        add(JSON.parse(next.textContent || '{}'));
      } catch (_) {}
    }
    return roots;
  }

  function walkable(value) {
    return !!value && typeof value === 'object' && value !== window && value !== document && !(typeof Element !== 'undefined' && value instanceof Element);
  }

  function appDataOptions(label) {
    const cacheKey = norm(label);
    if (appOptionCache.has(cacheKey)) return appOptionCache.get(cacheKey);
    const roots = appDataRoots();
    if (!roots.length) {
      appOptionCache.set(cacheKey, []);
      return [];
    }

    const seen = new WeakSet();
    const queue = roots.map(value => ({ value, score: 0 }));
    let best = [];
    let bestScore = 0;
    let steps = 0;

    while (queue.length && steps < 14000) {
      steps += 1;
      const current = queue.shift();
      const node = current && current.value;
      const baseScore = (current && current.score) || 0;
      if (!walkable(node) || seen.has(node)) continue;
      seen.add(node);

      if (Array.isArray(node)) {
        const arr = optionArrayValue(node);
        if (arr.length && baseScore > bestScore) {
          best = arr;
          bestScore = baseScore;
        }
        node.slice(0, 20).forEach(child => {
          if (walkable(child)) queue.push({ value: child, score: Math.max(baseScore - 2, 0) });
        });
        continue;
      }

      const keys = Object.keys(node).slice(0, 60);
      let scoreValue = baseScore;
      keys.forEach(key => {
        const value = node[key];
        if (scalarish(value) && LABEL_KEYS.test(key)) scoreValue = Math.max(scoreValue, labelScore(label, String(value)));
      });
      keys.forEach(key => {
        const arr = OPTION_KEYS.test(key) ? optionArrayValue(node[key]) : [];
        if (arr.length && (scoreValue > bestScore || (scoreValue === bestScore && arr.length > best.length))) {
          best = arr;
          bestScore = scoreValue;
        }
      });
      keys.forEach(key => {
        if (/^(ownerDocument|document|window|parentNode|offsetParent|children|childNodes|nextSibling|previousSibling|nextElementSibling|previousElementSibling|firstChild|lastChild|_owner)$/i.test(key)) return;
        const value = node[key];
        if (walkable(value)) queue.push({ value, score: Math.max(scoreValue - 1, 0) });
      });
    }

    const result = bestScore >= 60 ? best : [];
    appOptionCache.set(cacheKey, result);
    return result;
  }

  function reactAttachments(el) {
    if (!el) return [];
    return Object.keys(el)
      .filter(key => key.startsWith('__reactProps$') || key.startsWith('__reactFiber$') || key.startsWith('__reactEventHandlers$'))
      .map(key => el[key])
      .filter(Boolean);
  }

  function reactBase(trigger) {
    const roots = [
      trigger,
      activator(trigger),
      trigger && closestMatch(trigger, '[id^="dropdown"]'),
      trigger && closestMatch(trigger, '.react-select'),
      trigger && closestMatch(trigger, '[class*="react-select"]')
    ].filter(Boolean);

    for (const root of roots) {
      const nodes = [root, ...ql('[id]', root).slice(0, 12)];
      for (const node of nodes) {
        const match = String(node.id || '').match(/^(react-select-\d+)/);
        if (match) return match[1];
      }
    }
    return '';
  }

  function matchesReactBase(obj, base) {
    if (!base || !obj || typeof obj !== 'object') return false;
    const short = base.replace('react-select-', '');
    const values = [];
    ['id', 'inputId', 'instanceId', 'instancePrefix', 'controlId', 'menuId', 'placeholderId', 'name', 'htmlFor'].forEach(key => {
      if (obj[key] != null) values.push(String(obj[key]));
    });
    ['aria-controls', 'aria-labelledby'].forEach(key => {
      if (obj[key] != null) values.push(String(obj[key]));
    });
    return values.some(value => value === base || value.startsWith(base) || value.includes(base) || value === short);
  }

  function objectContextScore(obj, label, base) {
    if (!obj || typeof obj !== 'object') return 0;
    let scoreValue = 0;
    [obj, obj.selectProps, obj.props, obj.memoizedProps, obj.pendingProps].forEach(node => {
      if (matchesReactBase(node, base)) scoreValue = Math.max(scoreValue, 90);
    });
    Object.keys(obj).slice(0, 24).forEach(key => {
      const value = obj[key];
      if (scalarish(value) && LABEL_KEYS.test(key)) scoreValue = Math.max(scoreValue, labelScore(label, String(value)) + 20);
    });
    return scoreValue;
  }

  function chooseBestOptions(best, flat, scoreValue) {
    if (!flat.length) return best;
    if (!best.items.length || scoreValue > best.score || (scoreValue === best.score && scoreValue > 0 && flat.length < best.items.length) || (scoreValue === best.score && scoreValue <= 0 && flat.length > best.items.length)) {
      best.items = flat;
      best.score = scoreValue;
    }
    return best;
  }

  function internalOptionSets(seed, label, base) {
    const queue = [{ value: seed, score: 0 }];
    const seen = new WeakSet();
    let steps = 0;
    const best = { items: [], score: -1 };

    while (queue.length && steps < 3000) {
      const current = queue.shift();
      steps += 1;
      const node = current && current.value;
      const inheritedScore = (current && current.score) || 0;
      if (!walkable(node) || seen.has(node)) continue;
      seen.add(node);
      const scoreValue = Math.max(inheritedScore, objectContextScore(node, label, base));

      if (Array.isArray(node)) {
        chooseBestOptions(best, optionArrayValue(node), inheritedScore);
        node.slice(0, 12).forEach(child => {
          if (walkable(child)) queue.push({ value: child, score: Math.max(inheritedScore - 8, 0) });
        });
        continue;
      }

      chooseBestOptions(best, optionArrayValue(node.options), scoreValue + 10);
      chooseBestOptions(best, optionArrayValue(node.selectProps && node.selectProps.options), scoreValue + 16);
      chooseBestOptions(best, optionArrayValue(node.props && node.props.options), scoreValue + 12);
      chooseBestOptions(best, optionArrayValue(node.memoizedProps && node.memoizedProps.options), scoreValue + 10);
      chooseBestOptions(best, optionArrayValue(node.pendingProps && node.pendingProps.options), scoreValue + 8);

      ['selectProps', 'props', 'memoizedProps', 'pendingProps', 'return', 'child', 'sibling', 'stateNode'].forEach(key => {
        const value = node[key];
        if (walkable(value)) queue.push({ value, score: Math.max(scoreValue - 6, 0) });
      });

      Object.keys(node).slice(0, 30).forEach(key => {
        if (key.startsWith('__')) return;
        const value = node[key];
        if (walkable(value)) queue.push({ value, score: Math.max(scoreValue - 10, 0) });
      });
    }

    return best.items.length ? [best.items] : [];
  }

  function isReactSelectLike(el) {
    if (!el) return false;
    const raw = `${el.className || ''} ${el.id || ''}`.toLowerCase();
    return raw.includes('react-select');
  }

  function internalRoots(trigger) {
    const roots = [];
    const add = el => {
      if (el && !roots.includes(el)) roots.push(el);
    };
    const act = activator(trigger);
    const shell = closestMatch(trigger, '.react-select,[class*="react-select"],[id^="dropdown"]') || (act && closestMatch(act, '.react-select,[class*="react-select"],[id^="dropdown"]'));
    const control = closestMatch(trigger, '.react-select__control,[class*="__control"],[class*="-control"]') || (act && closestMatch(act, '.react-select__control,[class*="__control"],[class*="-control"]'));
    add(trigger);
    add(act);
    add(shell);
    add(control);
    const scope = shell || control || parentLike(trigger) || ownerDoc(trigger).body;
    ql('[id^="react-select-"],.react-select__control,[class*="__control"],[class*="-control"],[class*="react-select"]', scope).slice(0, 24).forEach(add);
    const id = (trigger.id && /^react-select-\d+/.test(trigger.id) ? trigger.id : '') || (act && act.id && /^react-select-\d+/.test(act.id) ? act.id : '') || (scope && scope.querySelector && scope.querySelector('[id^="react-select-"]') && scope.querySelector('[id^="react-select-"]').id) || '';
    if (id) {
      const prefix = id.replace(/-(input|placeholder|listbox|option).*$/, '');
      ql(`[id^="${escAttr(prefix)}"]`, scope).forEach(add);
    }
    if (!shell) {
      ancestorChain(trigger, 4).forEach(add);
    }
    return roots;
  }

  function internalOptions(trigger, label) {
    const base = reactBase(trigger) || reactBase(activator(trigger));
    let best = [];
    internalRoots(trigger).forEach(root => {
      reactAttachments(root).forEach(obj => {
        const hit = internalOptionSets(obj, label, base)[0] || [];
        if (hit.length && (!best.length || hit.length < best.length)) best = hit;
      });
    });
    if (!best.length) best = appDataOptions(label);
    return best;
  }

  function safeDispatch(target, event) {
    try {
      target.dispatchEvent(event);
    } catch (_) {}
  }

  function focusish(el) {
    if (!el) return;
    try {
      el.focus({ preventScroll: true });
      return;
    } catch (_) {}
    try {
      el.focus();
    } catch (_) {}
  }

  function clickish(el) {
    if (!el) return;
    const view = ownerWin(el);
    const opts = { bubbles: true, cancelable: true, view };
    focusish(el);
    if (typeof view.PointerEvent === 'function') {
      safeDispatch(el, new view.PointerEvent('pointerdown', opts));
      safeDispatch(el, new view.PointerEvent('pointerup', opts));
    }
    safeDispatch(el, new view.MouseEvent('mousedown', opts));
    safeDispatch(el, new view.MouseEvent('mouseup', opts));
    safeDispatch(el, new view.MouseEvent('click', opts));
    try {
      el.click();
    } catch (_) {}
  }

  function keyCodeFor(key) {
    const map = {
      Enter: 13,
      Escape: 27,
      ' ': 32,
      ArrowDown: 40,
      ArrowUp: 38,
      Tab: 9
    };
    return map[key] || 0;
  }

  function keyEvent(target, key) {
    if (!target) return;
    const view = ownerWin(target);
    const code = keyCodeFor(key);
    const init = { key, code: key === ' ' ? 'Space' : key, keyCode: code, which: code, bubbles: true, cancelable: true };
    safeDispatch(target, new view.KeyboardEvent('keydown', init));
    safeDispatch(target, new view.KeyboardEvent('keyup', init));
  }

  function activator(trigger) {
    trigger = normalizeTrigger(trigger);
    if (!trigger) return null;
    if (trigger.matches && trigger.matches('button,[role="button"],[role="combobox"],input,select,textarea')) return trigger;
    const picks = ql('button,[role="button"],[role="combobox"],input,select,[class*="control"],[class*="trigger"]', trigger).filter(vis);
    if (picks[0]) return picks.sort((a, b) => score(b) - score(a) || area(b) - area(a))[0];
    return trigger;
  }

  function popups(doc) {
    const out = [];
    const seen = new Set();
    const add = el => {
      if (!el || !vis(el) || seen.has(el)) return;
      if (doc && ownerDoc(el) !== doc) return;
      seen.add(el);
      out.push(el);
    };
    q(POP).forEach(add);
    q(OPT).forEach(el => add(closestMatch(el, '[role="listbox"],[role="menu"],ul,div') || parentLike(el)));
    return out;
  }

  function optionSnapshot(doc) {
    const seen = new Set();
    popups(doc).forEach(popup => {
      looseOpts(popup).forEach(item => seen.add(item));
    });
    q(OPT).forEach(el => {
      if (doc && ownerDoc(el) !== doc) return;
      if (vis(el)) {
        const item = optionItem(el);
        if (item) seen.add(item);
      }
    });
    return seen;
  }

  function popupIdsFor(el) {
    if (!el) return [];
    return norm(`${el.getAttribute('aria-controls') || ''} ${el.getAttribute('aria-owns') || ''}`).split(/\s+/).filter(Boolean);
  }

  function popupFromAria(el) {
    for (const id of popupIdsFor(el)) {
      const node = deepGetById(id, ownerDoc(el));
      if (!node) continue;
      if (vis(node)) return node;
      const wrap = closestMatch(node, '[role="listbox"],[role="menu"],[data-state="open"],[data-headlessui-state="open"],[class*="dropdown-menu"]');
      if (wrap && vis(wrap)) return wrap;
    }
    return null;
  }

  function popupFromLabelledBy(el) {
    if (!el || !el.id) return null;
    const doc = ownerDoc(el);
    const selector = `[aria-labelledby~="${cssEsc(el.id)}"]`;
    const direct = q(selector).find(node => ownerDoc(node) === doc && vis(node));
    if (direct) return direct;
    const scoped = q('[aria-labelledby]').find(node => {
      if (ownerDoc(node) !== doc || !vis(node)) return false;
      const ids = norm(node.getAttribute('aria-labelledby')).split(/\s+/).filter(Boolean);
      return ids.includes(el.id);
    });
    if (!scoped) return null;
    if (scoped.matches && scoped.matches('[role="listbox"],[role="menu"],[class*="dropdown-menu"],[data-state="open"],[data-headlessui-state="open"]')) return scoped;
    const wrap = closestMatch(scoped, '[role="listbox"],[role="menu"],[class*="dropdown-menu"],[data-state="open"],[data-headlessui-state="open"]');
    return wrap && vis(wrap) ? wrap : scoped;
  }

  function dist(a, b) {
    const ax = a.left + a.width / 2;
    const ay = a.top + a.height / 2;
    const bx = b.left + b.width / 2;
    const by = b.top + b.height / 2;
    return Math.hypot(ax - bx, ay - by);
  }

  function popupFor(trigger, before) {
    const doc = ownerDoc(trigger);
    const direct = popupFromAria(trigger) || popupFromAria(activator(trigger)) || popupFromLabelledBy(trigger) || popupFromLabelledBy(activator(trigger));
    if (direct) return direct;
    const base = reactBase(trigger) || reactBase(activator(trigger));
    if (base) {
      const react = deepGetById(`${base}-listbox`, doc) || q(`[id^="${escAttr(base)}-listbox"]`).find(el => ownerDoc(el) === doc && vis(el));
      if (react && vis(react)) return react;
    }
    const current = popups(doc);
    let list = current.filter(popup => !(before || []).includes(popup));
    if (!list.length) list = current.length ? current : popups();
    if (!list.length) return null;
    const rect = trigger.getBoundingClientRect();
    list.sort((a, b) => dist(rect, a.getBoundingClientRect()) - dist(rect, b.getBoundingClientRect()));
    return list[0] || null;
  }

  function scrollContainers(root) {
    const nodes = [root, ...ql('*', root)];
    return nodes
      .filter(el => {
        if (!(el instanceof Element) || !vis(el)) return false;
        const styles = ownerWin(el).getComputedStyle(el);
        const overflow = `${styles.overflowY || ''} ${styles.overflow || ''}`;
        return el.scrollHeight > el.clientHeight + 16 && /(auto|scroll)/.test(overflow);
      })
      .sort((a, b) => (b.scrollHeight - b.clientHeight) - (a.scrollHeight - a.clientHeight));
  }

  async function harvestPopupOptions(root) {
    const found = new Set();
    const addCurrent = () => {
      looseOpts(root).forEach(item => found.add(item));
    };

    addCurrent();
    const scroller = scrollContainers(root)[0];
    if (scroller) {
      const start = scroller.scrollTop;
      const max = Math.max(scroller.scrollHeight - scroller.clientHeight, 0);
      let stagnant = 0;
      let lastCount = found.size;
      scroller.scrollTop = 0;
      await delay(70);
      addCurrent();
      for (let step = 0; step < MAX_VIRTUAL_SCROLL_STEPS; step += 1) {
        const ratio = MAX_VIRTUAL_SCROLL_STEPS <= 1 ? 1 : step / (MAX_VIRTUAL_SCROLL_STEPS - 1);
        scroller.scrollTop = Math.round(max * ratio);
        await delay(70);
        addCurrent();
        if (found.size === lastCount) stagnant += 1;
        else stagnant = 0;
        lastCount = found.size;
        if (ratio === 1 && stagnant >= 2) break;
      }
      scroller.scrollTop = start;
      await delay(20);
    }

    return [...found];
  }

  async function openReactSelect(trigger, base) {
    const root = normalizeTrigger(trigger);
    const doc = ownerDoc(root);
    clickish(root);
    await delay(120);
    const input = deepGetById(`${base}-input`, doc) || q(`[id^="${escAttr(base)}-input"]`).find(el => ownerDoc(el) === doc && vis(el));
    if (input) {
      focusish(input);
      keyEvent(input, 'ArrowDown');
    } else {
      keyEvent(root, 'ArrowDown');
      keyEvent(root, 'Enter');
    }
    await delay(220);
    return deepGetById(`${base}-listbox`, doc) || q(`[id^="${escAttr(base)}-listbox"]`).find(el => ownerDoc(el) === doc && vis(el)) || null;
  }

  async function openJS(trigger, before) {
    const root = normalizeTrigger(trigger);
    const act = activator(root) || root;
    const candidates = [act, root].filter((node, idx, arr) => node && arr.indexOf(node) === idx);

    for (const candidate of candidates) {
      clickish(candidate);
      await delay(120);
      let popup = popupFor(candidate, before);
      if (popup) return popup;
      keyEvent(candidate, 'ArrowDown');
      await delay(140);
      popup = popupFor(candidate, before);
      if (popup) return popup;
      keyEvent(candidate, 'Enter');
      await delay(180);
      popup = popupFor(candidate, before);
      if (popup) return popup;
      keyEvent(candidate, ' ');
      await delay(140);
      popup = popupFor(candidate, before);
      if (popup) return popup;
    }

    return popupFor(act, before) || null;
  }

  async function closeJS(trigger) {
    const root = normalizeTrigger(trigger);
    const act = activator(root) || root;
    [act, root, ownerDoc(act || root)].forEach(target => {
      if (target) keyEvent(target, 'Escape');
    });
    await delay(60);
    const body = ownerDoc(act || root).body;
    if (body) {
      try {
        body.click();
      } catch (_) {}
    }
    await delay(100);
  }

  function setNative(sel, value) {
    const view = ownerWin(sel);
    const desc = Object.getOwnPropertyDescriptor(view.HTMLSelectElement.prototype, 'value');
    if (desc && desc.set) desc.set.call(sel, value);
    else sel.value = value;
    safeDispatch(sel, new view.Event('input', { bubbles: true }));
    safeDispatch(sel, new view.Event('change', { bubbles: true }));
  }

  function keyName(obj, base) {
    const seed = base || 'Field';
    let out = seed;
    let n = 2;
    while (Object.prototype.hasOwnProperty.call(obj, out)) out = `${seed} (${n++})`;
    return out;
  }

  function activeTargets(includeInputs) {
    return [...new Set(includeInputs ? [...scanCache.triggers, ...scanCache.inputs] : [...scanCache.triggers])].sort(order);
  }

  function everyTarget() {
    return [...new Set([...scanCache.triggers, ...scanCache.inputs])].sort(order);
  }

  function refreshScan() {
    labelCache = new WeakMap();
    appOptionCache.clear();
    refreshDocOrder();
    scanCache.triggers = collectTriggers();
    scanCache.inputs = collectInputs();
  }

  function decodeSnippet(text) {
    const doc = document;
    const ta = doc.createElement('textarea');
    ta.innerHTML = String(text || '');
    return ta.value;
  }

  function snippetOptionsFromText(text) {
    const source = decodeSnippet(text);
    const out = [];
    const patterns = [
      /<Dropdown\.Item\b[^>]*>([\s\S]*?)<\/Dropdown\.Item>/gi,
      /<option\b[^>]*>([\s\S]*?)<\/option>/gi,
      /<MenuItem\b[^>]*>([\s\S]*?)<\/MenuItem>/gi,
      /label\s*:\s*['"`]([^'"`]+)['"`]/gi
    ];

    patterns.forEach(pattern => {
      let match;
      while ((match = pattern.exec(source))) {
        const clean = norm(String(match[1]).replace(/<[^>]+>/g, ' '));
        if (!clean || clean.length > 180 || placeholderLike(clean)) continue;
        if (/^(dropdown\.?item|menuitem|option)$/i.test(clean)) continue;
        out.push(clean);
      }
    });

    return [...new Set(out)];
  }

  function codeSnippetOptions(trigger) {
    const scopes = [
      closestMatch(trigger, '.playgroundContainer_TGbA,[class*="playgroundContainer"],[class*="codeBlock"],.theme-code-block'),
      ...ancestorChain(trigger, 4)
    ].filter(Boolean);
    const seen = new Set();

    for (const scope of scopes) {
      const texts = [];
      ql('textarea,[code],pre code,code,[class*="token-line"]', scope).forEach(node => {
        const text = ('value' in node && node.value) ? String(node.value) : norm(node.getAttribute && node.getAttribute('code')) || txt(node);
        if (text && text.length > 40) texts.push(text);
      });
      const combined = texts.join('\n');
      if (!combined || seen.has(combined)) continue;
      seen.add(combined);
      const opts = snippetOptionsFromText(combined);
      if (opts.length) return opts;
    }

    return [];
  }

  function allDocs() {
    const docs = [];
    const seen = new Set();
    walkRoots(root => {
      const doc = root instanceof Document ? root : root.ownerDocument;
      if (doc && !seen.has(doc)) {
        seen.add(doc);
        docs.push(doc);
      }
    });
    return docs;
  }

  async function exportTargets(targets, statusNode) {
    const out = {};
    const ordered = [...targets].sort(order).map(trigger => normalizeTrigger(trigger)).filter(Boolean);
    const records = [];
    ordered.forEach(trigger => {
      const input = isTextField(trigger);
      const rawLabel = labelFor(trigger);
      const label = keyName(out, input ? `${rawLabel} [INPUT]` : rawLabel);
      out[label] = null;
      records.push({ trigger, label, rawLabel, isInput: input });
    });

    const nativeRecords = [];
    const nativeParents = [];

    for (let i = 0; i < records.length; i += 1) {
      const record = records[i];
      const trigger = record.trigger;
      if (record.isInput) {
        out[record.label] = inputDetails(trigger);
        continue;
      }

      const sel = trigger.tagName === 'SELECT' ? trigger : nearbySelect(trigger);
      if (sel) {
        record.sel = sel;
        nativeRecords.push(record);
        if (!nativeParents.includes(sel)) nativeParents.push(sel);
        const opts = nativeOptions(sel).filter(opt => !isPlaceholderOpt(opt));
        out[record.label] = opts.length ? opts.map(opt => formatOption(opt.text, opt.value)) : null;
        continue;
      }

      statusNode.textContent = `Scanning ${i + 1}/${records.length}: "${record.label.slice(0, SCAN_STATUS_TRIM)}"...`;
      const act = activator(trigger);
      const base = reactBase(trigger) || reactBase(act);
      const likelyReact = !!base || isReactSelectLike(trigger) || isReactSelectLike(act);
      let internal = likelyReact ? internalOptions(trigger, record.rawLabel) : [];
      if (!internal.length) internal = appDataOptions(record.rawLabel);
      const preferDirect = likelyReact && (disabledish(trigger) || disabledish(act));
      if (preferDirect && internal.length) {
        out[record.label] = internal;
        continue;
      }

      try {
        let opts = [];
        if (likelyReact && base) {
          const list = await openReactSelect(trigger, base);
          opts = list ? await harvestPopupOptions(list) : [];
        }
        if (!opts.length) {
          const beforePopups = popups(ownerDoc(trigger));
          let popup = await openJS(trigger, beforePopups);
          opts = popup ? await harvestPopupOptions(popup) : [];
          if (!opts.length) {
            await delay(250);
            popup = popup || popupFor(normalizeTrigger(trigger), beforePopups);
            opts = popup ? await harvestPopupOptions(popup) : [];
          }
        }
        if (!opts.length && internal.length) opts = internal;
        if (!opts.length) opts = appDataOptions(record.rawLabel);
        if (!opts.length) opts = codeSnippetOptions(trigger);
        out[record.label] = [...new Set((opts || []).filter(Boolean))];
        await closeJS(trigger);
      } catch (err) {
        console.error('Field Exporter: JS dropdown scan failed', err);
        const fallback = internal.length ? internal : appDataOptions(record.rawLabel);
        out[record.label] = fallback.length ? fallback : codeSnippetOptions(trigger);
      }
    }

    for (const record of nativeRecords.filter(item => out[item.label] === null)) {
      const mapping = {};
      let foundParent = false;
      for (const parentSel of nativeParents) {
        if (parentSel === record.sel) continue;
        const parentOpts = nativeOptions(parentSel).filter(opt => !isPlaceholderOpt(opt));
        if (!parentOpts.length) continue;
        const savedValue = parentSel.value;
        let anyChild = false;
        for (const pOpt of parentOpts) {
          statusNode.textContent = `Probing "${record.label.slice(0, SCAN_STATUS_TRIM)}" <- "${pOpt.text}"...`;
          setNative(parentSel, pOpt.value);
          await delay(320);
          const childOpts = nativeOptions(record.sel).filter(opt => !isPlaceholderOpt(opt));
          if (childOpts.length) {
            mapping[pOpt.text] = childOpts.map(opt => formatOption(opt.text, opt.value));
            anyChild = true;
          }
        }
        setNative(parentSel, savedValue);
        await delay(160);
        if (anyChild) {
          foundParent = true;
          break;
        }
      }
      out[record.label] = foundParent && Object.keys(mapping).length ? mapping : [];
    }

    records.forEach(record => {
      if (out[record.label] === null) out[record.label] = [];
    });

    return out;
  }

  function formatOutput(allFound, scannedCount, includeInputs) {
    let inputFields = 0;
    let dropdownItems = 0;
    Object.entries(allFound).forEach(([label, value]) => {
      if (value == null) return;
      if (/\s\[INPUT\]$/.test(label)) {
        inputFields += 1;
        return;
      }
      if (Array.isArray(value)) dropdownItems += value.length;
      else Object.values(value).forEach(arr => {
        dropdownItems += arr.length;
      });
    });

    const title = includeInputs ? 'FIELD EXPORT' : 'DROPDOWN EXPORT';
    const summary = includeInputs ? `Scanned: ${scannedCount} | Inputs: ${inputFields} | Dropdown options: ${dropdownItems}` : `Scanned: ${scannedCount} | Found: ${dropdownItems} options`;
    let text = `=== ${title} ===\n${summary}\n${'='.repeat(40)}\n\n`;

    Object.entries(allFound).forEach(([label, value]) => {
      if (value == null) return;
      text += `${label}\n`;
      if (Array.isArray(value)) {
        if (!value.length) text += '   (no items)\n';
        else value.forEach((item, index) => {
          text += `   ${index + 1}. ${item}\n`;
        });
      } else {
        const entries = Object.entries(value);
        if (!entries.length) {
          text += '   (no items)\n';
        } else {
          text += '   Options depend on parent selection:\n';
          entries.forEach(([parentOpt, items]) => {
            text += `   When parent = ${quoted(parentOpt)}:\n`;
            items.forEach((item, index) => {
              text += `      ${index + 1}. ${item}\n`;
            });
          });
        }
      }
      text += '\n';
    });

    return { text, total: includeInputs ? inputFields + dropdownItems : dropdownItems };
  }

  const orig = new WeakMap();
  let selecting = false;
  let exporting = false;
  let hover = null;
  let includeInputs = false;
  let selectionDocs = [];

  const hl = (el, color) => {
    if (!orig.has(el)) {
      orig.set(el, { outline: el.style.outline, outlineOffset: el.style.outlineOffset, boxShadow: el.style.boxShadow });
    }
    el.style.outline = `2px solid ${color}`;
    el.style.outlineOffset = '2px';
    el.style.boxShadow = `0 0 0 4px ${color}33`;
  };

  const unhl = el => {
    const state = orig.get(el);
    if (!state) return;
    el.style.outline = state.outline;
    el.style.outlineOffset = state.outlineOffset;
    el.style.boxShadow = state.boxShadow;
  };

  function refreshHighlights(chosen) {
    const active = new Set(activeTargets(includeInputs));
    everyTarget().forEach(el => {
      if (chosen.has(el) && active.has(el)) hl(el, '#22c55e');
      else unhl(el);
    });
  }

  refreshScan();

  box = document.createElement('div');
  box.id = OID;
  box.style.cssText = 'position:fixed;top:16px;right:16px;z-index:2147483647;width:360px;max-height:84vh;background:#111827;color:#e5e7eb;border:1px solid #374151;border-radius:12px;font:13px/1.45 system-ui,sans-serif;box-shadow:0 16px 48px rgba(0,0,0,.55);display:flex;flex-direction:column;user-select:none;';
  box.innerHTML = `
    <div id="h" style="padding:12px 14px;background:#0f172a;border-radius:12px 12px 0 0;cursor:move;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #1f2937;gap:8px;">
      <div style="display:flex;flex-direction:column;gap:2px;min-width:0;">
        <span style="font-weight:700;font-size:14px;">Field Exporter Pro</span>
        <span style="font-size:11px;color:#94a3b8;">v${VERSION}</span>
      </div>
      <div style="display:flex;align-items:center;gap:6px;flex-shrink:0;">
        <button id="rs" title="Rescan page" style="background:#1f2937;border:1px solid #374151;color:#cbd5e1;font-size:13px;cursor:pointer;line-height:1;padding:5px 8px;border-radius:7px;">Rescan</button>
        <button id="x" title="Close" style="background:none;border:none;color:#9ca3af;font-size:18px;cursor:pointer;line-height:1;padding:0 2px;">&times;</button>
      </div>
    </div>
    <div style="display:flex;border-bottom:1px solid #1f2937;">
      <button id="ma" style="flex:1;padding:9px 0;border:none;background:#22c55e;color:#052e16;font-weight:700;cursor:pointer;font-size:12px;">Export All</button>
      <button id="ms" style="flex:1;padding:9px 0;border:none;background:#1f2937;color:#94a3b8;font-weight:600;cursor:pointer;font-size:12px;">Select Mode</button>
    </div>
    <label style="display:flex;align-items:center;gap:8px;padding:8px 14px;border-bottom:1px solid #1f2937;font-size:12px;color:#d1d5db;cursor:pointer;">
      <input id="ii" type="checkbox" style="accent-color:#22c55e;cursor:pointer;">
      <span>Include input fields</span>
    </label>
    <div id="pa" style="padding:14px;display:flex;flex-direction:column;gap:10px;">
      <div id="info" style="font-size:12px;color:#9ca3af;line-height:1.6;"></div>
      <button id="ea" data-label="Export All Dropdowns" style="padding:10px;border-radius:8px;border:none;background:#22c55e;color:#052e16;font-weight:700;cursor:pointer;font-size:13px;">Export All Dropdowns</button>
    </div>
    <div id="ps" style="display:none;flex-direction:column;flex:1;overflow:hidden;">
      <div style="padding:8px 14px;background:#0b3b62;font-size:12px;color:#bfdbfe;"><b>Hover</b> to highlight. <b>Click</b> a field on the page to toggle it. Press <b>Esc</b> to exit selection mode.</div>
      <div style="padding:8px 14px;display:flex;gap:8px;border-bottom:1px solid #1f2937;">
        <button id="sa" style="flex:1;padding:5px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;cursor:pointer;font-size:11px;">All</button>
        <button id="sc" style="flex:1;padding:5px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;cursor:pointer;font-size:11px;">Clear</button>
      </div>
      <div id="list" style="overflow-y:auto;flex:1;padding:8px 6px;min-height:80px;max-height:calc(84vh - 290px);"></div>
      <div style="padding:10px 14px;border-top:1px solid #1f2937;">
        <button id="es" data-label="Export Selected" style="width:100%;padding:9px;border-radius:8px;border:none;background:#22c55e;color:#052e16;font-weight:700;cursor:pointer;font-size:13px;">Export Selected</button>
      </div>
    </div>
    <div id="st" style="padding:4px 14px 10px;font-size:11px;color:#6b7280;text-align:center;min-height:20px;"></div>
  `;
  (document.body || document.documentElement).appendChild(box);

  const st = box.querySelector('#st');
  const list = box.querySelector('#list');
  const pa = box.querySelector('#pa');
  const ps = box.querySelector('#ps');
  const ma = box.querySelector('#ma');
  const ms = box.querySelector('#ms');
  const ea = box.querySelector('#ea');
  const es = box.querySelector('#es');
  const ii = box.querySelector('#ii');
  const info = box.querySelector('#info');
  const chosen = new Set();
  ii.checked = includeInputs;

  function targetDisabled(el) {
    return disabledish(el) || disabledish(activator(el)) || disabledish(nearbySelect(el));
  }

  function refreshInfo() {
    const count = activeTargets(includeInputs).length;
    info.innerHTML = `Exports <b style="color:#e5e7eb;">all ${count} ${includeInputs ? 'fields' : 'dropdowns'}</b> found on this page.<br>${includeInputs ? 'Supports dropdowns, text inputs, textareas, shadow DOM, iframes, hidden selects, generic field wrappers, React/portal menus, and virtualized option lists.' : 'Supports native selects, shadow DOM, iframes, hidden selects, generic field wrappers, React/portal menus, and virtualized option lists.'}`;
    ea.dataset.label = includeInputs ? 'Export All Fields' : 'Export All Dropdowns';
    if (!ea.disabled) ea.textContent = ea.dataset.label;
  }

  function activeSelected() {
    const active = new Set(activeTargets(includeInputs));
    return [...chosen].filter(el => active.has(el));
  }

  function pruneChosen() {
    const active = new Set(everyTarget());
    [...chosen].forEach(el => {
      if (!active.has(el) || !el.isConnected) chosen.delete(el);
    });
  }

  function status() {
    const selectedCount = activeSelected().length;
    if (ps.style.display !== 'none') st.textContent = `${selectedCount} / ${activeTargets(includeInputs).length} selected`;
    es.style.opacity = selectedCount ? '1' : '0.45';
    es.disabled = !selectedCount;
  }

  function build() {
    pruneChosen();
    const current = activeTargets(includeInputs);
    list.innerHTML = '';
    if (!current.length) {
      list.innerHTML = `<div style="padding:16px;color:#6b7280;text-align:center;">${includeInputs ? 'No matching fields detected.' : 'No dropdowns detected.'}</div>`;
      status();
      return;
    }

    current.forEach(el => {
      const row = document.createElement('div');
      const cb = document.createElement('input');
      const label = document.createElement('span');
      const tag = document.createElement('span');
      const fieldLabel = labelFor(el);
      const kind = isTextField(el) ? 'input' : (el.tagName === 'SELECT' ? 'native' : 'js');
      const disabled = targetDisabled(el);
      row.style.cssText = 'display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:7px;cursor:pointer;transition:background .15s;margin-bottom:2px;';
      cb.type = 'checkbox';
      cb.style.cssText = 'width:15px;height:15px;accent-color:#22c55e;cursor:pointer;flex-shrink:0;';
      label.textContent = fieldLabel.length > 46 ? `${fieldLabel.slice(0, 46)}...` : fieldLabel;
      label.style.cssText = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;${disabled ? 'color:#9ca3af;' : ''}`;
      tag.textContent = kind;
      tag.style.cssText = `font-size:10px;padding:1px 5px;border-radius:4px;flex-shrink:0;background:${kind === 'native' ? '#1d4ed8' : kind === 'input' ? '#92400e' : '#7c3aed'};color:${kind === 'native' ? '#bfdbfe' : kind === 'input' ? '#fde68a' : '#ddd6fe'};`;
      const state = document.createElement('span');
      state.textContent = 'disabled';
      state.style.cssText = 'font-size:10px;padding:1px 5px;border-radius:4px;flex-shrink:0;background:#3f3f46;color:#e4e4e7;';
      const sync = () => {
        cb.checked = chosen.has(el);
        row.style.background = chosen.has(el) ? '#14532d55' : 'transparent';
      };
      const toggle = () => {
        if (chosen.has(el)) chosen.delete(el);
        else chosen.add(el);
        sync();
        refreshHighlights(chosen);
        status();
      };
      row.append(cb, label, tag);
      if (disabled) row.append(state);
      row.addEventListener('click', toggle);
      cb.addEventListener('click', event => {
        event.preventDefault();
        event.stopPropagation();
        toggle();
      });
      row.addEventListener('mouseenter', () => {
        row.style.background = chosen.has(el) ? '#14532d88' : '#1f2937';
        if (!chosen.has(el)) hl(el, '#3b82f6');
      });
      row.addEventListener('mouseleave', () => {
        row.style.background = chosen.has(el) ? '#14532d55' : 'transparent';
        if (!chosen.has(el)) unhl(el);
      });
      sync();
      list.appendChild(row);
    });

    status();
  }

  function eventTargetField(event) {
    const path = typeof event.composedPath === 'function' ? event.composedPath() : [event.target];
    return activeTargets(includeInputs).find(target => path.includes(target) || path.some(node => node instanceof Element && target.contains(node)));
  }

  function onOver(event) {
    if (!selecting || exporting || box.contains(event.target)) return;
    const el = eventTargetField(event);
    if (!el) {
      if (hover && !chosen.has(hover)) unhl(hover);
      hover = null;
      return;
    }
    if (hover && hover !== el && !chosen.has(hover)) unhl(hover);
    hover = el;
    if (!chosen.has(el)) hl(el, '#3b82f6');
  }

  function onClick(event) {
    if (!selecting || exporting || box.contains(event.target)) return;
    const el = eventTargetField(event);
    if (!el) return;
    event.preventDefault();
    event.stopImmediatePropagation();
    if (chosen.has(el)) chosen.delete(el);
    else chosen.add(el);
    refreshHighlights(chosen);
    build();
    status();
  }

  function onKeydown(event) {
    if (!selecting) return;
    if (event.key === 'Escape') {
      event.preventDefault();
      mode('all');
    }
  }

  function listeners(on) {
    if (on === selecting) return;
    const docs = on ? allDocs() : selectionDocs;
    docs.forEach(doc => {
      doc[on ? 'addEventListener' : 'removeEventListener']('mouseover', onOver, true);
      doc[on ? 'addEventListener' : 'removeEventListener']('click', onClick, true);
      doc[on ? 'addEventListener' : 'removeEventListener']('keydown', onKeydown, true);
    });
    selectionDocs = on ? docs : [];
    selecting = on;
    if (!on && hover && !chosen.has(hover)) {
      unhl(hover);
      hover = null;
    }
  }

  function rescanUI(message) {
    refreshScan();
    pruneChosen();
    if (selecting) {
      listeners(false);
      listeners(true);
    }
    refreshHighlights(chosen);
    refreshInfo();
    if (ps.style.display !== 'none') build();
    else status();
    if (message) {
      st.textContent = message;
      st.style.color = '#60a5fa';
    }
  }

  function mode(name) {
    const allMode = name === 'all';
    pa.style.display = allMode ? 'flex' : 'none';
    ps.style.display = allMode ? 'none' : 'flex';
    ma.style.background = allMode ? '#22c55e' : '#1f2937';
    ma.style.color = allMode ? '#052e16' : '#94a3b8';
    ms.style.background = allMode ? '#1f2937' : '#22c55e';
    ms.style.color = allMode ? '#94a3b8' : '#052e16';
    st.style.color = '#6b7280';
    rescanUI();
    if (allMode) {
      listeners(false);
      st.textContent = '';
    } else {
      listeners(true);
      build();
      status();
    }
  }

  async function copyText(text) {
    if (navigator.clipboard && navigator.clipboard.writeText) {
      await navigator.clipboard.writeText(text);
      return;
    }
    throw new Error('ClipboardUnavailable');
  }

  function openFallbackText(text) {
    const ta = document.createElement('textarea');
    ta.value = text;
    ta.style.cssText = 'position:fixed;top:5%;left:5%;width:90%;height:90%;z-index:2147483647;padding:12px;font:13px monospace;background:#111827;color:#e5e7eb;border:2px solid #374151;border-radius:8px;';
    document.body.appendChild(ta);
    ta.select();
    const close = document.createElement('button');
    close.textContent = 'Close';
    close.style.cssText = 'position:fixed;top:6%;right:6%;z-index:2147483647;padding:6px 14px;background:#b91c1c;color:white;border:none;border-radius:6px;cursor:pointer;';
    close.onclick = () => {
      ta.remove();
      close.remove();
    };
    document.body.appendChild(close);
  }

  async function run(which, btn) {
    exporting = true;
    listeners(false);
    st.style.color = '#6b7280';
    btn.textContent = 'Scanning...';
    btn.disabled = true;

    try {
      rescanUI();
      const selected = which === 'all' ? activeTargets(includeInputs) : activeSelected();
      if (!selected.length) {
        st.textContent = 'Nothing selected.';
        st.style.color = '#f59e0b';
        return;
      }
      const result = await exportTargets(selected, st);
      window.__ddx_lastExport = {
        version: VERSION,
        exportedAt: new Date().toISOString(),
        includeInputs,
        count: selected.length,
        results: result
      };
      const { text, total } = formatOutput(result, selected.length, includeInputs);
      try {
        await copyText(text);
        st.textContent = `Copied ${total} ${includeInputs ? 'items' : 'options'} to the clipboard. Raw data is also in window.__ddx_lastExport.`;
        st.style.color = '#22c55e';
      } catch (err) {
        if (!(err && err.name === 'NotAllowedError')) console.error('Field Exporter: clipboard export failed', err);
        openFallbackText(text);
        st.textContent = `Opened fallback text view with ${total} ${includeInputs ? 'items' : 'options'}. Raw data is also in window.__ddx_lastExport.`;
        st.style.color = '#f59e0b';
      }
    } finally {
      btn.textContent = btn.dataset.label;
      btn.disabled = false;
      exporting = false;
      if (ps.style.display !== 'none') listeners(true);
    }
  }

  box.querySelector('#x').addEventListener('click', cleanup);
  box.querySelector('#rs').addEventListener('click', () => rescanUI('Rescanned page.'));
  ma.addEventListener('click', () => mode('all'));
  ms.addEventListener('click', () => mode('sel'));
  ii.addEventListener('change', () => {
    includeInputs = ii.checked;
    refreshInfo();
    refreshHighlights(chosen);
    if (ps.style.display !== 'none') build();
    else {
      status();
      st.textContent = '';
    }
  });
  box.querySelector('#sa').addEventListener('click', () => {
    activeTargets(includeInputs).forEach(el => chosen.add(el));
    build();
    refreshHighlights(chosen);
  });
  box.querySelector('#sc').addEventListener('click', () => {
    chosen.clear();
    everyTarget().forEach(unhl);
    build();
  });
  ea.addEventListener('click', async () => {
    await run('all', ea);
  });
  es.addEventListener('click', async () => {
    if (!activeSelected().length) return;
    await run('selected', es);
  });

  const hdr = box.querySelector('#h');
  let drag = false;
  let ox = 0;
  let oy = 0;
  hdr.addEventListener('mousedown', event => {
    drag = true;
    const rect = box.getBoundingClientRect();
    ox = event.clientX - rect.left;
    oy = event.clientY - rect.top;
    event.preventDefault();
  });
  function onMove(event) {
    if (!drag) return;
    box.style.right = 'auto';
    box.style.left = `${event.clientX - ox}px`;
    box.style.top = `${event.clientY - oy}px`;
  }
  function onUp() {
    drag = false;
  }
  document.addEventListener('mousemove', onMove);
  document.addEventListener('mouseup', onUp);

  function cleanup() {
    listeners(false);
    document.removeEventListener('mousemove', onMove);
    document.removeEventListener('mouseup', onUp);
    everyTarget().forEach(unhl);
    box.remove();
  }

  box.__cleanup = cleanup;
  refreshInfo();
  mode('all');
})();

@minanagehsalalma
Copy link
Copy Markdown
Author

Debug script that was used while making the react version.

javascript:(function(){
  function isVisible(el) {
    if (!el) return false;
    const r = el.getBoundingClientRect();
    if (r.width < 2 || r.height < 2) return false;
    const s = getComputedStyle(el);
    return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0';
  }

  const report = [];

  function log(section, data) {
    report.push({ section, data });
  }

  // ── 1. All SELECT elements ─────────────────────────────────────────────────
  const nativeSelects = [...document.querySelectorAll('select')];
  log('NATIVE <select> elements', nativeSelects.map(el => ({
    id: el.id || '(no id)',
    name: el.name || '(no name)',
    class: el.className || '(no class)',
    visible: isVisible(el),
    optionCount: el.options.length,
    options: [...el.options].map(o => `"${o.text}" [value="${o.value}"]`),
    rect: el.getBoundingClientRect(),
  })));

  // ── 2. ARIA-based candidates ───────────────────────────────────────────────
  const ariaSelectors = [
    '[role="combobox"]',
    '[role="listbox"]',
    '[aria-haspopup]',
    '[aria-expanded]',
  ];
  const ariaCandidates = [];
  ariaSelectors.forEach(sel => {
    document.querySelectorAll(sel).forEach(el => {
      ariaCandidates.push({
        selector: sel,
        tag: el.tagName,
        id: el.id || '(none)',
        class: el.className?.toString().slice(0,80) || '(none)',
        role: el.getAttribute('role'),
        ariaHaspopup: el.getAttribute('aria-haspopup'),
        ariaExpanded: el.getAttribute('aria-expanded'),
        visible: isVisible(el),
        text: el.textContent.trim().replace(/\s+/g,' ').slice(0,60),
        rect: (() => { const r = el.getBoundingClientRect(); return {top:Math.round(r.top),left:Math.round(r.left),w:Math.round(r.width),h:Math.round(r.height)}; })(),
      });
    });
  });
  log('ARIA-based candidates', ariaCandidates);

  // ── 3. All buttons on the page ────────────────────────────────────────────
  const buttons = [...document.querySelectorAll('button,[role="button"]')].filter(isVisible);
  log('Visible BUTTONS', buttons.map(el => ({
    tag: el.tagName,
    id: el.id || '(none)',
    class: el.className?.toString().slice(0,80) || '(none)',
    text: el.textContent.trim().replace(/\s+/g,' ').slice(0,60),
    ariaHaspopup: el.getAttribute('aria-haspopup'),
    ariaExpanded: el.getAttribute('aria-expanded'),
    rect: (() => { const r = el.getBoundingClientRect(); return {top:Math.round(r.top),left:Math.round(r.left),w:Math.round(r.width),h:Math.round(r.height)}; })(),
  })));

  // ── 4. Field-sized elements with cursor:pointer ───────────────────────────
  const pointerEls = [];
  document.querySelectorAll('div[class],span[class],[tabindex="0"]').forEach(el => {
    if (!isVisible(el)) return;
    const r = el.getBoundingClientRect();
    if (r.width < 80 || r.height < 24 || r.height > 100) return;
    if (getComputedStyle(el).cursor !== 'pointer') return;
    pointerEls.push({
      tag: el.tagName,
      id: el.id || '(none)',
      class: el.className?.toString().slice(0,80) || '(none)',
      text: el.textContent.trim().replace(/\s+/g,' ').slice(0,60),
      rect: {top:Math.round(r.top),left:Math.round(r.left),w:Math.round(r.width),h:Math.round(r.height)},
    });
  });
  log('Field-sized cursor:pointer elements', pointerEls);

  // ── 5. Elements containing "Select…" placeholder text ────────────────────
  const placeholderEls = [];
  const placeholderRe = /^(select|choose|pick)(\.\.\.|\u2026|\s|$)/i;
  document.querySelectorAll('*').forEach(el => {
    if (!isVisible(el)) return;
    const direct = [...el.childNodes]
      .filter(n => n.nodeType === 3)
      .map(n => n.textContent.trim())
      .join(' ');
    const inner = el.querySelector('[class*="placeholder"],[class*="Placeholder"],[data-placeholder]');
    const text = (inner?.textContent || direct).trim();
    if (placeholderRe.test(text)) {
      placeholderEls.push({
        tag: el.tagName,
        id: el.id || '(none)',
        class: el.className?.toString().slice(0,80) || '(none)',
        text: el.textContent.trim().replace(/\s+/g,' ').slice(0,60),
        rect: (() => { const r = el.getBoundingClientRect(); return {top:Math.round(r.top),left:Math.round(r.left),w:Math.round(r.width),h:Math.round(r.height)}; })(),
      });
    }
  });
  log('"Select…" placeholder text elements', placeholderEls);

  // ── 6. What's currently in the DOM that looks like options ───────────────
  const OPTION_SEL = '[role="option"],[role="menuitem"],[aria-selected],[data-value],li[class*="option"],div[class*="option"],li[class*="item"]';
  const existingOptions = [...document.querySelectorAll(OPTION_SEL)].filter(isVisible);
  log('Currently visible "option-like" elements (before any clicking)', existingOptions.map(el => ({
    tag: el.tagName,
    role: el.getAttribute('role'),
    class: el.className?.toString().slice(0,60),
    text: el.textContent.trim().replace(/\s+/g,' ').slice(0,60),
  })));

  // ── Render report ─────────────────────────────────────────────────────────
  const overlay = document.createElement('div');
  overlay.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:2147483647;background:rgba(0,0,0,0.85);display:flex;align-items:center;justify-content:center;font-family:monospace;';

  const box = document.createElement('div');
  box.style.cssText = 'background:#0d1117;color:#c9d1d9;width:90vw;height:90vh;border-radius:10px;display:flex;flex-direction:column;border:1px solid #30363d;overflow:hidden;';

  const header = document.createElement('div');
  header.style.cssText = 'padding:12px 16px;background:#161b22;border-bottom:1px solid #30363d;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;';
  header.innerHTML = `<span style="font-weight:700;font-size:14px;color:#58a6ff;">🔍 Dropdown Debug Report</span><button id="__dbg_copy__" style="padding:5px 12px;background:#238636;color:white;border:none;border-radius:6px;cursor:pointer;font-size:12px;margin-right:8px;">📋 Copy JSON</button><button id="__dbg_close__" style="background:none;border:none;color:#8b949e;font-size:20px;cursor:pointer;line-height:1;">×</button>`;

  const body = document.createElement('div');
  body.style.cssText = 'overflow:auto;flex:1;padding:16px;font-size:12px;line-height:1.6;';

  report.forEach(({ section, data }) => {
    const sec = document.createElement('div');
    sec.style.cssText = 'margin-bottom:24px;';
    const title = document.createElement('div');
    title.style.cssText = 'color:#f0883e;font-weight:700;font-size:13px;margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid #21262d;';
    title.textContent = `▶ ${section} (${Array.isArray(data) ? data.length : '—'})`;
    const pre = document.createElement('pre');
    pre.style.cssText = 'margin:0;background:#161b22;padding:12px;border-radius:6px;overflow-x:auto;color:#c9d1d9;white-space:pre-wrap;word-break:break-word;';
    pre.textContent = JSON.stringify(data, null, 2);
    sec.append(title, pre); body.appendChild(sec);
  });

  box.append(header, body);
  overlay.appendChild(box);
  document.body.appendChild(overlay);

  overlay.querySelector('#__dbg_close__').onclick = () => overlay.remove();
  overlay.querySelector('#__dbg_copy__').onclick = () => {
    navigator.clipboard.writeText(JSON.stringify(report, null, 2)).then(() => {
      overlay.querySelector('#__dbg_copy__').textContent = '✅ Copied!';
      setTimeout(() => { overlay.querySelector('#__dbg_copy__')&&(overlay.querySelector('#__dbg_copy__').textContent='📋 Copy JSON'); }, 2000);
    });
  };
})();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment