Skip to content

refactor(panels): framework selector gear button#2539

Merged
koala73 merged 1 commit intomainfrom
feat/framework-selector-gear-btn
Mar 30, 2026
Merged

refactor(panels): framework selector gear button#2539
koala73 merged 1 commit intomainfrom
feat/framework-selector-gear-btn

Conversation

@koala73
Copy link
Copy Markdown
Owner

@koala73 koala73 commented Mar 30, 2026

Why

The analysis framework <select> sitting directly in the panel header was visually noisy and inconsistent across all 4 panels that carry it (Daily Market Brief, Market Implications, Deduction, Insights).

What changed

  • FrameworkSelector.ts: select element removed from the header DOM. Replaced by a small icon-btn. Clicking it opens a compact position: fixed popup (appended to body) containing the framework select + note text, positioned directly below the button. Closes on outside click or after making a selection. aria-expanded toggles for CSS active state.
  • main.css: Old .framework-selector* rules replaced with .framework-settings-btn, .framework-settings-popup, .framework-popup-select, .framework-settings-note.
  • No changes to any panel file (the this.header.appendChild(this.fwSelector.el) call is unchanged in all 4 panels).

Free-tier behavior is unchanged: the gear button opens the PRO gated CTA.

…ear button

The full <select> element in the panel header was visually heavy and
inconsistent across 4 panels (DailyMarketBrief, MarketImplications,
Deduction, Insights). Replaced with a compact gear icon-btn that opens
a fixed-position popup containing the framework select and note text.

Popup positions below the button, closes on outside click or selection,
and uses aria-expanded for active state styling. Free-tier button
redirects to the gated CTA as before.
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
worldmonitor Ignored Ignored Mar 30, 2026 5:09am

Request Review

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 30, 2026

Greptile Summary

This PR refactors the analysis framework selector across all four AI panels (Daily Market Brief, Market Implications, Deduction, Insights) from an always-visible <select> in the panel header to a compact gear button that opens a position: fixed popup. The change reduces visual noise in the header while preserving all existing functionality, including the PRO-gated CTA path for free-tier users.

Key implementation details:

  • The <select> element is kept as a detached DOM node when the popup is closed and re-inserted into a freshly-created popup <div> each time it opens — event listeners and value state are preserved correctly across open/close cycles.
  • Popup cleanup is wired into destroy() via closePopup(), preventing orphaned DOM nodes or document-level event listeners when a panel is torn down.
  • CSS is updated to use design-token CSS custom properties (--bg-panel, --border, --text, --accent, --text-dim) consistently with the rest of the app.
  • No panel files needed to change since this.header.appendChild(this.fwSelector.el) still works — el just points to the button now instead of the old select/wrap.

Minor findings (all P2):

  • aria-expanded is not set to 'false' in the constructor; its absence before first interaction can affect screen-reader state announcements.
  • The outside-click guard e.target !== this.btn is unreachable dead code because both button click handlers call e.stopPropagation().
  • The popup's fixed position is computed once at open time; scrolling or resizing while the popup is open causes it to drift away from the button.

Confidence Score: 5/5

Safe to merge — all findings are P2 style/accessibility suggestions with no impact on correctness or runtime behaviour.

The refactor is well-scoped: DOM lifecycle (detach/re-attach of the shared select node, event listener cleanup) is handled correctly, destroy() properly tears down the popup, and the CSS changes use existing design tokens. The three P2 findings are non-blocking quality improvements.

src/components/FrameworkSelector.ts — minor aria and dead-code issues noted above, but nothing blocking.

Important Files Changed

Filename Overview
src/components/FrameworkSelector.ts Replaces inline select with a gear-icon button that opens a body-appended fixed popup; DOM lifecycle (detach/re-attach of the select, cleanup in destroy) is handled correctly; three minor P2 style issues found (aria-expanded not initialized, dead e.target check, no scroll/resize repositioning)
src/styles/main.css Old .framework-selector* rules replaced with new .framework-settings-btn, .framework-settings-popup, .framework-popup-select, and .framework-settings-note rules; uses CSS custom properties consistently; no issues found

