Repository navigation
Allow opening in the side panel as well as the popup - #134
Merged
Merged
Conversation
The popup closes the moment anything else takes focus, so clicking into the
page to check a replacement dismisses it and the terms have to be typed again.
Settings now offers "Open in: Popup | Side panel".
Both surfaces load the same assets/popup.html, popup.js and style.css — there
is no second copy of the UI. background/surface.ts registers the side panel
with ?surface=sidepanel, and that query string is the only thing popup.ts keys
its layout off: the panel fills the width it is given, pins the action buttons
to the bottom of the window, and drops the history list's height cap, which
only exists to keep the popup inside Chrome's 600px.
Clearing default_popup is the part that makes the toolbar button work: while a
popup is registered Chrome opens it and never fires action.onClicked, so
openPanelOnActionClick has nothing to act on. Neither that nor the panel
behaviour survives a browser restart, so both are reapplied on startup as well
as on install and whenever the preference changes.
side_panel.default_path in the manifest stays a plain path. A manifest Chrome
rejects stops the extension loading at all, and --load-extension does not work
in this environment to prove a query string there is accepted — the unmodified
manifest fails to install identically — so the query is applied through
sidePanel.setOptions instead, which runs before the panel can ever be opened.
The panel outlives a tab, unlike the popup, so it recounts when the user
switches tab or navigates; a count left over from a tab they have since moved
away from is worse than no count.
Also:
- chrome.sidePanel needs Chrome 114. Rather than setting
minimum_chrome_version and cutting older browsers off from updates, the
preference is hidden where the API is missing.
- The toggle-popup command had no handler, so Ctrl+Shift+S has never done
anything. It now opens whichever surface is configured.
- Switching to the side panel from inside the popup opens the panel there and
then, rather than leaving the user to work out they must click the toolbar
button again.
Documented in the help page, and translated into all ten locales.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The preference under the cog is for choosing a default; this is for switching surface there and then. In the popup the icon opens the side panel and closes the popup behind it; in the panel it closes the panel. There is no sidePanel.close(), so the panel closes its own window, which is also how the popup dismisses itself once the panel has taken over. The window id is looked up on load rather than in the click handler: sidePanel.open has to be called while the user gesture is still in effect, and awaiting windows.getCurrent() inside the handler risks spending it. The sixth icon exposed a rule that has never worked. #header form set flex-wrap: nowrap, but the markup carried bootstrap's .flex-wrap, whose !important beat it — with five icons they happened to fit, so nobody noticed. The class is gone, the form no longer shrinks, and the gap is 4px, so all six icons and the full title fit across 320px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six icons and the title were fighting over 320px: the icons ended up crowded together and the title was trimmed to "Search and Repl...". The icons now sit on a row of their own beneath the title, with the full width to spread across. That row costs about 27px, which took every section back over the 600px at which Chrome scrolls the whole popup, so the header padding drops to py-2, the gap above the icons is 6px, and the history list cap comes down to 340px. Measured across all ten locales: the tallest is now Russian at exactly 600. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A row of six evenly spaced glyphs under the title read as undifferentiated. They are back on the title row, in two groups with a divider between them: the two that only tell you something (help, about) and the four that do something (history, settings, saved rules, side panel). Fitting all six and the untruncated title across 320px needed the width back: the icons are 16px rather than 18px, the gap within a group is 3px, and the title span loses a redundant .mx-1 that doubled up with the flex gap. Title renders in full at 320px. Back on one row, every section has room again: the tallest is Russian at 575 against Chrome's 600px cap, so the history list cap returns to 380px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Help and about only ever open documentation; history, settings, saved rules and the side panel toggle change what you are looking at. The first pair now sits on the title line, and the other four form a full-bleed tab strip beneath it, sharing the width equally. The underline marking the open panel spans the whole tab rather than just the glyph. The strip costs about 26px, so the body panel's vertical padding drops to 12px and the history list cap to 330px. Measured across all ten locales: the tallest is Russian at 593 against Chrome's 600px cap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 21, 2026
forgetso
added a commit
that referenced
this pull request
Sep 21, 2026
* Allow opening in the side panel as well as the popup (#121) The popup closes the moment anything else takes focus, so clicking into the page to check a replacement dismisses it and the terms have to be typed again. Settings now offers "Open in: Popup | Side panel". Both surfaces load the same assets/popup.html, popup.js and style.css — there is no second copy of the UI. background/surface.ts registers the side panel with ?surface=sidepanel, and that query string is the only thing popup.ts keys its layout off: the panel fills the width it is given, pins the action buttons to the bottom of the window, and drops the history list's height cap, which only exists to keep the popup inside Chrome's 600px. Clearing default_popup is the part that makes the toolbar button work: while a popup is registered Chrome opens it and never fires action.onClicked, so openPanelOnActionClick has nothing to act on. Neither that nor the panel behaviour survives a browser restart, so both are reapplied on startup as well as on install and whenever the preference changes. side_panel.default_path in the manifest stays a plain path. A manifest Chrome rejects stops the extension loading at all, and --load-extension does not work in this environment to prove a query string there is accepted — the unmodified manifest fails to install identically — so the query is applied through sidePanel.setOptions instead, which runs before the panel can ever be opened. The panel outlives a tab, unlike the popup, so it recounts when the user switches tab or navigates; a count left over from a tab they have since moved away from is worse than no count. Also: - chrome.sidePanel needs Chrome 114. Rather than setting minimum_chrome_version and cutting older browsers off from updates, the preference is hidden where the API is missing. - The toggle-popup command had no handler, so Ctrl+Shift+S has never done anything. It now opens whichever surface is configured. - Switching to the side panel from inside the popup opens the panel there and then, rather than leaving the user to work out they must click the toolbar button again. Documented in the help page, and translated into all ten locales. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Add a side panel open/close button to the header The preference under the cog is for choosing a default; this is for switching surface there and then. In the popup the icon opens the side panel and closes the popup behind it; in the panel it closes the panel. There is no sidePanel.close(), so the panel closes its own window, which is also how the popup dismisses itself once the panel has taken over. The window id is looked up on load rather than in the click handler: sidePanel.open has to be called while the user gesture is still in effect, and awaiting windows.getCurrent() inside the handler risks spending it. The sixth icon exposed a rule that has never worked. #header form set flex-wrap: nowrap, but the markup carried bootstrap's .flex-wrap, whose !important beat it — with five icons they happened to fit, so nobody noticed. The class is gone, the form no longer shrinks, and the gap is 4px, so all six icons and the full title fit across 320px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Move the header icons onto their own row Six icons and the title were fighting over 320px: the icons ended up crowded together and the title was trimmed to "Search and Repl...". The icons now sit on a row of their own beneath the title, with the full width to spread across. That row costs about 27px, which took every section back over the 600px at which Chrome scrolls the whole popup, so the header padding drops to py-2, the gap above the icons is 6px, and the history list cap comes down to 340px. Measured across all ten locales: the tallest is now Russian at exactly 600. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Group the header icons instead of putting them on their own row A row of six evenly spaced glyphs under the title read as undifferentiated. They are back on the title row, in two groups with a divider between them: the two that only tell you something (help, about) and the four that do something (history, settings, saved rules, side panel). Fitting all six and the untruncated title across 320px needed the width back: the icons are 16px rather than 18px, the gap within a group is 3px, and the title span loses a redundant .mx-1 that doubled up with the flex gap. Title renders in full at 320px. Back on one row, every section has room again: the tallest is Russian at 575 against Chrome's 600px cap, so the history list cap returns to 380px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Split the header into a title line and a tab strip Help and about only ever open documentation; history, settings, saved rules and the side panel toggle change what you are looking at. The first pair now sits on the title line, and the other four form a full-bleed tab strip beneath it, sharing the width equally. The underline marking the open panel spans the whole tab rather than just the glyph. The strip costs about 26px, so the body panel's vertical padding drops to 12px and the history list cap to 330px. Measured across all ten locales: the tallest is Russian at 593 against Chrome's 600px cap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
forgetso
added a commit
that referenced
this pull request
Sep 21, 2026
* Allow opening in the side panel as well as the popup (#121) The popup closes the moment anything else takes focus, so clicking into the page to check a replacement dismisses it and the terms have to be typed again. Settings now offers "Open in: Popup | Side panel". Both surfaces load the same assets/popup.html, popup.js and style.css — there is no second copy of the UI. background/surface.ts registers the side panel with ?surface=sidepanel, and that query string is the only thing popup.ts keys its layout off: the panel fills the width it is given, pins the action buttons to the bottom of the window, and drops the history list's height cap, which only exists to keep the popup inside Chrome's 600px. Clearing default_popup is the part that makes the toolbar button work: while a popup is registered Chrome opens it and never fires action.onClicked, so openPanelOnActionClick has nothing to act on. Neither that nor the panel behaviour survives a browser restart, so both are reapplied on startup as well as on install and whenever the preference changes. side_panel.default_path in the manifest stays a plain path. A manifest Chrome rejects stops the extension loading at all, and --load-extension does not work in this environment to prove a query string there is accepted — the unmodified manifest fails to install identically — so the query is applied through sidePanel.setOptions instead, which runs before the panel can ever be opened. The panel outlives a tab, unlike the popup, so it recounts when the user switches tab or navigates; a count left over from a tab they have since moved away from is worse than no count. Also: - chrome.sidePanel needs Chrome 114. Rather than setting minimum_chrome_version and cutting older browsers off from updates, the preference is hidden where the API is missing. - The toggle-popup command had no handler, so Ctrl+Shift+S has never done anything. It now opens whichever surface is configured. - Switching to the side panel from inside the popup opens the panel there and then, rather than leaving the user to work out they must click the toolbar button again. Documented in the help page, and translated into all ten locales. * Add a side panel open/close button to the header The preference under the cog is for choosing a default; this is for switching surface there and then. In the popup the icon opens the side panel and closes the popup behind it; in the panel it closes the panel. There is no sidePanel.close(), so the panel closes its own window, which is also how the popup dismisses itself once the panel has taken over. The window id is looked up on load rather than in the click handler: sidePanel.open has to be called while the user gesture is still in effect, and awaiting windows.getCurrent() inside the handler risks spending it. The sixth icon exposed a rule that has never worked. #header form set flex-wrap: nowrap, but the markup carried bootstrap's .flex-wrap, whose !important beat it — with five icons they happened to fit, so nobody noticed. The class is gone, the form no longer shrinks, and the gap is 4px, so all six icons and the full title fit across 320px. * Move the header icons onto their own row Six icons and the title were fighting over 320px: the icons ended up crowded together and the title was trimmed to "Search and Repl...". The icons now sit on a row of their own beneath the title, with the full width to spread across. That row costs about 27px, which took every section back over the 600px at which Chrome scrolls the whole popup, so the header padding drops to py-2, the gap above the icons is 6px, and the history list cap comes down to 340px. Measured across all ten locales: the tallest is now Russian at exactly 600. * Group the header icons instead of putting them on their own row A row of six evenly spaced glyphs under the title read as undifferentiated. They are back on the title row, in two groups with a divider between them: the two that only tell you something (help, about) and the four that do something (history, settings, saved rules, side panel). Fitting all six and the untruncated title across 320px needed the width back: the icons are 16px rather than 18px, the gap within a group is 3px, and the title span loses a redundant .mx-1 that doubled up with the flex gap. Title renders in full at 320px. Back on one row, every section has room again: the tallest is Russian at 575 against Chrome's 600px cap, so the history list cap returns to 380px. * Split the header into a title line and a tab strip Help and about only ever open documentation; history, settings, saved rules and the side panel toggle change what you are looking at. The first pair now sits on the title line, and the other four form a full-bleed tab strip beneath it, sharing the width equally. The underline marking the open panel spans the whole tab rather than just the glyph. The strip costs about 26px, so the body panel's vertical padding drops to 12px and the history list cap to 330px. Measured across all ten locales: the tallest is Russian at 593 against Chrome's 600px cap. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #121.
The popup closes the moment anything else takes focus, so clicking into the page to check a replacement dismisses it and the terms have to be typed again. Settings now offers Open in: Popup | Side panel, and the header carries a button to switch surface there and then.
Note
Stacked on #133 — base that branch, not
master, because both touchpopup.html,style.css,help.htmland every locale file. Review #133 first; this diff is the side panel alone.One UI, two surfaces
Both surfaces load the same
assets/popup.html,popup.jsandstyle.css— there is no second copy of the UI to keep in step.background/surface.tsregisters the panel with?surface=sidepanel, and that query string is the only thingpopup.tskeys its layout off:max-height, which only exists to keep the popup inside Chrome's 600pxMeasured at 320, 400 and 560px wide: no horizontal overflow, footer pinned, layout intact.
Making the toolbar button reach it
Clearing
default_popupis the part that matters. While a popup is registered Chrome opens it and never firesaction.onClicked, soopenPanelOnActionClickhas nothing to act on and the panel would only be reachable from Chrome's own side panel menu. NeithersetPopupnor the panel behaviour survives a browser restart, so both are reapplied ononStartupas well as on install and whenever the preference changes.The manifest, deliberately plain
side_panel.default_pathstaysassets/popup.htmlwith no query string. A manifest Chrome rejects stops the extension loading at all, and I could not prove a query string there is accepted:--load-extensiondoes not install anything in this environment, and the unmodified manifest frommasterfails identically, so that test is inconclusive rather than reassuring. The query is applied throughsidePanel.setOptions()instead, which runs on install, on startup and on every preference change — always before the panel can be opened. If it somehow had not run, the panel still renders, just in the popup's narrow column.Header, reorganised
The side panel toggle was a sixth icon, and six icons plus the title could not share 320px without the icons crowding and the title truncating to "Search and Repl...". The header is now split by what the icons do:
Help and about only ever open documentation, so they stay on the title line. History, settings, saved rules and the side panel toggle change what you are looking at, so they form a full-bleed tab strip sharing the width equally, with the underline marking the open panel spanning the whole tab.
That strip costs ~26px, which matters because Chrome scrolls the entire popup past 600px. Body padding drops to 12px and the history cap to 330px; measured across all ten locales the tallest is now Russian at 593.
This also exposed a rule that had never worked:
#header formsetflex-wrap: nowrap, but the markup carried bootstrap's.flex-wrap, whose!importantbeat it. With five icons they happened to fit, so nobody noticed.Switching surface
The header button opens the side panel from the popup and closes it from within the panel. There is no
sidePanel.close(), so the panel closes its own window — which is also how the popup dismisses itself once the panel has taken over. The window id is looked up on load rather than in the click handler, becausesidePanel.openmust be called while the user gesture is still in effect and awaitingwindows.getCurrent()inside the handler risks spending it.Behaviour differences the panel forces
The panel outlives a tab, where the popup is thrown away with it. It recounts on
tabs.onActivatedand on navigation of the tab on display — a match count left over from a tab the user has since switched away from is worse than no count at all.Also in here
chrome.sidePanelneeds Chrome 114. Rather than settingminimum_chrome_versionand cutting older browsers off from updates entirely, the preference is hidden where the API is missing and everything carries on as a popup.toggle-popupcommand had no handler, soCtrl+Shift+Shas never done anything. It now opens whichever surface is configured.Defaults
Unchanged:
popup. The issue asks for a choice rather than a replacement, and silently moving every existing user into a side panel is not a choice. Say the word if you would rather the panel were the default.Checks
npm run checkspasses — typecheck, lint, format, 643 tests, build. 16 new tests coversurface.ts, including the Chrome-113 path and the browser refusing the call.Not verifiable from here, worth a smoke test on an unpacked build: that the manifest loads, that the panel opens from the toolbar button and from
Ctrl+Shift+S, and thatsetOptionsgets the query string in place before first use.Follow-up filed as #135: counting and replacing should be one pass over one set of instances rather than two independent walks that disagree.
🤖 Generated with Claude Code