diff --git a/_locales/ar/messages.json b/_locales/ar/messages.json index beeb3eb..bf1ce4a 100644 --- a/_locales/ar/messages.json +++ b/_locales/ar/messages.json @@ -522,5 +522,45 @@ "history_removed": { "message": "(محذوف)", "description": "Shown in the History panel when the replacement term is empty" + }, + "open_in": { + "message": "الفتح في", + "description": "Settings: popup or side panel" + }, + "open_in_popup": { + "message": "نافذة منبثقة", + "description": "Settings: popup or side panel" + }, + "open_in_side_panel": { + "message": "اللوحة الجانبية", + "description": "Settings: popup or side panel" + }, + "open_in_hint": { + "message": "تظل اللوحة الجانبية مفتوحة أثناء عملك على الصفحة.", + "description": "Settings: popup or side panel" + }, + "sidebar_title": { + "message": "نافذة منبثقة أم لوحة جانبية", + "description": "Help section title" + }, + "sidebar_p1": { + "message": "افتراضيًا تفتح الإضافة كنافذة منبثقة أسفل زر شريط الأدوات. وتُغلق النافذة المنبثقة بمجرد انتقال التركيز إلى شيء آخر، فالنقر داخل الصفحة للتحقق من الاستبدال يُخفيها ويضطرك إلى كتابة الكلمات من جديد.", + "description": "Help section on the side panel" + }, + "sidebar_p2": { + "message": "اختر اللوحة الجانبية أسفل أيقونة الترس لتفتح الإضافة في اللوحة الجانبية لكروم بدلًا من ذلك. تظل مفتوحة أثناء عملك على الصفحة، وتنتقل معك من تبويب إلى آخر، ويمكن توسيعها بالسحب. وكل ما عدا ذلك يعمل تمامًا كما في النافذة المنبثقة.", + "description": "Help section on the side panel" + }, + "sidebar_p3": { + "message": "يتبع هذا الإعداد ملفك الشخصي في كروم، ويفتح اختصار لوحة المفاتيح Ctrl+Shift+S ما اخترته منهما. وتتطلب اللوحة الجانبية كروم 114 أو أحدث؛ ولا يظهر الإعداد في الإصدارات الأقدم.", + "description": "Help section on the side panel" + }, + "open_side_panel": { + "message": "الفتح في اللوحة الجانبية", + "description": "Tooltip on the side panel icon in the popup" + }, + "close_side_panel": { + "message": "إغلاق اللوحة الجانبية", + "description": "Tooltip on the side panel icon when shown in the panel" } } diff --git a/_locales/de/messages.json b/_locales/de/messages.json index c425f12..4235665 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -522,5 +522,45 @@ "history_removed": { "message": "(entfernt)", "description": "Shown in the History panel when the replacement term is empty" + }, + "open_in": { + "message": "Öffnen in", + "description": "Settings: popup or side panel" + }, + "open_in_popup": { + "message": "Popup", + "description": "Settings: popup or side panel" + }, + "open_in_side_panel": { + "message": "Seitenleiste", + "description": "Settings: popup or side panel" + }, + "open_in_hint": { + "message": "Die Seitenleiste bleibt geöffnet, während Sie auf der Seite arbeiten.", + "description": "Settings: popup or side panel" + }, + "sidebar_title": { + "message": "Popup oder Seitenleiste", + "description": "Help section title" + }, + "sidebar_p1": { + "message": "Standardmäßig öffnet sich die Erweiterung als Popup unter der Symbolleisten-Schaltfläche. Ein Popup schließt sich, sobald etwas anderes den Fokus erhält: Ein Klick in die Seite, um eine Ersetzung zu prüfen, lässt es verschwinden, und die Begriffe müssen erneut eingegeben werden.", + "description": "Help section on the side panel" + }, + "sidebar_p2": { + "message": "Wählen Sie unter dem Zahnradsymbol Seitenleiste, um die Erweiterung stattdessen in der Seitenleiste von Chrome zu öffnen. Sie bleibt geöffnet, während Sie auf der Seite arbeiten, folgt Ihnen von Tab zu Tab und lässt sich breiter ziehen. Alles andere funktioniert genau wie im Popup.", + "description": "Help section on the side panel" + }, + "sidebar_p3": { + "message": "Die Einstellung folgt Ihrem Chrome-Profil, und das Tastenkürzel Strg+Umschalt+S öffnet jeweils die gewählte Variante. Die Seitenleiste erfordert Chrome 114 oder neuer; in älteren Versionen wird die Einstellung nicht angezeigt.", + "description": "Help section on the side panel" + }, + "open_side_panel": { + "message": "In der Seitenleiste öffnen", + "description": "Tooltip on the side panel icon in the popup" + }, + "close_side_panel": { + "message": "Seitenleiste schließen", + "description": "Tooltip on the side panel icon when shown in the panel" } } diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1530a9f..92596ba 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -522,5 +522,45 @@ "history_removed": { "message": "(removed)", "description": "Shown in the History panel when the replacement term is empty" + }, + "open_in": { + "message": "Open in", + "description": "Settings: popup or side panel" + }, + "open_in_popup": { + "message": "Popup", + "description": "Settings: popup or side panel" + }, + "open_in_side_panel": { + "message": "Side panel", + "description": "Settings: popup or side panel" + }, + "open_in_hint": { + "message": "The side panel stays open while you work on the page.", + "description": "Settings: popup or side panel" + }, + "sidebar_title": { + "message": "Popup or side panel", + "description": "Help section title" + }, + "sidebar_p1": { + "message": "By default the extension opens as a popup under the toolbar button. A popup closes as soon as anything else takes the focus, so clicking into the page to check a replacement dismisses it and the terms have to be typed again.", + "description": "Help section on the side panel" + }, + "sidebar_p2": { + "message": "Choose Side panel under the cog icon to open in Chrome's side panel instead. It stays open while you work on the page, follows you from tab to tab, and can be dragged wider. Everything else works exactly as it does in the popup.", + "description": "Help section on the side panel" + }, + "sidebar_p3": { + "message": "The setting follows your Chrome profile, and the keyboard shortcut Ctrl+Shift+S opens whichever one you have chosen. The side panel needs Chrome 114 or newer; on anything older the setting is not shown.", + "description": "Help section on the side panel" + }, + "open_side_panel": { + "message": "Open in the side panel", + "description": "Tooltip on the side panel icon in the popup" + }, + "close_side_panel": { + "message": "Close the side panel", + "description": "Tooltip on the side panel icon when shown in the panel" } } diff --git a/_locales/es/messages.json b/_locales/es/messages.json index d732685..95eeed3 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -522,5 +522,45 @@ "history_removed": { "message": "(eliminado)", "description": "Shown in the History panel when the replacement term is empty" + }, + "open_in": { + "message": "Abrir en", + "description": "Settings: popup or side panel" + }, + "open_in_popup": { + "message": "Ventana emergente", + "description": "Settings: popup or side panel" + }, + "open_in_side_panel": { + "message": "Panel lateral", + "description": "Settings: popup or side panel" + }, + "open_in_hint": { + "message": "El panel lateral permanece abierto mientras trabaja en la página.", + "description": "Settings: popup or side panel" + }, + "sidebar_title": { + "message": "Ventana emergente o panel lateral", + "description": "Help section title" + }, + "sidebar_p1": { + "message": "De forma predeterminada la extensión se abre como una ventana emergente bajo el botón de la barra de herramientas. Una ventana emergente se cierra en cuanto cualquier otra cosa recibe el foco, así que hacer clic en la página para comprobar una sustitución la descarta y hay que volver a escribir los términos.", + "description": "Help section on the side panel" + }, + "sidebar_p2": { + "message": "Elija Panel lateral bajo el icono del engranaje para abrirla en el panel lateral de Chrome. Permanece abierto mientras trabaja en la página, le sigue de pestaña en pestaña y se puede ensanchar. Todo lo demás funciona exactamente igual que en la ventana emergente.", + "description": "Help section on the side panel" + }, + "sidebar_p3": { + "message": "El ajuste sigue a su perfil de Chrome, y el atajo de teclado Ctrl+Mayús+S abre el que haya elegido. El panel lateral necesita Chrome 114 o posterior; en versiones anteriores el ajuste no se muestra.", + "description": "Help section on the side panel" + }, + "open_side_panel": { + "message": "Abrir en el panel lateral", + "description": "Tooltip on the side panel icon in the popup" + }, + "close_side_panel": { + "message": "Cerrar el panel lateral", + "description": "Tooltip on the side panel icon when shown in the panel" } } diff --git a/_locales/hi/messages.json b/_locales/hi/messages.json index f0745ff..f914e43 100644 --- a/_locales/hi/messages.json +++ b/_locales/hi/messages.json @@ -522,5 +522,45 @@ "history_removed": { "message": "(हटाया गया)", "description": "Shown in the History panel when the replacement term is empty" + }, + "open_in": { + "message": "इसमें खोलें", + "description": "Settings: popup or side panel" + }, + "open_in_popup": { + "message": "पॉपअप", + "description": "Settings: popup or side panel" + }, + "open_in_side_panel": { + "message": "साइड पैनल", + "description": "Settings: popup or side panel" + }, + "open_in_hint": { + "message": "जब आप पृष्ठ पर काम करते हैं, साइड पैनल खुला रहता है।", + "description": "Settings: popup or side panel" + }, + "sidebar_title": { + "message": "पॉपअप या साइड पैनल", + "description": "Help section title" + }, + "sidebar_p1": { + "message": "डिफ़ॉल्ट रूप से यह एक्सटेंशन टूलबार बटन के नीचे पॉपअप के रूप में खुलता है। जैसे ही फ़ोकस कहीं और जाता है पॉपअप बंद हो जाता है, इसलिए बदलाव जाँचने के लिए पृष्ठ पर क्लिक करते ही वह गायब हो जाता है और शब्द फिर से लिखने पड़ते हैं।", + "description": "Help section on the side panel" + }, + "sidebar_p2": { + "message": "इसके बजाय Chrome के साइड पैनल में खोलने के लिए गियर आइकन के नीचे साइड पैनल चुनें। जब आप पृष्ठ पर काम करते हैं यह खुला रहता है, टैब बदलने पर साथ चलता है, और इसे चौड़ा खींचा जा सकता है। बाकी सब कुछ ठीक वैसे ही काम करता है जैसे पॉपअप में।", + "description": "Help section on the side panel" + }, + "sidebar_p3": { + "message": "यह सेटिंग आपकी Chrome प्रोफ़ाइल के साथ चलती है, और कीबोर्ड शॉर्टकट Ctrl+Shift+S वही खोलता है जो आपने चुना है। साइड पैनल के लिए Chrome 114 या नया चाहिए; उससे पुराने में यह सेटिंग नहीं दिखती।", + "description": "Help section on the side panel" + }, + "open_side_panel": { + "message": "साइड पैनल में खोलें", + "description": "Tooltip on the side panel icon in the popup" + }, + "close_side_panel": { + "message": "साइड पैनल बंद करें", + "description": "Tooltip on the side panel icon when shown in the panel" } } diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 088c69b..d53577b 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -522,5 +522,45 @@ "history_removed": { "message": "(削除)", "description": "Shown in the History panel when the replacement term is empty" + }, + "open_in": { + "message": "開く場所", + "description": "Settings: popup or side panel" + }, + "open_in_popup": { + "message": "ポップアップ", + "description": "Settings: popup or side panel" + }, + "open_in_side_panel": { + "message": "サイドパネル", + "description": "Settings: popup or side panel" + }, + "open_in_hint": { + "message": "サイドパネルはページでの作業中も開いたままになります。", + "description": "Settings: popup or side panel" + }, + "sidebar_title": { + "message": "ポップアップかサイドパネルか", + "description": "Help section title" + }, + "sidebar_p1": { + "message": "既定では、この拡張機能はツールバーのボタンの下にポップアップとして開きます。ポップアップは他のものにフォーカスが移った時点で閉じるため、置換を確認しようとページをクリックすると消えてしまい、検索語を入力し直すことになります。", + "description": "Help section on the side panel" + }, + "sidebar_p2": { + "message": "歯車アイコンの下でサイドパネルを選ぶと、代わりに Chrome のサイドパネルで開きます。ページでの作業中も開いたままで、タブを切り替えても付いてきて、幅を広げることもできます。それ以外はポップアップとまったく同じように動作します。", + "description": "Help section on the side panel" + }, + "sidebar_p3": { + "message": "この設定は Chrome のプロファイルに従い、キーボードショートカット Ctrl+Shift+S は選んだ方を開きます。サイドパネルには Chrome 114 以降が必要で、それより古い場合この設定は表示されません。", + "description": "Help section on the side panel" + }, + "open_side_panel": { + "message": "サイドパネルで開く", + "description": "Tooltip on the side panel icon in the popup" + }, + "close_side_panel": { + "message": "サイドパネルを閉じる", + "description": "Tooltip on the side panel icon when shown in the panel" } } diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json index 34f01d6..3204c8c 100644 --- a/_locales/ko/messages.json +++ b/_locales/ko/messages.json @@ -522,5 +522,45 @@ "history_removed": { "message": "(삭제됨)", "description": "Shown in the History panel when the replacement term is empty" + }, + "open_in": { + "message": "열기 위치", + "description": "Settings: popup or side panel" + }, + "open_in_popup": { + "message": "팝업", + "description": "Settings: popup or side panel" + }, + "open_in_side_panel": { + "message": "사이드 패널", + "description": "Settings: popup or side panel" + }, + "open_in_hint": { + "message": "사이드 패널은 페이지에서 작업하는 동안 계속 열려 있습니다.", + "description": "Settings: popup or side panel" + }, + "sidebar_title": { + "message": "팝업 또는 사이드 패널", + "description": "Help section title" + }, + "sidebar_p1": { + "message": "기본적으로 이 확장 프로그램은 도구 모음 버튼 아래에 팝업으로 열립니다. 팝업은 다른 곳으로 포커스가 옮겨가는 순간 닫히므로, 바꾼 결과를 확인하려고 페이지를 클릭하면 사라지고 검색어를 다시 입력해야 합니다.", + "description": "Help section on the side panel" + }, + "sidebar_p2": { + "message": "톱니바퀴 아이콘 아래에서 사이드 패널을 선택하면 Chrome의 사이드 패널에서 열립니다. 페이지에서 작업하는 동안 계속 열려 있고, 탭을 옮겨도 따라오며, 넓게 늘릴 수도 있습니다. 그 밖의 모든 것은 팝업과 똑같이 동작합니다.", + "description": "Help section on the side panel" + }, + "sidebar_p3": { + "message": "이 설정은 Chrome 프로필을 따라가며, 키보드 단축키 Ctrl+Shift+S는 선택한 쪽을 엽니다. 사이드 패널은 Chrome 114 이상이 필요하고, 그보다 낮은 버전에서는 이 설정이 표시되지 않습니다.", + "description": "Help section on the side panel" + }, + "open_side_panel": { + "message": "사이드 패널에서 열기", + "description": "Tooltip on the side panel icon in the popup" + }, + "close_side_panel": { + "message": "사이드 패널 닫기", + "description": "Tooltip on the side panel icon when shown in the panel" } } diff --git a/_locales/pt_PT/messages.json b/_locales/pt_PT/messages.json index d01da4a..372e75a 100644 --- a/_locales/pt_PT/messages.json +++ b/_locales/pt_PT/messages.json @@ -522,5 +522,45 @@ "history_removed": { "message": "(removido)", "description": "Shown in the History panel when the replacement term is empty" + }, + "open_in": { + "message": "Abrir em", + "description": "Settings: popup or side panel" + }, + "open_in_popup": { + "message": "Janela pop-up", + "description": "Settings: popup or side panel" + }, + "open_in_side_panel": { + "message": "Painel lateral", + "description": "Settings: popup or side panel" + }, + "open_in_hint": { + "message": "O painel lateral permanece aberto enquanto trabalha na página.", + "description": "Settings: popup or side panel" + }, + "sidebar_title": { + "message": "Janela pop-up ou painel lateral", + "description": "Help section title" + }, + "sidebar_p1": { + "message": "Por predefinição a extensão abre como uma janela pop-up por baixo do botão da barra de ferramentas. Uma pop-up fecha assim que outra coisa recebe o foco, por isso clicar na página para verificar uma substituição fá-la desaparecer e os termos têm de ser escritos de novo.", + "description": "Help section on the side panel" + }, + "sidebar_p2": { + "message": "Escolha Painel lateral sob o ícone da roda dentada para abrir no painel lateral do Chrome. Fica aberto enquanto trabalha na página, acompanha-o de separador em separador e pode ser alargado. Tudo o resto funciona exatamente como na pop-up.", + "description": "Help section on the side panel" + }, + "sidebar_p3": { + "message": "A definição acompanha o seu perfil do Chrome, e o atalho de teclado Ctrl+Shift+S abre aquele que tiver escolhido. O painel lateral requer o Chrome 114 ou mais recente; em versões anteriores a definição não é apresentada.", + "description": "Help section on the side panel" + }, + "open_side_panel": { + "message": "Abrir no painel lateral", + "description": "Tooltip on the side panel icon in the popup" + }, + "close_side_panel": { + "message": "Fechar o painel lateral", + "description": "Tooltip on the side panel icon when shown in the panel" } } diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index f51687c..ba40787 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -522,5 +522,45 @@ "history_removed": { "message": "(удалено)", "description": "Shown in the History panel when the replacement term is empty" + }, + "open_in": { + "message": "Открывать в", + "description": "Settings: popup or side panel" + }, + "open_in_popup": { + "message": "Всплывающем окне", + "description": "Settings: popup or side panel" + }, + "open_in_side_panel": { + "message": "Боковой панели", + "description": "Settings: popup or side panel" + }, + "open_in_hint": { + "message": "Боковая панель остаётся открытой, пока вы работаете со страницей.", + "description": "Settings: popup or side panel" + }, + "sidebar_title": { + "message": "Всплывающее окно или боковая панель", + "description": "Help section title" + }, + "sidebar_p1": { + "message": "По умолчанию расширение открывается во всплывающем окне под кнопкой на панели инструментов. Такое окно закрывается, как только фокус переходит на что-то другое: щелчок по странице, чтобы проверить замену, закрывает его, и слова приходится вводить заново.", + "description": "Help section on the side panel" + }, + "sidebar_p2": { + "message": "Выберите Боковая панель под значком шестерёнки, чтобы расширение открывалось в боковой панели Chrome. Она остаётся открытой, пока вы работаете со страницей, следует за вами при переключении вкладок, и её можно растянуть шире. Всё остальное работает точно так же, как во всплывающем окне.", + "description": "Help section on the side panel" + }, + "sidebar_p3": { + "message": "Настройка привязана к вашему профилю Chrome, а сочетание клавиш Ctrl+Shift+S открывает выбранный вариант. Боковая панель требует Chrome 114 или новее; в более старых версиях настройка не отображается.", + "description": "Help section on the side panel" + }, + "open_side_panel": { + "message": "Открыть в боковой панели", + "description": "Tooltip on the side panel icon in the popup" + }, + "close_side_panel": { + "message": "Закрыть боковую панель", + "description": "Tooltip on the side panel icon when shown in the panel" } } diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 3f8c533..80a517c 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -522,5 +522,45 @@ "history_removed": { "message": "(已删除)", "description": "Shown in the History panel when the replacement term is empty" + }, + "open_in": { + "message": "打开方式", + "description": "Settings: popup or side panel" + }, + "open_in_popup": { + "message": "弹出窗口", + "description": "Settings: popup or side panel" + }, + "open_in_side_panel": { + "message": "侧边栏", + "description": "Settings: popup or side panel" + }, + "open_in_hint": { + "message": "你在页面上操作时,侧边栏会一直保持打开。", + "description": "Settings: popup or side panel" + }, + "sidebar_title": { + "message": "弹出窗口还是侧边栏", + "description": "Help section title" + }, + "sidebar_p1": { + "message": "默认情况下,扩展会在工具栏按钮下方以弹出窗口的形式打开。只要焦点转到别处,弹出窗口就会关闭——点击页面查看替换结果会让它消失,搜索词也得重新输入。", + "description": "Help section on the side panel" + }, + "sidebar_p2": { + "message": "在齿轮图标下选择侧边栏,扩展就会改在 Chrome 的侧边栏中打开。你在页面上操作时它会一直开着,切换标签页时也会跟随,还可以拖宽。其余一切都和弹出窗口中完全一样。", + "description": "Help section on the side panel" + }, + "sidebar_p3": { + "message": "该设置随你的 Chrome 配置文件同步,键盘快捷键 Ctrl+Shift+S 会打开你所选择的那一种。侧边栏需要 Chrome 114 或更高版本;更早的版本不会显示此设置。", + "description": "Help section on the side panel" + }, + "open_side_panel": { + "message": "在侧边栏中打开", + "description": "Tooltip on the side panel icon in the popup" + }, + "close_side_panel": { + "message": "关闭侧边栏", + "description": "Tooltip on the side panel icon when shown in the panel" } } diff --git a/assets/help.html b/assets/help.html index 8359982..df4694b 100644 --- a/assets/help.html +++ b/assets/help.html @@ -51,6 +51,7 @@

