diff --git a/src/components/FrameworkSelector.ts b/src/components/FrameworkSelector.ts index f8ce94b629..2b422a158f 100644 --- a/src/components/FrameworkSelector.ts +++ b/src/components/FrameworkSelector.ts @@ -18,56 +18,105 @@ export class FrameworkSelector { readonly el: HTMLElement; private select: HTMLSelectElement | null = null; private panelId: AnalysisPanelId; + private popup: HTMLElement | null = null; + private btn: HTMLButtonElement; + private outsideClickHandler: ((e: MouseEvent) => void) | null = null; + private note: string | undefined; constructor(opts: FrameworkSelectorOptions) { this.panelId = opts.panelId; + this.note = opts.note; + + const btn = document.createElement('button'); + btn.className = 'icon-btn framework-settings-btn'; + btn.innerHTML = '⚙'; + this.btn = btn; if (opts.isPremium) { const select = document.createElement('select'); - select.className = 'framework-selector'; + select.className = 'framework-popup-select'; this.select = select; - this.populateOptions(select); select.value = getActiveFrameworkForPanel(opts.panelId)?.id ?? ''; - select.addEventListener('change', () => { setActiveFrameworkForPanel(opts.panelId, select.value || null); + this.updateBtnTitle(); + this.closePopup(); }); - if (opts.note) { - const wrap = document.createElement('span'); - wrap.className = 'framework-selector-wrap'; - const noteEl = document.createElement('span'); - noteEl.className = 'framework-selector-note'; - noteEl.title = opts.note; - noteEl.textContent = '*'; - wrap.append(select, noteEl); - this.el = wrap; - } else { - this.el = select; - } + btn.addEventListener('click', (e) => { + e.stopPropagation(); + if (this.popup) { + this.closePopup(); + } else { + this.openPopup(); + } + }); } else { - const select = document.createElement('select'); - select.className = 'framework-selector framework-selector--locked'; - select.disabled = true; - const defaultOpt = document.createElement('option'); - defaultOpt.value = ''; - defaultOpt.textContent = 'Default (Neutral)'; - select.appendChild(defaultOpt); - - const badge = document.createElement('span'); - badge.className = 'framework-selector-pro-badge'; - badge.textContent = 'PRO'; - - const wrap = document.createElement('span'); - wrap.className = 'framework-selector-wrap'; - wrap.append(select, badge); - if (opts.panel) { - wrap.addEventListener('click', () => { - opts.panel!.showGatedCta(PanelGateReason.FREE_TIER, () => {}); - }); + btn.classList.add('framework-settings-btn--locked'); + btn.addEventListener('click', (e) => { + e.stopPropagation(); + opts.panel?.showGatedCta(PanelGateReason.FREE_TIER, () => {}); + }); + } + + this.updateBtnTitle(); + this.el = btn; + } + + private updateBtnTitle(): void { + const fw = this.select ? getActiveFrameworkForPanel(this.panelId) : null; + this.btn.title = fw ? `Framework: ${fw.name}` : 'Analysis framework'; + } + + private openPopup(): void { + const btnRect = this.btn.getBoundingClientRect(); + + const popup = document.createElement('div'); + popup.className = 'framework-settings-popup'; + popup.style.top = `${btnRect.bottom + 4}px`; + popup.style.right = `${document.documentElement.clientWidth - btnRect.right}px`; + + const label = document.createElement('div'); + label.className = 'framework-settings-label'; + label.textContent = 'Analysis Framework'; + popup.appendChild(label); + + if (this.select) { + popup.appendChild(this.select); + } + + if (this.note) { + const noteEl = document.createElement('div'); + noteEl.className = 'framework-settings-note'; + noteEl.textContent = this.note; + popup.appendChild(noteEl); + } + + document.body.appendChild(popup); + this.popup = popup; + this.btn.setAttribute('aria-expanded', 'true'); + + const handler = (e: MouseEvent) => { + if (!popup.contains(e.target as Node) && e.target !== this.btn) { + this.closePopup(); } - this.el = wrap; + }; + this.outsideClickHandler = handler; + setTimeout(() => document.addEventListener('click', handler), 0); + } + + private closePopup(): void { + if (!this.popup) return; + if (this.select && this.popup.contains(this.select)) { + this.popup.removeChild(this.select); + } + this.popup.remove(); + this.popup = null; + this.btn.setAttribute('aria-expanded', 'false'); + if (this.outsideClickHandler) { + document.removeEventListener('click', this.outsideClickHandler); + this.outsideClickHandler = null; } } @@ -91,9 +140,10 @@ export class FrameworkSelector { const current = this.select.value; this.populateOptions(this.select); this.select.value = getActiveFrameworkForPanel(this.panelId)?.id ?? current; + this.updateBtnTitle(); } destroy(): void { - // No async listeners to clean up; GC handles the rest + this.closePopup(); } } diff --git a/src/styles/main.css b/src/styles/main.css index 1a10f1fd08..2ba19e8e1d 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -22135,38 +22135,56 @@ body.map-width-resizing { margin-right: 4px; } -.framework-selector { - font-size: 11px; - background: transparent; - border: 1px solid rgba(255,255,255,0.15); - border-radius: 3px; - color: inherit; - padding: 1px 4px; - cursor: pointer; - max-width: 150px; +.framework-settings-btn { + font-size: 12px; + opacity: 0.6; + transition: opacity 0.15s ease, background 0.15s ease, color 0.15s ease; } -.framework-selector--locked { - opacity: 0.5; - cursor: not-allowed; +.framework-settings-btn:hover, +.framework-settings-btn[aria-expanded="true"] { + opacity: 1; } -.framework-selector-wrap { - display: inline-flex; - align-items: center; - gap: 4px; +.framework-settings-btn--locked { + opacity: 0.35; } -.framework-selector-pro-badge { - font-size: 9px; - font-weight: 700; - background: var(--color-accent, #7c3aed); - color: #fff; + +.framework-settings-popup { + position: fixed; + z-index: 9999; + background: var(--bg-panel, #0f1117); + border: 1px solid var(--border); + border-radius: 5px; + padding: 10px 12px; + display: flex; + flex-direction: column; + gap: 8px; + min-width: 190px; + box-shadow: 0 4px 16px rgba(0,0,0,0.45); +} +.framework-settings-label { + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-dim); +} +.framework-popup-select { + font-size: 12px; + background: var(--overlay-subtle, rgba(255,255,255,0.05)); + border: 1px solid var(--border); border-radius: 3px; - padding: 1px 4px; - letter-spacing: 0.05em; + color: var(--text); + padding: 4px 6px; + cursor: pointer; + width: 100%; } -.framework-selector-note { +.framework-popup-select:focus { + outline: 1px solid var(--accent); +} +.framework-settings-note { font-size: 10px; - opacity: 0.5; - cursor: help; + color: var(--text-dim); + line-height: 1.4; + opacity: 0.7; } /* ── WM Analyst Chat Panel ──────────────────────────────────────── */