Skip to content

Allow opening in the side panel as well as the popup - #134

Merged
forgetso merged 5 commits into
help-rules-popup-refreshfrom
side-panel
Sep 21, 2026
Merged

forgetso merged 5 commits into
help-rules-popup-refreshfrom
side-panel

Conversation

@forgetso

@forgetso forgetso commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

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 touch popup.html, style.css, help.html and 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.js and style.css — there is no second copy of the UI to keep in step. background/surface.ts registers the 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 rather than sitting in a 320px column
  • the action buttons pin to the bottom of the window instead of floating halfway up it
  • the history list drops its max-height, which only exists to keep the popup inside Chrome's 600px

Measured at 320, 400 and 560px wide: no horizontal overflow, footer pinned, layout intact.

Making the toolbar button reach it

Clearing default_popup is the part that matters. While a popup is registered Chrome opens it and never fires action.onClicked, so openPanelOnActionClick has nothing to act on and the panel would only be reachable from Chrome's own side panel menu. Neither setPopup nor the panel behaviour survives a browser restart, so both are reapplied on onStartup as well as on install and whenever the preference changes.

The manifest, deliberately plain

side_panel.default_path stays assets/popup.html with 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-extension does not install anything in this environment, and the unmodified manifest from master fails identically, so that test is inconclusive rather than reassuring. The query is applied through sidePanel.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:

🔍 Search and Replace                    ?  i
─────────────────────────────────────────────
      🕐         ⚙         💾         ▯
                 ▔▔▔▔

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 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.

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, because sidePanel.open must be called while the user gesture is still in effect and awaiting windows.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.onActivated and 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.sidePanel needs Chrome 114. Rather than setting minimum_chrome_version and cutting older browsers off from updates entirely, the preference is hidden where the API is missing and everything carries on as a popup.
  • The toggle-popup command had no handler, so Ctrl+Shift+S has 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 checks passes — typecheck, lint, format, 643 tests, build. 16 new tests cover surface.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 that setOptions gets 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

forgetso and others added 5 commits September 21, 2026 22:31
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>
@forgetso
forgetso merged commit 67e8bdf into help-rules-popup-refresh Sep 21, 2026
@forgetso forgetso linked an issue Sep 21, 2026 that may be closed by this pull request
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>
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.

Allow opening in sidebar or popup

1 participant