Help

Expressions
  • Replacing text in WordPress
  • Save and apply on page reload
  • +
  • Popup or side panel
  • History, Swap and language
  • Reporting Issues
  • @@ -411,6 +412,34 @@

    Multiple Matching Rules

    + +

    History, Swap and language

    diff --git a/assets/popup-overview.png b/assets/popup-overview.png index 1ba7b11..5666404 100644 Binary files a/assets/popup-overview.png and b/assets/popup-overview.png differ diff --git a/assets/popup-overview@2x.png b/assets/popup-overview@2x.png index 2f474d9..c666578 100644 Binary files a/assets/popup-overview@2x.png and b/assets/popup-overview@2x.png differ diff --git a/assets/popup.html b/assets/popup.html index af4ab38..9cf2bf4 100644 --- a/assets/popup.html +++ b/assets/popup.html @@ -1,264 +1,294 @@ - - - - - - - - - Search and Replace - - - -

    - searching and replacing... -
    -
    - -
    -
    -
    -
    - - -
    -
    -
    - -
    -
    - - -
    -
    -
    -
    - ? - -
    -
    - ? - -
    -
    - ? - -
    -
    - ? - -
    -
    - ? - -
    -
    - ? - -
    -
    - - -
    -
    - -
    -
    -
    - back iconBack -
    -
    Settings
    -

    Select a language

    - -
    -
    -
    - back iconBack -
    -
    About
    -
    - -
    -
    Search and Replace
    -
    Version -
    -
    -
    -

    Search for text on a webpage and replace it with different text. -

    -
    -
    Author
    -
    Chris Taylor — - prosopo.io -
    -
    Licence
    -
    Apache-2.0
    -
    - -
    -
    -
    - back iconBack -
    -
    History
    -
    -

    - Your recent searches will appear here. -

    -
      -
      - -
      -
      -
      -
      -
      - -
      - -
      - Version - -
      - - - - + + + + + + + + + Search and Replace + + + +
      + searching and replacing... +
      +
      + +
      +
      +
      +
      + + +
      +
      +
      + +
      +
      + + +
      +
      +
      +
      + ? + +
      +
      + ? + +
      +
      + ? + +
      +
      + ? + +
      +
      + ? + +
      +
      + ? + +
      +
      + + +
      +
      + +
      +
      +
      + back iconBack +
      +
      Settings
      +
      + + +
      +
      + + +

      + The side panel stays open while you work on the page. +

      +
      +
      +
      +
      + back iconBack +
      +
      About
      +
      + +
      +
      Search and Replace
      +
      Version +
      +
      +
      +

      Search for text on a webpage and replace it with different text. +

      +
      +
      Author
      +
      Chris Taylor — + prosopo.io +
      +
      Licence
      +
      Apache-2.0
      +
      + +
      +
      +
      + back iconBack +
      +
      History
      +
      +

      + Your recent searches will appear here. +

      +
        +
        + +
        +
        +
        +
        +
        + +
        + +
        + Version + +
        + + + + \ No newline at end of file diff --git a/assets/setup-hidden-content.png b/assets/setup-hidden-content.png index cba6a98..2cc682e 100644 Binary files a/assets/setup-hidden-content.png and b/assets/setup-hidden-content.png differ diff --git a/assets/setup-hidden-content@2x.png b/assets/setup-hidden-content@2x.png index 3626019..bd6159f 100644 Binary files a/assets/setup-hidden-content@2x.png and b/assets/setup-hidden-content@2x.png differ diff --git a/assets/setup-input-fields.png b/assets/setup-input-fields.png index 62b19bf..09bb77e 100644 Binary files a/assets/setup-input-fields.png and b/assets/setup-input-fields.png differ diff --git a/assets/setup-input-fields@2x.png b/assets/setup-input-fields@2x.png index ee48fa4..e8240fd 100644 Binary files a/assets/setup-input-fields@2x.png and b/assets/setup-input-fields@2x.png differ diff --git a/assets/setup-match-case.png b/assets/setup-match-case.png index bdae290..19db1a9 100644 Binary files a/assets/setup-match-case.png and b/assets/setup-match-case.png differ diff --git a/assets/setup-match-case@2x.png b/assets/setup-match-case@2x.png index 61b42ce..e65cd0e 100644 Binary files a/assets/setup-match-case@2x.png and b/assets/setup-match-case@2x.png differ diff --git a/assets/setup-regex.png b/assets/setup-regex.png index 332eaed..79206b6 100644 Binary files a/assets/setup-regex.png and b/assets/setup-regex.png differ diff --git a/assets/setup-regex@2x.png b/assets/setup-regex@2x.png index 2eab7bc..022bcd8 100644 Binary files a/assets/setup-regex@2x.png and b/assets/setup-regex@2x.png differ diff --git a/assets/setup-replace-html.png b/assets/setup-replace-html.png index d44cacf..69eac54 100644 Binary files a/assets/setup-replace-html.png and b/assets/setup-replace-html.png differ diff --git a/assets/setup-replace-html@2x.png b/assets/setup-replace-html@2x.png index 91a70f4..0152aed 100644 Binary files a/assets/setup-replace-html@2x.png and b/assets/setup-replace-html@2x.png differ diff --git a/assets/setup-save-rule.png b/assets/setup-save-rule.png index 9a24edc..bfc1c71 100644 Binary files a/assets/setup-save-rule.png and b/assets/setup-save-rule.png differ diff --git a/assets/setup-save-rule@2x.png b/assets/setup-save-rule@2x.png index b9a7d7d..1ed09b9 100644 Binary files a/assets/setup-save-rule@2x.png and b/assets/setup-save-rule@2x.png differ diff --git a/assets/setup-side-panel.png b/assets/setup-side-panel.png new file mode 100644 index 0000000..cea0929 Binary files /dev/null and b/assets/setup-side-panel.png differ diff --git a/assets/setup-side-panel@2x.png b/assets/setup-side-panel@2x.png new file mode 100644 index 0000000..78186cf Binary files /dev/null and b/assets/setup-side-panel@2x.png differ diff --git a/assets/setup-whole-word.png b/assets/setup-whole-word.png index 06f9d14..cf5993f 100644 Binary files a/assets/setup-whole-word.png and b/assets/setup-whole-word.png differ diff --git a/assets/setup-whole-word@2x.png b/assets/setup-whole-word@2x.png index bfd03d2..2dec02f 100644 Binary files a/assets/setup-whole-word@2x.png and b/assets/setup-whole-word@2x.png differ diff --git a/assets/setup-wordpress.png b/assets/setup-wordpress.png index f262e8a..fe7a0a0 100644 Binary files a/assets/setup-wordpress.png and b/assets/setup-wordpress.png differ diff --git a/assets/setup-wordpress@2x.png b/assets/setup-wordpress@2x.png index ec8970b..270c831 100644 Binary files a/assets/setup-wordpress@2x.png and b/assets/setup-wordpress@2x.png differ diff --git a/assets/style.css b/assets/style.css index dfbe535..d3e1dd8 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1,443 +1,542 @@ -/* Palette. These are the greys the popup used before the redesign; the redesign's layout is - kept, but its green-and-white colouring is not. Change a value here rather than in the - rules below. */ -:root { - --sr-bg: #eee; - --sr-surface: #e2e2e2; - --sr-text: #444; - --sr-muted: #6b6b6b; - --sr-accent: #333; - --sr-accent-soft: #9a9a9a; - --sr-border: #cfcfcf; - --sr-hover-bg: #ccc; - --sr-button-bg: #ddd; - --sr-button-bg-hover: #ccc; - --sr-control: #6c757d; - - /* Spacing scale. Every gap in the popup should come from one of these, so that the - vertical rhythm stays consistent as sections are added. */ - --sr-space-xs: 4px; - --sr-space-sm: 8px; - --sr-space-md: 12px; - --sr-space-lg: 16px; - - /* Horizontal padding of the popup's panels, so the header, body and footer line up */ - --sr-gutter: 16px; -} - -html { - height: fit-content; -} - -body, -body div table { - font-family: 'Open Sans', sans-serif; - background-color: var(--sr-bg); - font-size: 13px; - text-shadow: 0 1px rgba(255, 255, 255, 0.5); - /* width: 320px; */ - height: fit-content; - color: var(--sr-text); -} - -/* Header and footer panels. Replaces bootstrap's .bg-white so the panels read as a slightly - darker grey than the body rather than as white. */ -.bg-surface { - background-color: var(--sr-surface); -} - -textarea { - height: calc(1.5em + 0.75rem + calc(var(--bs-border-width))); - overflow-y: hidden; -} - -button { - width: 100%; -} - -a { - color: #333; - text-decoration: none; -} - -#loader { - display: none; - width: 320px; -} - -/* History list. Each entry used to be a bare "term -> term" string on an alternating white and - grey stripe, which neither matched the popup's palette nor coped with a term long enough to - run past the edge of the popup. Entries are now cards that wrap. */ - -/* Chrome caps a popup at 600px and then scrolls the whole thing, header and buttons included. - Ten entries are taller than that, so the list scrolls within itself and the rest of the panel - stays put. */ -#historyList { - display: flex; - flex-direction: column; - gap: var(--sr-space-xs); - max-height: 380px; - overflow-y: auto; - margin: 0; - padding-inline-start: 0; -} - -.history-item { - list-style-type: none; - margin: 0; - padding: 6px var(--sr-space-sm); - font-size: 13px; - background-color: var(--sr-surface); - border: 1px solid var(--sr-border); - border-radius: 6px; - cursor: pointer; -} - -.history-item:hover { - background-color: var(--sr-hover-bg); - border-color: var(--sr-accent-soft); -} - -.history-item__terms { - display: flex; - flex-wrap: wrap; - align-items: baseline; - gap: 0 6px; -} - -/* A search term can be any length and need not contain a space, so it has to be allowed to break - mid-word rather than push the popup wider */ -.history-item__term { - overflow-wrap: anywhere; - min-width: 0; -} - -.history-item__term--empty { - color: var(--sr-muted); - font-style: italic; -} - -.history-item__arrow { - color: var(--sr-muted); -} - -.history-item__options { - margin-top: 2px; - font-size: 11px; - color: var(--sr-muted); -} - -.history-empty { - margin: 0 0 var(--sr-space-sm); - color: var(--sr-muted); -} - -#content { - width: 320px; -} - -/* Was display:table, which shrink-wraps to its content rather than filling the popup width */ -#body { - display: block; -} - -/* Main body panel between the header and footer. - Was a fixed 445px tall with a white swirl background image. The fixed height left a band of - dead space under the short sections (Settings, About) and overflowed the long ones, so the - panel now sizes to its content and Chrome sizes the popup to match. */ -.background-img { - background-color: var(--sr-bg); - padding: var(--sr-space-lg) var(--sr-gutter); -} - -/* The header, body and footer panels are full-bleed, so each supplies its own gutter. Keeping - them equal is what makes the three panels line up down the left and right edges. */ -#header > div, -#footer > div { - padding-left: var(--sr-gutter); - padding-right: var(--sr-gutter); -} - -/* Form groups set the vertical rhythm of the search form */ -#searchReplaceForm .form-group + .form-group, -.swap-row + .form-group { - margin-top: var(--sr-space-md); -} - -/* Reserves no space until popup.ts renders a hint into it */ -#hints:empty { - display: none; -} - -#searchReplaceForm label { - display: inline-block; - margin-bottom: var(--sr-space-xs); -} - -/* Row holding the match count and the swap button, between the two text areas */ -.swap-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--sr-space-sm); - margin-top: var(--sr-space-sm); -} - -/* The count gives up its space first: it can wrap, where the Swap button label cannot */ -#searchTermCount { - color: var(--sr-muted); - text-align: left; - min-width: 0; -} - -#swapTerms { - flex: none; - width: auto; - max-width: 60%; - padding: 2px var(--sr-space-sm); - font-size: 12px; - line-height: 1.6; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* The option checkboxes read as one group, set apart from the fields above them. - The spacing is deliberately tight: in German, Spanish and Russian the longer labels wrap onto - a second line, and the form was tipping over the 600px at which Chrome starts scrolling the - whole popup. */ -.options-group { - margin-top: var(--sr-space-md); - padding-top: var(--sr-space-md); - border-top: 1px solid var(--sr-border); -} - -.options-group .form-check { - margin-bottom: 2px; - min-height: auto; -} - -/* Bootstrap's .mb-3 on the last checkbox is 16px of dead space above the footer */ -.options-group .form-check:last-child { - margin-bottom: 0 !important; -} - -.options-group .form-check-label { - cursor: pointer; -} - -/* Back link at the top of the Settings, About and History sections */ -#settingSection #back, -#aboutSection #back, -#historySection #back { - align-items: center; - gap: var(--sr-space-xs); - margin-bottom: var(--sr-space-sm); -} - -#settingSection h5, -#aboutSection h5, -#historySection h5 { - font-size: 16px; - margin-bottom: var(--sr-space-md); -} - -/* About panel */ - -.about-identity { - display: flex; - align-items: center; - gap: var(--sr-space-md); - margin-bottom: var(--sr-space-md); -} - -.about-version { - color: var(--sr-muted); -} - -#aboutSection p { - margin-bottom: var(--sr-space-md); -} - -/* Author and licence, as label/value pairs rather than prose */ -.about-meta { - display: grid; - grid-template-columns: auto 1fr; - gap: var(--sr-space-xs) var(--sr-space-md); - margin: 0 0 var(--sr-space-lg); - padding-top: var(--sr-space-md); - border-top: 1px solid var(--sr-border); -} - -.about-meta dt { - color: var(--sr-muted); - font-weight: 400; -} - -.about-meta dd { - margin: 0; -} - -.about-links { - margin: 0; - padding: var(--sr-space-md) 0 0; - list-style: none; - border-top: 1px solid var(--sr-border); -} - -.about-links li { - padding: var(--sr-space-xs) 0; -} - -.about-links a, -.about-meta a { - text-decoration: underline; - text-underline-offset: 2px; -} - -.about-links a:hover, -.about-meta a:hover { - color: #000; -} - -.shadow-top { - box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1); /* Adjust values as needed */ -} - -.custom-rounded-top { - border-top-left-radius: 12px; - border-top-right-radius: 12px; -} - -/* Title block in the header: logo and name on one baseline, never wrapped. It is allowed to - shrink so that a long translation of the extension name is trimmed rather than pushing the - row of icons past the edge of the popup. */ -.display-flow { - display: flex; - align-items: center; - gap: var(--sr-space-sm); - min-width: 0; - white-space: nowrap; -} - -.display-flow span { - overflow: hidden; - text-overflow: ellipsis; -} - -.cursor-point { - cursor: pointer; -} - -.header-text { - font-size: 22px; -} - -/* The header's icon buttons, spaced evenly rather than crowded together, and kept on one row */ -#header form { - align-items: center; - flex-wrap: nowrap; - gap: 6px; -} - -/* The markup carries .mx-1, which bootstrap declares !important, so the gap above would be - added to it and the five icons would wrap onto a second row */ -#header form > .form-group { - margin: 0 !important; -} - -#header form a { - display: flex; - align-items: center; -} - -.icon-selected { - border-bottom: 2px solid var(--sr-accent); - padding-bottom: 2px; -} - -.icon-selected a i svg path { - fill: var(--sr-accent); -} - -/* body section */ - -.bg-button { - background-color: var(--sr-button-bg); - color: var(--sr-text) !important; -} - -.bg-button:hover { - background-color: var(--sr-button-bg-hover) !important; - color: var(--sr-text) !important; -} - -.text-color { - color: var(--sr-accent); -} - -.form-check-input:checked { - background-color: var(--sr-control); - border-color: var(--sr-control); -} - -.content { - display: none; -} - -.content.active { - display: block; -} -/* footer section */ - -#github_version { - display: none; -} - -.custom-rounded-bottom { - border-bottom-left-radius: 12px; - border-bottom-right-radius: 12px; -} - -.h-footer { - min-height: 54px; -} - -/* The two action buttons share the footer evenly and must not wrap: "Replace Next" used to - break onto a second line while "Replace All" stayed on one, leaving them different heights */ -#footer .d-flex { - gap: var(--sr-space-md); -} - -/* The labels have to be allowed to wrap: "Reemplazar Siguiente" and "Заменить следующее" are - wider than half the popup, and nowrap pushed them straight off the right-hand edge. The row - stretches its items, so a button that takes two lines makes both buttons taller rather than - leaving them mismatched, which is what the nowrap was there to prevent. */ -#replaceNext, -#replaceAll { - /* `auto` rather than `0` for the basis: the buttons start from the width of their own label - and only share the leftover space, so a longer translation is not forced to wrap while - there is still room in the row */ - flex: 1 1 auto; - /* Without this the blanket `button { width: 100% }` above makes the auto basis resolve to - the full row width for both buttons, so they always shrink back to exact halves */ - width: auto; - min-width: 0; - padding: var(--sr-space-sm) var(--sr-space-sm); - /* Bootstrap's 16px is oversized next to the popup's 13px body text, and the 6% it gives back - is the difference between one line and two for several translations */ - font-size: 15px; - line-height: 1.25; - text-wrap: balance; - overflow-wrap: anywhere; -} - -#clearHistory { - width: auto; - padding: var(--sr-space-xs) var(--sr-space-md); - margin: var(--sr-space-sm) 0 0 auto; -} - -#swapTerms { - text-align: right; -} +/* Palette. These are the greys the popup used before the redesign; the redesign's layout is + kept, but its green-and-white colouring is not. Change a value here rather than in the + rules below. */ +:root { + --sr-bg: #eee; + --sr-surface: #e2e2e2; + --sr-text: #444; + --sr-muted: #6b6b6b; + --sr-accent: #333; + --sr-accent-soft: #9a9a9a; + --sr-border: #cfcfcf; + --sr-hover-bg: #ccc; + --sr-button-bg: #ddd; + --sr-button-bg-hover: #ccc; + --sr-control: #6c757d; + + /* Spacing scale. Every gap in the popup should come from one of these, so that the + vertical rhythm stays consistent as sections are added. */ + --sr-space-xs: 4px; + --sr-space-sm: 8px; + --sr-space-md: 12px; + --sr-space-lg: 16px; + + /* Horizontal padding of the popup's panels, so the header, body and footer line up */ + --sr-gutter: 16px; +} + +html { + height: fit-content; +} + +body, +body div table { + font-family: 'Open Sans', sans-serif; + background-color: var(--sr-bg); + font-size: 13px; + text-shadow: 0 1px rgba(255, 255, 255, 0.5); + /* width: 320px; */ + height: fit-content; + color: var(--sr-text); +} + +/* Header and footer panels. Replaces bootstrap's .bg-white so the panels read as a slightly + darker grey than the body rather than as white. */ +.bg-surface { + background-color: var(--sr-surface); +} + +textarea { + height: calc(1.5em + 0.75rem + calc(var(--bs-border-width))); + overflow-y: hidden; +} + +button { + width: 100%; +} + +a { + color: #333; + text-decoration: none; +} + +#loader { + display: none; + width: 320px; +} + +/* History list. Each entry used to be a bare "term -> term" string on an alternating white and + grey stripe, which neither matched the popup's palette nor coped with a term long enough to + run past the edge of the popup. Entries are now cards that wrap. */ + +/* Chrome caps a popup at 600px and then scrolls the whole thing, header and buttons included. + Ten entries are taller than that, so the list scrolls within itself and the rest of the panel + stays put. */ +#historyList { + display: flex; + flex-direction: column; + gap: var(--sr-space-xs); + max-height: 330px; + overflow-y: auto; + margin: 0; + padding-inline-start: 0; +} + +.history-item { + list-style-type: none; + margin: 0; + padding: 6px var(--sr-space-sm); + font-size: 13px; + background-color: var(--sr-surface); + border: 1px solid var(--sr-border); + border-radius: 6px; + cursor: pointer; +} + +.history-item:hover { + background-color: var(--sr-hover-bg); + border-color: var(--sr-accent-soft); +} + +.history-item__terms { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 0 6px; +} + +/* A search term can be any length and need not contain a space, so it has to be allowed to break + mid-word rather than push the popup wider */ +.history-item__term { + overflow-wrap: anywhere; + min-width: 0; +} + +.history-item__term--empty { + color: var(--sr-muted); + font-style: italic; +} + +.history-item__arrow { + color: var(--sr-muted); +} + +.history-item__options { + margin-top: 2px; + font-size: 11px; + color: var(--sr-muted); +} + +.history-empty { + margin: 0 0 var(--sr-space-sm); + color: var(--sr-muted); +} + +#content { + width: 320px; +} + +/* Side panel. + Chrome sizes the toolbar popup to the document, which is why #content carries a fixed width. + The panel is the other way round: it is as tall as the browser window and the user can drag + its edge, so the same document has to fill whatever it is given instead of sitting in a + 320px column with grey either side. popup.ts adds .side-panel when the document is loaded + with ?surface=sidepanel. */ +body.side-panel, +body.side-panel #content, +body.side-panel #loader { + width: 100%; +} + +body.side-panel { + min-width: 320px; +} + +/* Pins the action buttons to the bottom of the panel rather than leaving them floating halfway + up it, which is where they would otherwise sit in a window-height panel */ +body.side-panel #content { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +body.side-panel .row.bg-surface { + flex: 1; +} + +/* The list has the whole height of the panel to play with, so it does not need the cap that + keeps it inside the popup's 600px */ +body.side-panel #historyList { + max-height: none; +} + +/* Keeps the text columns readable when the panel is dragged out wide */ +body.side-panel #body, +body.side-panel #header > div, +body.side-panel #footer > div { + max-width: 640px; + margin-inline: auto; + width: 100%; +} + +/* Was display:table, which shrink-wraps to its content rather than filling the popup width */ +#body { + display: block; +} + +/* Main body panel between the header and footer. + Was a fixed 445px tall with a white swirl background image. The fixed height left a band of + dead space under the short sections (Settings, About) and overflowed the long ones, so the + panel now sizes to its content and Chrome sizes the popup to match. */ +.background-img { + background-color: var(--sr-bg); + padding: var(--sr-space-md) var(--sr-gutter); +} + +/* The header, body and footer panels are full-bleed, so each supplies its own gutter. Keeping + them equal is what makes the three panels line up down the left and right edges. */ +#header > div, +#footer > div { + padding-left: var(--sr-gutter); + padding-right: var(--sr-gutter); +} + +/* Form groups set the vertical rhythm of the search form */ +#searchReplaceForm .form-group + .form-group, +.swap-row + .form-group { + margin-top: var(--sr-space-md); +} + +/* Reserves no space until popup.ts renders a hint into it */ +#hints:empty { + display: none; +} + +#searchReplaceForm label { + display: inline-block; + margin-bottom: var(--sr-space-xs); +} + +/* Row holding the match count and the swap button, between the two text areas */ +.swap-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--sr-space-sm); + margin-top: var(--sr-space-sm); +} + +/* The count gives up its space first: it can wrap, where the Swap button label cannot */ +#searchTermCount { + color: var(--sr-muted); + text-align: left; + min-width: 0; +} + +#swapTerms { + flex: none; + width: auto; + max-width: 60%; + padding: 2px var(--sr-space-sm); + font-size: 12px; + line-height: 1.6; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* The option checkboxes read as one group, set apart from the fields above them. + The spacing is deliberately tight: in German, Spanish and Russian the longer labels wrap onto + a second line, and the form was tipping over the 600px at which Chrome starts scrolling the + whole popup. */ +.options-group { + margin-top: var(--sr-space-md); + padding-top: var(--sr-space-md); + border-top: 1px solid var(--sr-border); +} + +.options-group .form-check { + margin-bottom: 2px; + min-height: auto; +} + +/* Bootstrap's .mb-3 on the last checkbox is 16px of dead space above the footer */ +.options-group .form-check:last-child { + margin-bottom: 0 !important; +} + +.options-group .form-check-label { + cursor: pointer; +} + +/* Back link at the top of the Settings, About and History sections */ +#settingSection #back, +#aboutSection #back, +#historySection #back { + align-items: center; + gap: var(--sr-space-xs); + margin-bottom: var(--sr-space-sm); +} + +/* A labelled control in the Settings panel */ +.setting + .setting { + margin-top: var(--sr-space-lg); +} + +.setting__label { + display: block; + margin-bottom: var(--sr-space-xs); +} + +.setting__hint { + margin: var(--sr-space-xs) 0 0; + color: var(--sr-muted); +} + +#settingSection h5, +#aboutSection h5, +#historySection h5 { + font-size: 16px; + margin-bottom: var(--sr-space-md); +} + +/* About panel */ + +.about-identity { + display: flex; + align-items: center; + gap: var(--sr-space-md); + margin-bottom: var(--sr-space-md); +} + +.about-version { + color: var(--sr-muted); +} + +#aboutSection p { + margin-bottom: var(--sr-space-md); +} + +/* Author and licence, as label/value pairs rather than prose */ +.about-meta { + display: grid; + grid-template-columns: auto 1fr; + gap: var(--sr-space-xs) var(--sr-space-md); + margin: 0 0 var(--sr-space-lg); + padding-top: var(--sr-space-md); + border-top: 1px solid var(--sr-border); +} + +.about-meta dt { + color: var(--sr-muted); + font-weight: 400; +} + +.about-meta dd { + margin: 0; +} + +.about-links { + margin: 0; + padding: var(--sr-space-md) 0 0; + list-style: none; + border-top: 1px solid var(--sr-border); +} + +.about-links li { + padding: var(--sr-space-xs) 0; +} + +.about-links a, +.about-meta a { + text-decoration: underline; + text-underline-offset: 2px; +} + +.about-links a:hover, +.about-meta a:hover { + color: #000; +} + +.shadow-top { + box-shadow: 0 -2px 4px rgba(0, 0, 0, 0.1); /* Adjust values as needed */ +} + +.custom-rounded-top { + border-top-left-radius: 12px; + border-top-right-radius: 12px; +} + +/* Title block in the header: logo and name on one baseline, never wrapped. The ellipsis is a + safety net for a translation longer than the popup; it has the whole row to itself. */ +.display-flow { + display: flex; + align-items: center; + gap: var(--sr-space-sm); + min-width: 0; + white-space: nowrap; +} + +.display-flow span { + overflow: hidden; + text-overflow: ellipsis; +} + +.cursor-point { + cursor: pointer; +} + +.header-text { + font-size: 22px; +} + +/* Header: the title and the two reference icons on one line, then the panels and actions as a + tab strip beneath. + The six icons used to share the title's line, which at 320px left them crowded and the title + truncated. Splitting them by what they do — help and about only ever open documentation, + the rest change what you are looking at — gives the tabs the whole width to spread across. */ +.header-bar { + padding-top: var(--sr-space-sm); +} + +.header-title { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--sr-space-sm); +} + +.header-reference { + display: flex; + align-items: center; + flex: none; + gap: var(--sr-space-sm); +} + +.header-tabs { + display: flex; + margin-top: 6px; + /* Full-bleed, cancelling the gutter .header-bar applies, so the strip reads as a tab bar + rather than a row of icons floating inside the panel */ + margin-left: calc(var(--sr-gutter) * -1); + margin-right: calc(var(--sr-gutter) * -1); + border-top: 1px solid var(--sr-border); +} + +/* Equal shares of the row, so the icons land on a regular rhythm however many there are */ +.header-tabs > .form-group { + flex: 1 1 0; + display: flex; + justify-content: center; + /* The markup carries .mx-1, which bootstrap declares !important */ + margin: 0 !important; + padding: 6px 0 4px; + border-bottom: 2px solid transparent; +} + +.header-tabs > .form-group:hover { + background-color: rgba(0, 0, 0, 0.04); +} + +/* The underline marking the open panel spans the whole tab rather than just the glyph */ +.header-tabs > .icon-selected { + border-bottom-color: var(--sr-accent); + padding-bottom: 4px; +} + +.header-title a, +.header-tabs a { + display: flex; + align-items: center; +} + +.icon-selected { + border-bottom: 2px solid var(--sr-accent); + padding-bottom: 2px; +} + +.icon-selected a i svg path { + fill: var(--sr-accent); +} + +/* body section */ + +.bg-button { + background-color: var(--sr-button-bg); + color: var(--sr-text) !important; +} + +.bg-button:hover { + background-color: var(--sr-button-bg-hover) !important; + color: var(--sr-text) !important; +} + +.text-color { + color: var(--sr-accent); +} + +.form-check-input:checked { + background-color: var(--sr-control); + border-color: var(--sr-control); +} + +.content { + display: none; +} + +.content.active { + display: block; +} +/* footer section */ + +#github_version { + display: none; +} + +.custom-rounded-bottom { + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; +} + +.h-footer { + min-height: 54px; +} + +/* The two action buttons share the footer evenly and must not wrap: "Replace Next" used to + break onto a second line while "Replace All" stayed on one, leaving them different heights */ +#footer .d-flex { + gap: var(--sr-space-md); +} + +/* The labels have to be allowed to wrap: "Reemplazar Siguiente" and "Заменить следующее" are + wider than half the popup, and nowrap pushed them straight off the right-hand edge. The row + stretches its items, so a button that takes two lines makes both buttons taller rather than + leaving them mismatched, which is what the nowrap was there to prevent. */ +#replaceNext, +#replaceAll { + /* `auto` rather than `0` for the basis: the buttons start from the width of their own label + and only share the leftover space, so a longer translation is not forced to wrap while + there is still room in the row */ + flex: 1 1 auto; + /* Without this the blanket `button { width: 100% }` above makes the auto basis resolve to + the full row width for both buttons, so they always shrink back to exact halves */ + width: auto; + min-width: 0; + padding: var(--sr-space-sm) var(--sr-space-sm); + /* Bootstrap's 16px is oversized next to the popup's 13px body text, and the 6% it gives back + is the difference between one line and two for several translations */ + font-size: 15px; + line-height: 1.25; + text-wrap: balance; + overflow-wrap: anywhere; +} + +#clearHistory { + width: auto; + padding: var(--sr-space-xs) var(--sr-space-md); + margin: var(--sr-space-sm) 0 0 auto; +} + +#swapTerms { + text-align: right; +} diff --git a/manifest.json b/manifest.json index 4d8554b..be08f0f 100644 --- a/manifest.json +++ b/manifest.json @@ -9,7 +9,7 @@ }, "commands": { "toggle-popup": { - "description": "Toggle Search and Replace popup", + "description": "Open Search and Replace", "global": true, "suggested_key": { "default": "Ctrl+Shift+S" @@ -28,7 +28,10 @@ "name": "Search and Replace", "description": "__MSG_ext_description__", "manifest_version": 3, - "permissions": ["activeTab", "storage", "notifications", "scripting"], + "permissions": ["activeTab", "storage", "notifications", "scripting", "sidePanel"], + "side_panel": { + "default_path": "assets/popup.html" + }, "host_permissions": ["http://*/*", "https://*/*"], "update_url": "http://clients2.google.com/service/update2/crx", "version": "2.4.1", diff --git a/scripts/screenshots.mjs b/scripts/screenshots.mjs index 85e2c62..747c0d5 100644 --- a/scripts/screenshots.mjs +++ b/scripts/screenshots.mjs @@ -316,6 +316,42 @@ async function popupHarness(shot) { return body.replace('', harness) } +/** The Settings panel, showing the popup-or-side-panel preference */ +async function settingsHarness() { + const popup = await readFile(resolve(assets, 'popup.html'), 'utf8') + const body = popup + .replace('', '') + .replace(/
        [\s\S]*?<\/div>\s*<\/body>/, '') + + const harness = ` + + + ` + + return body.replace('', harness) +} + /** * The saved rules page. The card markup mirrors instanceToHTML() in src/options.ts, which * cannot be imported here because it is TypeScript and needs the chrome.* APIs to run. @@ -491,6 +527,10 @@ async function main() { await shoot(shot.name, await popupHarness(shot), POPUP_WIDTH) } + if (wanted('setup-side-panel')) { + await shoot('setup-side-panel', await settingsHarness(), POPUP_WIDTH) + } + if (wanted('saved-rules')) { await shoot('saved-rules', rulesHarness(), 720) } diff --git a/src/background.ts b/src/background.ts index c109000..e72ad57 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,4 +1,5 @@ // Storing and retrieving popup values +import { OPEN_IN_KEY, applyStoredOpenIn, openConfiguredSurface } from './background/surface' import { SearchReplaceBackgroundMessage, SearchReplaceResponse } from './types' import { listenerAdmin } from './background/admin' import { listenerApplySavedInstances } from './background/saved' @@ -44,6 +45,29 @@ chrome.runtime.onMessage.addListener(function (msg: SearchReplaceResponse, sende // Listen for the extension being installed or updated chrome.runtime.onInstalled.addListener(function (details) { listenerInstall(details) + applyStoredOpenIn().catch((error) => console.error('BACKGROUND: Could not apply the open-in setting', error)) +}) + +// Neither the cleared popup nor the side panel behaviour survives a browser restart +chrome.runtime.onStartup.addListener(function () { + applyStoredOpenIn().catch((error) => console.error('BACKGROUND: Could not apply the open-in setting', error)) +}) + +// Follow the setting when it is changed, here or on another synced machine +chrome.storage.onChanged.addListener(function (changes, areaName) { + if (areaName === 'sync' && OPEN_IN_KEY in changes) { + applyStoredOpenIn().catch((error) => console.error('BACKGROUND: Could not apply the open-in setting', error)) + } +}) + +// The keyboard shortcut. Until now the command had no handler at all, so Ctrl+Shift+S did nothing. +chrome.commands.onCommand.addListener(function (command, tab) { + if (command !== 'toggle-popup') { + return + } + openConfiguredSurface(tab?.windowId).catch((error) => + console.error('BACKGROUND: Could not open from the keyboard shortcut', error) + ) }) // Listen for tab updates and apply any saved instances diff --git a/src/background/surface.test.ts b/src/background/surface.test.ts new file mode 100644 index 0000000..0cf9fa3 --- /dev/null +++ b/src/background/surface.test.ts @@ -0,0 +1,143 @@ +import { ChromeMock, installChromeMock } from '../testing/chromeMock' +import { + DEFAULT_OPEN_IN, + OPEN_IN_KEY, + SIDE_PANEL_PATH, + applyOpenIn, + applyStoredOpenIn, + getOpenIn, + openConfiguredSurface, + sidePanelAvailable, +} from './surface' +import { beforeEach, describe, expect, test } from 'vitest' + +let mock: ChromeMock + +beforeEach(() => { + mock = installChromeMock() +}) + +/** Chrome 113 and older, where chrome.sidePanel does not exist */ +function withoutSidePanel() { + delete (mock.chrome as { sidePanel?: unknown }).sidePanel +} + +describe('getOpenIn', () => { + test('defaults to the popup, so an existing install behaves as it always has', async () => { + expect(await getOpenIn()).toBe('popup') + expect(DEFAULT_OPEN_IN).toBe('popup') + }) + + test('returns the stored preference', async () => { + mock.sync.data[OPEN_IN_KEY] = 'sidePanel' + + expect(await getOpenIn()).toBe('sidePanel') + }) + + test('ignores a value that is neither surface', async () => { + mock.sync.data[OPEN_IN_KEY] = 'somewhere-else' + + expect(await getOpenIn()).toBe('popup') + }) + + test('stays on the popup where there is no side panel API to use', async () => { + mock.sync.data[OPEN_IN_KEY] = 'sidePanel' + withoutSidePanel() + + expect(await getOpenIn()).toBe('popup') + }) +}) + +describe('applyOpenIn', () => { + test('registers the popup so the toolbar button opens it', async () => { + await applyOpenIn('popup') + + expect(mock.surface.setPopup).toEqual(['assets/popup.html']) + expect(mock.surface.panelBehaviour).toEqual([false]) + }) + + test('clears the popup so the toolbar button reaches the side panel', async () => { + // While a popup is registered Chrome opens it and never fires action.onClicked, so + // openPanelOnActionClick would have nothing to act on + await applyOpenIn('sidePanel') + + expect(mock.surface.setPopup).toEqual(['']) + expect(mock.surface.panelBehaviour).toEqual([true]) + }) + + test('points the side panel at the shared popup document', async () => { + await applyOpenIn('sidePanel') + + expect(mock.surface.panelOptions).toEqual([{ path: SIDE_PANEL_PATH, enabled: true }]) + expect(SIDE_PANEL_PATH.startsWith('assets/popup.html')).toBe(true) + }) + + test('keeps the popup registered when asked for a side panel Chrome cannot show', async () => { + withoutSidePanel() + + await applyOpenIn('sidePanel') + + expect(mock.surface.setPopup).toEqual(['assets/popup.html']) + }) + + test('does not throw when the browser refuses the call', async () => { + mock.chrome.action.setPopup = () => Promise.reject(new Error('nope')) + + await expect(applyOpenIn('popup')).resolves.toBeUndefined() + }) +}) + +describe('applyStoredOpenIn', () => { + test('applies whatever is in storage', async () => { + mock.sync.data[OPEN_IN_KEY] = 'sidePanel' + + await applyStoredOpenIn() + + expect(mock.surface.setPopup).toEqual(['']) + }) +}) + +describe('openConfiguredSurface', () => { + test('opens the side panel in the window the shortcut was used in', async () => { + mock.sync.data[OPEN_IN_KEY] = 'sidePanel' + + await openConfiguredSurface(12) + + expect(mock.surface.opened).toEqual([12]) + expect(mock.surface.openPopupCalls).toBe(0) + }) + + test('opens the popup when that is the chosen surface', async () => { + await openConfiguredSurface(12) + + expect(mock.surface.openPopupCalls).toBe(1) + expect(mock.surface.opened).toEqual([]) + }) + + test('falls back to the popup when there is no window to open a panel in', async () => { + mock.sync.data[OPEN_IN_KEY] = 'sidePanel' + + await openConfiguredSurface(undefined) + + expect(mock.surface.opened).toEqual([]) + expect(mock.surface.openPopupCalls).toBe(1) + }) + + test('does nothing on a Chrome without action.openPopup rather than throwing', async () => { + delete (mock.chrome.action as { openPopup?: unknown }).openPopup + + await expect(openConfiguredSurface(12)).resolves.toBeUndefined() + }) +}) + +describe('sidePanelAvailable', () => { + test('is true where the API exists', () => { + expect(sidePanelAvailable()).toBe(true) + }) + + test('is false on Chrome 113 and older', () => { + withoutSidePanel() + + expect(sidePanelAvailable()).toBe(false) + }) +}) diff --git a/src/background/surface.ts b/src/background/surface.ts new file mode 100644 index 0000000..2333d03 --- /dev/null +++ b/src/background/surface.ts @@ -0,0 +1,103 @@ +// Where the extension opens: the toolbar popup, or Chrome's side panel. +// +// The popup closes the moment anything else takes focus, which makes it awkward to work +// through a page: clicking into the page to check a replacement dismisses it, and the terms +// have to be typed again. The side panel stays open, so this is a preference rather than a +// replacement (issue #121). + +import { OpenIn } from '../types' + +export const OPEN_IN_KEY = 'openIn' + +/** Unchanged behaviour for anyone who never visits Settings */ +export const DEFAULT_OPEN_IN: OpenIn = 'popup' + +const POPUP_PATH = 'assets/popup.html' + +/** + * The same document serves both surfaces, as popup.html and popup.js and style.css between + * them are the whole UI; the query string is the only thing that tells the two apart at load + * time. popup.ts keys its layout off it, because a side panel is as tall as the browser window + * and the user can drag it wider, where the popup is a fixed 320px box. + * + * It is applied here rather than in the manifest on purpose. `side_panel.default_path` in the + * manifest stays a plain path: a manifest Chrome rejects stops the extension loading at all, + * and that is not a risk worth taking for a query string. setOptions runs on install, on + * startup and whenever the preference changes, so it is always in place before the panel can be + * opened. If it somehow were not, the panel still renders — just in the popup's narrow column. + */ +export const SIDE_PANEL_PATH = `${POPUP_PATH}?surface=sidepanel` + +/** + * `chrome.sidePanel` arrived in 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. + */ +export function sidePanelAvailable(): boolean { + return typeof chrome !== 'undefined' && chrome.sidePanel !== undefined +} + +export async function getOpenIn(): Promise { + if (!sidePanelAvailable()) { + return DEFAULT_OPEN_IN + } + const stored = await chrome.storage.sync.get({ [OPEN_IN_KEY]: DEFAULT_OPEN_IN }) + return stored[OPEN_IN_KEY] === 'sidePanel' ? 'sidePanel' : DEFAULT_OPEN_IN +} + +/** + * Point the toolbar button at the chosen surface. + * + * 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 side panel would only ever be reachable from Chrome's own side panel menu. + * + * Neither setting survives a browser restart, so this runs on startup as well as on install and + * whenever the preference changes. + */ +export async function applyOpenIn(openIn: OpenIn): Promise { + const useSidePanel = openIn === 'sidePanel' && sidePanelAvailable() + + try { + await chrome.action.setPopup({ popup: useSidePanel ? '' : POPUP_PATH }) + } catch (error) { + console.error('BACKGROUND: Could not set the toolbar button behaviour', error) + } + + if (!sidePanelAvailable()) { + return + } + + try { + await chrome.sidePanel.setOptions({ path: SIDE_PANEL_PATH, enabled: true }) + await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: useSidePanel }) + } catch (error) { + console.error('BACKGROUND: Could not set the side panel behaviour', error) + } +} + +export async function applyStoredOpenIn(): Promise { + await applyOpenIn(await getOpenIn()) +} + +/** + * Opens whichever surface is configured, for the keyboard shortcut. + * + * `sidePanel.open` and `action.openPopup` both require a user gesture; a command counts as one, + * but only for as long as the handler is on the stack, so this must not be deferred behind + * anything slow. + */ +export async function openConfiguredSurface(windowId: number | undefined): Promise { + const openIn = await getOpenIn() + + if (openIn === 'sidePanel' && sidePanelAvailable() && windowId !== undefined) { + await chrome.sidePanel.open({ windowId }) + return + } + + // Generally available from Chrome 127. On anything older the shortcut cannot open the popup, + // which is no worse than before: the command had no handler at all and did nothing. + if (typeof chrome.action.openPopup === 'function') { + await chrome.action.openPopup() + } +} diff --git a/src/popup.ts b/src/popup.ts index 57676c1..3c94829 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -5,9 +5,11 @@ import { REPLACE_TERM_INPUT_ID, SEARCH_TERM_INPUT_ID, } from './popup/constants' +import { DEFAULT_OPEN_IN, OPEN_IN_KEY, sidePanelAvailable } from './background/surface' import { Hint, HintPreferences, + OpenIn, SearchReplaceActions, SearchReplaceBackgroundMessage, SearchReplaceCheckboxNames, @@ -42,6 +44,64 @@ enum ContentType { const RELEASE_NOTES_URL = 'https://github.com/forgetso/search-replace/releases/tag' +/** + * Whether this document is being shown in the side panel rather than the toolbar popup. + * + * Both surfaces load assets/popup.html; background/surface.ts registers the side panel with a + * query string so the two can be told apart. Defaulting to the popup means that if the query + * were ever lost the panel would still render, just at the popup's fixed width. + */ +function isSidePanel(): boolean { + return new URLSearchParams(globalThis.location.search).get('surface') === 'sidepanel' +} + +/** + * The window this document belongs to, looked up once on load. + * + * `sidePanel.open` has to be called while the user gesture that triggered it is still in effect, + * and awaiting `windows.getCurrent()` inside the click handler risks spending it, so the id is + * fetched ahead of time and used synchronously. + */ +let currentWindowId: number | undefined + +/** + * Opens the side panel from the popup, and closes it from within itself. + * + * There is no `sidePanel.close()`; closing the panel is done by the panel document closing its + * own window, which is also how the popup dismisses itself once the panel has taken over. + */ +function setUpSidePanelToggle(translationFn: TranslationProxy) { + const toggle = document.getElementById('sidePanelToggle') + if (!toggle || !sidePanelAvailable()) { + return + } + toggle.classList.remove('d-none') + + const showingPanel = isSidePanel() + toggle.querySelector('.side-panel-open')?.classList.toggle('d-none', showingPanel) + toggle.querySelector('.side-panel-close')?.classList.toggle('d-none', !showingPanel) + + const link = toggle.querySelector('a') + if (link) { + link.title = translationFn(showingPanel ? 'close_side_panel' : 'open_side_panel') + } + + toggle.addEventListener('click', function (event) { + event.preventDefault() + if (showingPanel) { + globalThis.close() + return + } + if (currentWindowId === undefined) { + return + } + chrome.sidePanel + .open({ windowId: currentWindowId }) + .then(() => globalThis.close()) + .catch((error) => console.error('POPUP: Could not open the side panel', error)) + }) +} + function getSearchTermElement() { return document.getElementById(SEARCH_TERM_INPUT_ID) } @@ -56,6 +116,21 @@ window.addEventListener('DOMContentLoaded', async function () { const langData = await getTranslation() const translationFn = createTranslationProxy(langData) + if (isSidePanel()) { + // The popup is a fixed 320px box; the panel is as tall as the window and the user can + // drag it wider, so the layout has to fill whatever it is given + document.body.classList.add('side-panel') + watchActiveTab(translationFn) + } + + if (sidePanelAvailable()) { + chrome.windows + .getCurrent() + .then((currentWindow) => (currentWindowId = currentWindow.id)) + .catch((error) => console.error('POPUP: Could not identify the current window', error)) + } + setUpSidePanelToggle(translationFn) + // Update popup version number and GitHub link dynamically with manifest.version for (const id of ['version_number', 'github_version_number']) { const versionNumberElement = document.getElementById(id) @@ -237,6 +312,7 @@ async function loadContent(contentType: ContentType) { inactiveButton(aboutBtn) activeButton(settingBtn) await loadLanguageOptions() + await loadOpenInOptions() break case ContentType.About: // pressed about icon hideElement(replaceNext) @@ -303,6 +379,70 @@ function inactiveButton(element: Element | null) { } } +/** + * Recount when the user moves to another tab, or navigates the one they are on. + * + * The popup never needed this: it is thrown away the moment focus leaves it, so it only ever + * describes the tab it was opened over. The side panel outlives both, and a match count left + * over from a tab the user has since switched away from is worse than no count at all. + */ +function watchActiveTab(translationFn: TranslationProxy) { + const recount = () => { + const searchTerm = getSearchTermElement().value + if (searchTerm.length < MIN_SEARCH_TERM_LENGTH) { + setCount(newEmptyResult(), translationFn) + return + } + contentScriptCall('count', getInputValues(false), []).catch((error) => + console.error('POPUP: Could not recount after the tab changed', error) + ) + } + + chrome.tabs.onActivated.addListener(recount) + chrome.tabs.onUpdated.addListener((_tabId, changeInfo, tab) => { + // Only once the new page is there to be counted, and only for the tab on display + if (changeInfo.status === 'complete' && tab.active) { + recount() + } + }) +} + +function newEmptyResult(): SearchReplaceResult { + return { count: { original: 0, replaced: 0 }, replaced: false } +} + +/** + * The popup-or-side-panel preference. Hidden entirely on a Chrome without `chrome.sidePanel`, + * where there is no choice to make. + */ +async function loadOpenInOptions() { + const setting = document.getElementById('openInSetting') + const select = document.getElementById('openInSelect') as HTMLSelectElement | null + if (!setting || !select || !sidePanelAvailable()) { + return + } + setting.classList.remove('d-none') + + const stored = await chrome.storage.sync.get({ [OPEN_IN_KEY]: DEFAULT_OPEN_IN }) + select.value = stored[OPEN_IN_KEY] === 'sidePanel' ? 'sidePanel' : DEFAULT_OPEN_IN + + select.addEventListener('change', function () { + const openIn = this.value as OpenIn + chrome.storage.sync.set({ [OPEN_IN_KEY]: openIn }) + + // Switching to the side panel from inside the popup: open the panel straight away rather + // than leaving the user to work out that they have to click the toolbar button again. + // The change event is the user gesture that sidePanel.open requires, so this cannot be + // deferred until after the storage write settles. + if (openIn === 'sidePanel' && !isSidePanel() && currentWindowId !== undefined) { + chrome.sidePanel + .open({ windowId: currentWindowId }) + .then(() => globalThis.close()) + .catch((error) => console.error('POPUP: Could not open the side panel', error)) + } + }) +} + async function loadLanguageOptions() { const languageSelect = document.getElementById('languageSelect') as HTMLSelectElement if (!languageSelect) return diff --git a/src/testing/chromeMock.ts b/src/testing/chromeMock.ts index d1986f9..336d89f 100644 --- a/src/testing/chromeMock.ts +++ b/src/testing/chromeMock.ts @@ -100,9 +100,25 @@ export interface ChromeMock { injectionDoesNotHelp: Set /** Set to make `chrome.tabs.sendMessage` reject with something other than a missing receiver */ tabMessageError?: Error + /** Calls recorded from the action, sidePanel and windows APIs */ + surface: SurfaceCalls chrome: typeof chrome } +/** What the popup-or-side-panel code did to the browser, for background/surface.ts's tests */ +export interface SurfaceCalls { + /** Every `chrome.action.setPopup` call, in order */ + setPopup: string[] + /** Every `chrome.sidePanel.setPanelBehavior` call, in order */ + panelBehaviour: boolean[] + /** Every `chrome.sidePanel.setOptions` call, in order */ + panelOptions: { path?: string; enabled?: boolean }[] + /** Window ids passed to `chrome.sidePanel.open` */ + opened: (number | undefined)[] + /** How many times `chrome.action.openPopup` was called */ + openPopupCalls: number +} + export function createChromeMock(options: { version?: string } = {}): ChromeMock { const local = createStorageArea() const sync = createStorageArea() @@ -116,6 +132,13 @@ export function createChromeMock(options: { version?: string } = {}): ChromeMock const tabsWithoutContentScript = new Set() const injectionDoesNotHelp = new Set() const state: { tabMessageError?: Error } = {} + const surface: SurfaceCalls = { + setPopup: [], + panelBehaviour: [], + panelOptions: [], + opened: [], + openPopupCalls: 0, + } function respondTo(message: unknown): unknown { const action = (message as { action?: string } | undefined)?.action @@ -166,6 +189,7 @@ export function createChromeMock(options: { version?: string } = {}): ChromeMock }, create: () => Promise.resolve({}), onUpdated: createEvent(), + onActivated: createEvent(), }, scripting: { executeScript: (injection: { target: { tabId: number }; files: string[] }) => { @@ -182,6 +206,38 @@ export function createChromeMock(options: { version?: string } = {}): ChromeMock return Promise.resolve([]) }, }, + action: { + setPopup: (details: { popup: string }) => { + surface.setPopup.push(details.popup) + return Promise.resolve() + }, + openPopup: () => { + surface.openPopupCalls++ + return Promise.resolve() + }, + }, + // Present only where the test arranges it, so that the Chrome-114-and-older path can be + // exercised by deleting it + sidePanel: { + setOptions: (options: { path?: string; enabled?: boolean }) => { + surface.panelOptions.push(options) + return Promise.resolve() + }, + setPanelBehavior: (behavior: { openPanelOnActionClick?: boolean }) => { + surface.panelBehaviour.push(behavior.openPanelOnActionClick === true) + return Promise.resolve() + }, + open: (options: { windowId?: number }) => { + surface.opened.push(options.windowId) + return Promise.resolve() + }, + }, + windows: { + getCurrent: () => Promise.resolve({ id: 7 }), + }, + commands: { + onCommand: createEvent(), + }, i18n: { getAcceptLanguages: (callback: (languages: string[]) => void) => callback(['en']), }, @@ -202,6 +258,7 @@ export function createChromeMock(options: { version?: string } = {}): ChromeMock blockedTabIds, tabsWithoutContentScript, injectionDoesNotHelp, + surface, get tabMessageError() { return state.tabMessageError }, diff --git a/src/types/types.ts b/src/types/types.ts index a1be794..774f820 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -217,3 +217,6 @@ export type ReplaceFunctionReturnType = { } export type PartitionResult = [(HTMLInputElement | HTMLTextAreaElement)[], HTMLElement[]] + +/** Where the extension opens when the toolbar button is used. See background/surface.ts. */ +export type OpenIn = 'popup' | 'sidePanel'