Sequence Diagram

sequenceDiagram
    participant User
    participant GearBtn as ⚙ Button
    participant FS as FrameworkSelector
    participant Body as document.body
    participant Store as analysis-framework-store

    User->>GearBtn: click
    GearBtn->>FS: btn click handler (stopPropagation)
    alt popup closed
        FS->>FS: openPopup()
        FS->>Body: appendChild(popup + select)
        FS->>GearBtn: aria-expanded = "true"
        FS->>Body: addEventListener("click", outsideClickHandler, setTimeout 0)
    else popup open
        FS->>FS: closePopup()
        FS->>FS: popup.removeChild(select) — preserve select node
        FS->>Body: popup.remove()
        FS->>GearBtn: aria-expanded = "false"
        FS->>Body: removeEventListener("click", outsideClickHandler)
    end

    User->>GearBtn: click outside popup
    Body->>FS: outsideClickHandler fires
    FS->>FS: closePopup()

    User->>GearBtn: select option in popup
    GearBtn->>Store: setActiveFrameworkForPanel()
    GearBtn->>FS: updateBtnTitle()
    GearBtn->>FS: closePopup()

    Note over FS,Body: destroy() → closePopup() cleans up popup + listener
Loading

Reviews (1): Last reviewed commit: "refactor(panels): replace framework sele..." | Re-trigger Greptile

Comment on lines +30 to +33
const btn = document.createElement('button');
btn.className = 'icon-btn framework-settings-btn';
btn.innerHTML = '⚙';
this.btn = btn;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 aria-expanded not initialized

The button's aria-expanded attribute is never set in the constructor. Before the popup is opened for the first time, the button carries no aria-expanded attribute at all. Screen readers may not announce the collapsed state correctly, and any CSS that targets [aria-expanded="false"] will silently not match.

Suggested change
const btn = document.createElement('button');
btn.className = 'icon-btn framework-settings-btn';
btn.innerHTML = '⚙';
this.btn = btn;
btn.className = 'icon-btn framework-settings-btn';
btn.innerHTML = '';
btn.setAttribute('aria-expanded', 'false');
this.btn = btn;

Comment on lines +100 to 103
const handler = (e: MouseEvent) => {
if (!popup.contains(e.target as Node) && e.target !== this.btn) {
this.closePopup();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 e.target !== this.btn check is dead code

Both button click listeners (premium and free-tier branches) call e.stopPropagation(), so a click on this.btn is never propagated to the document-level handler. The condition e.target !== this.btn can therefore never be false, making it unreachable dead code.

The guard is harmless today but could mislead a future developer into thinking button-clicks reach this handler. It can be simplified to just:

Suggested change
const handler = (e: MouseEvent) => {
if (!popup.contains(e.target as Node) && e.target !== this.btn) {
this.closePopup();
}
const handler = (e: MouseEvent) => {
if (!popup.contains(e.target as Node)) {
this.closePopup();
}
};

Comment on lines +72 to +78
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`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Popup position drifts on scroll / resize

top and right are computed once from getBoundingClientRect() at the moment the popup opens, then written as static position: fixed values. If the user scrolls the page (or resizes the window) while the popup is open, the gear button moves but the popup stays at the original coordinates, creating a visual disconnect.

Consider adding scroll and resize listeners (passive, on window) that call closePopup(), keeping behaviour consistent with other lightweight dropdowns in the app:

const closeOnScroll = () => this.closePopup();
window.addEventListener('scroll', closeOnScroll, { passive: true, capture: true });
window.addEventListener('resize', closeOnScroll, { passive: true });
// store refs and remove in closePopup()

@koala73 koala73 merged commit 09652ed into main Mar 30, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant