diff --git a/_locales/de/messages.json b/_locales/de/messages.json
index c8663f5..efcf5b9 100644
--- a/_locales/de/messages.json
+++ b/_locales/de/messages.json
@@ -116,9 +116,14 @@
"manage_exceptions": {
"message": "Verwalten"
},
+ "exceptions_mode": { "message": "Modus:" },
+ "exceptions_mode_blacklist": { "message": "Blacklist — auf diesen Sites nicht hervorheben" },
+ "exceptions_mode_whitelist": { "message": "Whitelist — nur auf diesen Sites hervorheben" },
"exceptions_list": {
"message": "Ausnahme-Websites:"
},
+ "exceptions_list_blacklist": { "message": "Sites ausschließen (Blacklist):" },
+ "exceptions_list_whitelist": { "message": "Sites einschließen (Whitelist):" },
"clear_all": {
"message": "Alle löschen"
},
@@ -167,6 +172,15 @@
"total_highlights": {
"message": "Gesamt"
},
+ "total_matches": {
+ "message": "Treffer gesamt"
+ },
+ "group_by_list": {
+ "message": "Nach Liste gruppieren"
+ },
+ "other": {
+ "message": "Sonstige"
+ },
"refresh": {
"message": "Aktualisieren"
},
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index bb23a84..9e563a1 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -122,11 +122,26 @@
"manage_exceptions": {
"message": "Manage"
},
+ "exceptions_mode": {
+ "message": "Mode:"
+ },
+ "exceptions_mode_blacklist": {
+ "message": "Blacklist — don't highlight on these sites"
+ },
+ "exceptions_mode_whitelist": {
+ "message": "Whitelist — only highlight on these sites"
+ },
"exceptions_list": {
"message": "Exception Sites:"
},
+ "exceptions_list_blacklist": {
+ "message": "Sites to exclude (blacklist):"
+ },
+ "exceptions_list_whitelist": {
+ "message": "Sites to include (whitelist):"
+ },
"clear_all": {
- "message": "Clear All"
+ "message": "Clear all"
},
"confirm_clear_exceptions": {
"message": "Are you sure you want to clear all exceptions?"
@@ -173,6 +188,18 @@
"total_highlights": {
"message": "Total"
},
+ "total_matches": {
+ "message": "Total matches"
+ },
+ "group_by_list": {
+ "message": "Group by list"
+ },
+ "other": {
+ "message": "Other"
+ },
+ "exception_domain_placeholder": {
+ "message": "example.com"
+ },
"refresh": {
"message": "Refresh"
},
diff --git a/_locales/es/messages.json b/_locales/es/messages.json
index 2aa4da7..c86c2bf 100644
--- a/_locales/es/messages.json
+++ b/_locales/es/messages.json
@@ -122,9 +122,14 @@
"manage_exceptions": {
"message": "Gestionar"
},
+ "exceptions_mode": { "message": "Modo:" },
+ "exceptions_mode_blacklist": { "message": "Lista negra — no resaltar en estos sitios" },
+ "exceptions_mode_whitelist": { "message": "Lista blanca — solo resaltar en estos sitios" },
"exceptions_list": {
"message": "Sitios de excepción:"
},
+ "exceptions_list_blacklist": { "message": "Sitios a excluir (lista negra):" },
+ "exceptions_list_whitelist": { "message": "Sitios a incluir (lista blanca):" },
"clear_all": {
"message": "Limpiar todo"
},
@@ -173,6 +178,15 @@
"total_highlights": {
"message": "Total"
},
+ "total_matches": {
+ "message": "Coincidencias totales"
+ },
+ "group_by_list": {
+ "message": "Agrupar por lista"
+ },
+ "other": {
+ "message": "Otros"
+ },
"refresh": {
"message": "Actualizar"
},
diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json
index e6837fd..eec0a70 100644
--- a/_locales/fr/messages.json
+++ b/_locales/fr/messages.json
@@ -122,9 +122,14 @@
"manage_exceptions": {
"message": "Gérer"
},
+ "exceptions_mode": { "message": "Mode :" },
+ "exceptions_mode_blacklist": { "message": "Liste noire — ne pas surligner sur ces sites" },
+ "exceptions_mode_whitelist": { "message": "Liste blanche — surligner uniquement sur ces sites" },
"exceptions_list": {
"message": "Sites d'exception :"
},
+ "exceptions_list_blacklist": { "message": "Sites à exclure (liste noire) :" },
+ "exceptions_list_whitelist": { "message": "Sites à inclure (liste blanche) :" },
"clear_all": {
"message": "Tout effacer"
},
@@ -173,6 +178,15 @@
"total_highlights": {
"message": "Total"
},
+ "total_matches": {
+ "message": "Correspondances totales"
+ },
+ "group_by_list": {
+ "message": "Grouper par liste"
+ },
+ "other": {
+ "message": "Autres"
+ },
"refresh": {
"message": "Actualiser"
},
diff --git a/_locales/hi/messages.json b/_locales/hi/messages.json
index 0ac772d..861c3ee 100644
--- a/_locales/hi/messages.json
+++ b/_locales/hi/messages.json
@@ -122,9 +122,14 @@
"manage_exceptions": {
"message": "प्रबंधित करें"
},
+ "exceptions_mode": { "message": "मोड:" },
+ "exceptions_mode_blacklist": { "message": "ब्लैकलिस्ट — इन साइटों पर हाइलाइट न करें" },
+ "exceptions_mode_whitelist": { "message": "व्हाइटलिस्ट — केवल इन साइटों पर हाइलाइट करें" },
"exceptions_list": {
"message": "अपवाद साइटें:"
},
+ "exceptions_list_blacklist": { "message": "बहिष्कृत साइटें (ब्लैकलिस्ट):" },
+ "exceptions_list_whitelist": { "message": "शामिल साइटें (व्हाइटलिस्ट):" },
"clear_all": {
"message": "सभी साफ करें"
},
@@ -173,6 +178,15 @@
"total_highlights": {
"message": "कुल"
},
+ "total_matches": {
+ "message": "कुल मिलान"
+ },
+ "group_by_list": {
+ "message": "सूची के अनुसार समूहित करें"
+ },
+ "other": {
+ "message": "अन्य"
+ },
"refresh": {
"message": "रीफ्रेश करें"
},
diff --git a/_locales/it/messages.json b/_locales/it/messages.json
index ddeb16d..cf232ee 100644
--- a/_locales/it/messages.json
+++ b/_locales/it/messages.json
@@ -122,9 +122,14 @@
"manage_exceptions": {
"message": "Gestisci"
},
+ "exceptions_mode": { "message": "Modalità:" },
+ "exceptions_mode_blacklist": { "message": "Lista nera — non evidenziare su questi siti" },
+ "exceptions_mode_whitelist": { "message": "Lista bianca — evidenziare solo su questi siti" },
"exceptions_list": {
"message": "Siti di eccezione:"
},
+ "exceptions_list_blacklist": { "message": "Siti da escludere (lista nera):" },
+ "exceptions_list_whitelist": { "message": "Siti da includere (lista bianca):" },
"clear_all": {
"message": "Cancella tutto"
},
@@ -173,6 +178,15 @@
"total_highlights": {
"message": "Totale"
},
+ "total_matches": {
+ "message": "Corrispondenze totali"
+ },
+ "group_by_list": {
+ "message": "Raggruppa per lista"
+ },
+ "other": {
+ "message": "Altri"
+ },
"refresh": {
"message": "Aggiorna"
},
diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json
index 1ccd326..726e2be 100644
--- a/_locales/ja/messages.json
+++ b/_locales/ja/messages.json
@@ -122,9 +122,14 @@
"manage_exceptions": {
"message": "管理"
},
+ "exceptions_mode": { "message": "モード:" },
+ "exceptions_mode_blacklist": { "message": "ブラックリスト — これらのサイトではハイライトしない" },
+ "exceptions_mode_whitelist": { "message": "ホワイトリスト — これらのサイトでのみハイライト" },
"exceptions_list": {
"message": "例外サイト:"
},
+ "exceptions_list_blacklist": { "message": "除外するサイト(ブラックリスト):" },
+ "exceptions_list_whitelist": { "message": "含めるサイト(ホワイトリスト):" },
"clear_all": {
"message": "すべてクリア"
},
@@ -173,6 +178,15 @@
"total_highlights": {
"message": "合計"
},
+ "total_matches": {
+ "message": "一致数の合計"
+ },
+ "group_by_list": {
+ "message": "リストでグループ化"
+ },
+ "other": {
+ "message": "その他"
+ },
"refresh": {
"message": "更新"
},
diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json
index 95d4403..ea484a2 100644
--- a/_locales/ko/messages.json
+++ b/_locales/ko/messages.json
@@ -122,9 +122,14 @@
"manage_exceptions": {
"message": "관리"
},
+ "exceptions_mode": { "message": "모드:" },
+ "exceptions_mode_blacklist": { "message": "차단 목록 — 이 사이트에서 강조 안 함" },
+ "exceptions_mode_whitelist": { "message": "허용 목록 — 이 사이트에서만 강조" },
"exceptions_list": {
"message": "예외 사이트:"
},
+ "exceptions_list_blacklist": { "message": "제외할 사이트 (차단 목록):" },
+ "exceptions_list_whitelist": { "message": "포함할 사이트 (허용 목록):" },
"clear_all": {
"message": "모두 지우기"
},
@@ -173,6 +178,15 @@
"total_highlights": {
"message": "전체"
},
+ "total_matches": {
+ "message": "총 일치 수"
+ },
+ "group_by_list": {
+ "message": "목록별로 그룹화"
+ },
+ "other": {
+ "message": "기타"
+ },
"refresh": {
"message": "새로고침"
},
diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json
index 83df711..37fc558 100644
--- a/_locales/nl/messages.json
+++ b/_locales/nl/messages.json
@@ -122,9 +122,14 @@
"manage_exceptions": {
"message": "Beheren"
},
+ "exceptions_mode": { "message": "Modus:" },
+ "exceptions_mode_blacklist": { "message": "Zwarte lijst — niet markeren op deze sites" },
+ "exceptions_mode_whitelist": { "message": "Witte lijst — alleen markeren op deze sites" },
"exceptions_list": {
"message": "Uitzondering sites:"
},
+ "exceptions_list_blacklist": { "message": "Uit te sluiten sites (zwarte lijst):" },
+ "exceptions_list_whitelist": { "message": "Toe te voegen sites (witte lijst):" },
"clear_all": {
"message": "Alles wissen"
},
@@ -173,6 +178,15 @@
"total_highlights": {
"message": "Totaal"
},
+ "total_matches": {
+ "message": "Totaal aantal matches"
+ },
+ "group_by_list": {
+ "message": "Groeperen op lijst"
+ },
+ "other": {
+ "message": "Overige"
+ },
"refresh": {
"message": "Vernieuwen"
},
diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json
index d0d164c..8001187 100644
--- a/_locales/pl/messages.json
+++ b/_locales/pl/messages.json
@@ -122,9 +122,14 @@
"manage_exceptions": {
"message": "Zarządzaj"
},
+ "exceptions_mode": { "message": "Tryb:" },
+ "exceptions_mode_blacklist": { "message": "Czarna lista — nie podświetlaj na tych stronach" },
+ "exceptions_mode_whitelist": { "message": "Biała lista — podświetlaj tylko na tych stronach" },
"exceptions_list": {
"message": "Strony wyjątków:"
},
+ "exceptions_list_blacklist": { "message": "Strony do wykluczenia (czarna lista):" },
+ "exceptions_list_whitelist": { "message": "Strony do uwzględnienia (biała lista):" },
"clear_all": {
"message": "Wyczyść wszystko"
},
@@ -173,6 +178,15 @@
"total_highlights": {
"message": "Łącznie"
},
+ "total_matches": {
+ "message": "Łączna liczba dopasowań"
+ },
+ "group_by_list": {
+ "message": "Grupuj według listy"
+ },
+ "other": {
+ "message": "Inne"
+ },
"refresh": {
"message": "Odśwież"
},
diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json
index d37c977..bed56a4 100644
--- a/_locales/pt_BR/messages.json
+++ b/_locales/pt_BR/messages.json
@@ -122,9 +122,14 @@
"manage_exceptions": {
"message": "Gerenciar"
},
+ "exceptions_mode": { "message": "Modo:" },
+ "exceptions_mode_blacklist": { "message": "Lista negra — não destacar nestes sites" },
+ "exceptions_mode_whitelist": { "message": "Lista branca — destacar apenas nestes sites" },
"exceptions_list": {
"message": "Sites de exceção:"
},
+ "exceptions_list_blacklist": { "message": "Sites a excluir (lista negra):" },
+ "exceptions_list_whitelist": { "message": "Sites a incluir (lista branca):" },
"clear_all": {
"message": "Limpar tudo"
},
@@ -173,6 +178,15 @@
"total_highlights": {
"message": "Total"
},
+ "total_matches": {
+ "message": "Total de correspondências"
+ },
+ "group_by_list": {
+ "message": "Agrupar por lista"
+ },
+ "other": {
+ "message": "Outros"
+ },
"refresh": {
"message": "Atualizar"
},
diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json
index 9b5238d..e51e20a 100644
--- a/_locales/ru/messages.json
+++ b/_locales/ru/messages.json
@@ -122,9 +122,14 @@
"manage_exceptions": {
"message": "Управление"
},
+ "exceptions_mode": { "message": "Режим:" },
+ "exceptions_mode_blacklist": { "message": "Чёрный список — не подсвечивать на этих сайтах" },
+ "exceptions_mode_whitelist": { "message": "Белый список — подсвечивать только на этих сайтах" },
"exceptions_list": {
"message": "Сайты-исключения:"
},
+ "exceptions_list_blacklist": { "message": "Исключить сайты (чёрный список):" },
+ "exceptions_list_whitelist": { "message": "Включить сайты (белый список):" },
"clear_all": {
"message": "Очистить все"
},
@@ -173,6 +178,15 @@
"total_highlights": {
"message": "Всего"
},
+ "total_matches": {
+ "message": "Всего совпадений"
+ },
+ "group_by_list": {
+ "message": "Группировать по списку"
+ },
+ "other": {
+ "message": "Прочее"
+ },
"refresh": {
"message": "Обновить"
},
diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json
index 762c7d7..6456b2b 100644
--- a/_locales/tr/messages.json
+++ b/_locales/tr/messages.json
@@ -122,9 +122,14 @@
"manage_exceptions": {
"message": "Yönet"
},
+ "exceptions_mode": { "message": "Mod:" },
+ "exceptions_mode_blacklist": { "message": "Kara liste — bu sitelerde vurgulama" },
+ "exceptions_mode_whitelist": { "message": "Beyaz liste — yalnızca bu sitelerde vurgula" },
"exceptions_list": {
"message": "İstisna Siteleri:"
},
+ "exceptions_list_blacklist": { "message": "Hariç tutulacak siteler (kara liste):" },
+ "exceptions_list_whitelist": { "message": "Dahil edilecek siteler (beyaz liste):" },
"clear_all": {
"message": "Hepsini Temizle"
},
@@ -173,6 +178,15 @@
"total_highlights": {
"message": "Toplam"
},
+ "total_matches": {
+ "message": "Toplam eşleşme"
+ },
+ "group_by_list": {
+ "message": "Listeye göre grupla"
+ },
+ "other": {
+ "message": "Diğer"
+ },
"refresh": {
"message": "Yenile"
},
diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json
index c9b623e..1521960 100644
--- a/_locales/zh_CN/messages.json
+++ b/_locales/zh_CN/messages.json
@@ -122,9 +122,14 @@
"manage_exceptions": {
"message": "管理"
},
+ "exceptions_mode": { "message": "模式:" },
+ "exceptions_mode_blacklist": { "message": "黑名单 — 不在这些网站上高亮" },
+ "exceptions_mode_whitelist": { "message": "白名单 — 仅在这些网站上高亮" },
"exceptions_list": {
"message": "例外网站:"
},
+ "exceptions_list_blacklist": { "message": "要排除的网站(黑名单):" },
+ "exceptions_list_whitelist": { "message": "要包含的网站(白名单):" },
"clear_all": {
"message": "清除全部"
},
@@ -173,6 +178,15 @@
"total_highlights": {
"message": "总计"
},
+ "total_matches": {
+ "message": "总匹配数"
+ },
+ "group_by_list": {
+ "message": "按列表分组"
+ },
+ "other": {
+ "message": "其他"
+ },
"refresh": {
"message": "刷新"
},
diff --git a/popup/popup.css b/popup/popup.css
index bfa6075..6addc17 100644
--- a/popup/popup.css
+++ b/popup/popup.css
@@ -799,52 +799,108 @@ body {
.page-highlights-section {
display: flex;
flex-direction: column;
- gap: 6px;
+ gap: 10px;
flex: 1;
min-height: 0;
}
-.page-highlights-info-card {
+.page-highlights-header-row {
display: flex;
align-items: center;
- justify-content: space-between;
- padding: 8px 10px;
- background: var(--input-bg);
- border: 1px solid var(--input-border);
- border-radius: 8px;
- font-size: 14px;
- color: var(--text-color);
-}
-
-.page-highlights-info-card strong {
- font-weight: 600;
- color: var(--accent);
-}
-
-.refresh-button {
- width: 100%;
- min-height: 28px;
- height: 28px;
- display: flex;
- align-items: center;
- justify-content: center;
gap: 8px;
- background: var(--input-bg);
- border: 1px solid var(--input-border);
- border-radius: 8px;
- cursor: pointer;
- font-size: 14px;
- color: var(--text-color);
- transition: all 0.2s;
flex-shrink: 0;
}
-.refresh-button:hover {
+.page-highlights-total-label {
+ font-size: var(--text-xs);
+ font-weight: 600;
+ letter-spacing: var(--tracking-wide);
+ text-transform: uppercase;
+ color: var(--text-color);
+ opacity: 0.7;
+ flex-shrink: 0;
+}
+
+.page-highlights-total-count {
+ flex: 1;
+ font-size: var(--text-base);
+ font-weight: 600;
+ color: var(--accent);
+ text-align: right;
+}
+
+.page-highlights-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ flex-shrink: 0;
+}
+
+.page-highlights-group-toggle {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ cursor: pointer;
+ padding: 8px 10px;
+ margin: 0 -2px;
+ border-radius: 6px;
+ font-size: var(--text-sm);
+ color: var(--text-color);
+ background: var(--input-bg);
+ border: 1px solid var(--input-border);
+ transition: background 0.2s, border-color 0.2s;
+}
+
+.page-highlights-group-toggle:hover {
background: var(--section-bg);
}
-.refresh-button i {
- font-size: 16px;
+.page-highlights-group-toggle .switch-wrapper {
+ flex-shrink: 0;
+}
+
+.page-highlights-group-label {
+ flex: 1;
+ font-weight: 500;
+ user-select: none;
+}
+
+.page-highlights-filters {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ align-items: center;
+}
+
+.page-highlights-filter-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 10px;
+ font-size: var(--text-xs);
+ border-radius: 6px;
+ cursor: pointer;
+ background: var(--input-bg);
+ border: 1px solid var(--input-border);
+ color: var(--text-color);
+ transition: all 0.2s;
+}
+
+.page-highlights-filter-chip:hover {
+ background: var(--section-bg);
+}
+
+.page-highlights-filter-chip.active {
+ border-color: var(--list-color, var(--accent));
+ background: color-mix(in srgb, var(--list-color, var(--accent)) 14%, var(--input-bg));
+ color: var(--text-color);
+}
+
+.page-highlights-filter-chip .filter-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ flex-shrink: 0;
}
.page-highlights-list {
@@ -852,7 +908,7 @@ body {
overflow-y: auto;
background: var(--input-bg);
border: 1px solid var(--input-border);
- border-radius: 8px;
+ border-radius: 6px;
min-height: 0;
}
@@ -864,25 +920,74 @@ body {
opacity: 0.6;
}
+.page-highlights-group-section {
+ border-bottom: 1px solid var(--input-border);
+}
+
+.page-highlights-group-section:last-child {
+ border-bottom: none;
+}
+
+.page-highlights-group-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ cursor: pointer;
+ font-size: var(--text-xs);
+ font-weight: 600;
+ letter-spacing: var(--tracking-wide);
+ text-transform: uppercase;
+ color: var(--text-color);
+ opacity: 0.7;
+ background: var(--section-bg);
+ transition: background 0.2s;
+}
+
+.page-highlights-group-header:hover {
+ opacity: 0.9;
+}
+
+.page-highlights-group-header i {
+ font-size: 10px;
+ transition: transform 0.2s;
+}
+
+.page-highlights-group-section.collapsed .page-highlights-group-header i {
+ transform: rotate(-90deg);
+}
+
+.page-highlights-group-header .group-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
.page-highlight-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
- padding: 10px 12px;
+ padding: 6px 12px 6px 14px;
border-bottom: 1px solid var(--input-border);
cursor: pointer;
transition: all 0.2s;
+ border-left: 3px solid transparent;
+ background: var(--input-bg);
}
+
.page-highlight-item:last-child {
border-bottom: none;
}
.page-highlight-item:hover {
background: var(--section-bg);
- border-left: 3px solid var(--accent);
- padding-left: 9px;
+}
+
+.page-highlight-item[style*="--item-tint"] {
+ background: color-mix(in srgb, var(--item-tint) 4%, var(--input-bg));
}
.page-highlight-word {
@@ -891,14 +996,31 @@ body {
align-items: center;
gap: 8px;
min-width: 0;
+ flex-wrap: wrap;
}
.page-highlight-preview {
- display: inline-block;
- padding: 3px 8px;
- border-radius: 4px;
- font-size: 13px;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 0;
+ font-size: var(--text-base);
font-weight: 500;
+ color: var(--text-color);
+}
+
+.page-highlight-preview .preview-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.page-highlight-list-tag {
+ font-size: var(--text-xs);
+ color: var(--text-color);
+ opacity: 0.5;
+ font-weight: 400;
}
.page-highlight-position {
@@ -949,69 +1071,156 @@ body {
min-height: 0;
}
-.exception-toggle-btn {
+.exceptions-mode-card {
+ padding: 6px 10px;
+ background: var(--input-bg);
+ border: 1px solid var(--input-border);
+ border-radius: 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
+.exceptions-mode-label {
+ font-size: var(--text-xs);
+ font-weight: 500;
+ letter-spacing: var(--tracking-wide);
+ text-transform: uppercase;
+ color: var(--text-color);
+ opacity: 0.6;
+}
+
+.exceptions-mode-select {
width: 100%;
min-height: 32px;
+ padding: 0 10px;
+ font-size: var(--text-base);
+ color: var(--text-color);
+ background: var(--input-bg);
+ border: 1px solid var(--input-border);
+ border-radius: 6px;
+ cursor: pointer;
+}
+
+.exceptions-add-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+}
+
+.exception-domain-input {
+ flex: 1;
+ min-width: 0;
height: 32px;
+ padding: 0 10px;
+ font-size: var(--text-base);
+ background: var(--input-bg);
+ border: 1px solid var(--input-border);
+ border-radius: 6px;
+ color: var(--text-color);
+}
+
+.exception-domain-input:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+
+.exception-add-btn {
+ width: 32px;
+ height: 32px;
+ min-width: 32px;
display: flex;
align-items: center;
justify-content: center;
- gap: 8px;
- background: var(--input-bg);
- border: 1px solid var(--input-border);
- border-radius: 8px;
+ border-radius: 6px;
cursor: pointer;
- font-size: 14px;
- color: var(--text-color);
transition: all 0.2s;
flex-shrink: 0;
}
-.exception-toggle-btn:hover {
- background: var(--section-bg);
-}
-
-.exception-toggle-btn.danger {
- background: rgba(239, 68, 68, 0.1);
- border-color: rgba(239, 68, 68, 0.3);
- color: var(--danger);
-}
-
-.exception-toggle-btn.danger:hover {
- background: rgba(239, 68, 68, 0.2);
-}
-
-.exception-toggle-btn i {
- font-size: 16px;
+.exception-add-btn i {
+ font-size: 14px;
}
.exceptions-list-wrapper {
display: flex;
flex-direction: column;
- gap: 6px;
+ gap: 4px;
flex: 1;
min-height: 0;
}
+.exceptions-list-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ flex-shrink: 0;
+}
+
+.clear-exceptions-link {
+ background: none;
+ border: none;
+ padding: 0;
+ font-size: var(--text-xs);
+ font-weight: 500;
+ color: var(--text-color);
+ opacity: 0.6;
+ cursor: pointer;
+ transition: opacity 0.2s, color 0.2s;
+}
+
+.clear-exceptions-link:hover {
+ opacity: 1;
+ color: var(--danger);
+ text-decoration: underline;
+}
+
.exceptions-list {
flex: 1;
overflow-y: auto;
background: var(--input-bg);
border: 1px solid var(--input-border);
- border-radius: 8px;
+ border-radius: 6px;
min-height: 0;
}
.exception-item {
display: flex;
align-items: center;
- justify-content: space-between;
- gap: 12px;
- padding: 10px 12px;
+ gap: 10px;
+ padding: 8px 12px;
border-bottom: 1px solid var(--input-border);
transition: background 0.2s;
}
+.exception-item .exception-domain-icon {
+ width: 22px;
+ height: 22px;
+ min-width: 22px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ font-size: 10px;
+ color: var(--text-color);
+ opacity: 0.5;
+ background: var(--section-bg);
+ border-radius: 50%;
+}
+
+.exception-item .exception-domain-icon i {
+ font-size: 10px;
+}
+
+.exception-item.exception-empty {
+ justify-content: center;
+ color: var(--text-color);
+ opacity: 0.6;
+}
+
.exception-item:last-child {
border-bottom: none;
}
@@ -1022,10 +1231,11 @@ body {
.exception-domain {
flex: 1;
- font-size: 14px;
+ font-size: var(--text-base);
color: var(--text-color);
word-break: break-word;
line-height: 1.4;
+ min-width: 0;
}
.exception-remove {
@@ -1054,29 +1264,6 @@ body {
font-size: 12px;
}
-.clear-exceptions-btn {
- width: 100%;
- height: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- background: rgba(239, 68, 68, 0.1);
- border: 1px solid rgba(239, 68, 68, 0.3);
- color: var(--danger);
- border-radius: 8px;
- cursor: pointer;
- font-size: 14px;
- transition: all 0.2s;
-}
-
-.clear-exceptions-btn:hover {
- background: rgba(239, 68, 68, 0.2);
-}
-
-.clear-exceptions-btn i {
- font-size: 16px;
-}
/* Settings Overlay */
.settings-overlay {
diff --git a/popup/popup.html b/popup/popup.html
index 40bb8a4..cbc0f16 100644
--- a/popup/popup.html
+++ b/popup/popup.html
@@ -198,16 +198,20 @@
-
-
Total:
-
0
+
+
+
+
-
-
-
@@ -221,23 +225,26 @@
Site Exceptions
-
-
-
+
+ Mode
+
+
+
+
+
+
-
-
diff --git a/shared/colors.css b/shared/colors.css
index 955a123..d9aa276 100644
--- a/shared/colors.css
+++ b/shared/colors.css
@@ -34,6 +34,8 @@
--section-bg: #ffffff;
--panel-bg: #ffffff;
--switch-bg: #e0e0e0;
+ --switch-thumb: #ffffff;
+ --switch-track-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
--checkbox-accent: #ff8c00;
--checkbox-border: #d0d0d0;
--focus-ring: 0 0 0 3px rgba(255, 140, 0, 0.25);
@@ -64,6 +66,8 @@ body.dark {
--section-bg: #121212;
--panel-bg: #121212;
--switch-bg: #2a2a2a;
+ --switch-thumb: #e5e5e5;
+ --switch-track-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
--checkbox-accent: #ff8c00;
--checkbox-border: #333333;
--focus-ring: 0 0 0 3px rgba(255, 140, 0, 0.3);
diff --git a/shared/ui-components.css b/shared/ui-components.css
index eccd47b..18acc98 100644
--- a/shared/ui-components.css
+++ b/shared/ui-components.css
@@ -93,8 +93,8 @@ button:disabled {
.switch-wrapper {
position: relative;
display: inline-block;
- width: 40px;
- height: 22px;
+ width: 42px;
+ height: 24px;
cursor: pointer;
flex-shrink: 0;
}
@@ -114,30 +114,46 @@ button:disabled {
right: 0;
bottom: 0;
background-color: var(--input-border);
- transition: 0.3s;
- border-radius: 11px;
+ border-radius: 9999px;
+ box-shadow: var(--switch-track-shadow);
+ transition: background-color 0.25s ease, box-shadow 0.2s ease;
+ backface-visibility: hidden;
+ -webkit-backface-visibility: hidden;
}
.switch-slider:before {
position: absolute;
content: "";
- height: 16px;
- width: 16px;
- left: 2px;
- bottom: 2px;
- background-color: white;
- transition: 0.3s;
+ width: 18px;
+ height: 18px;
+ left: 3px;
+ top: 3px;
+ background-color: var(--switch-thumb);
border-radius: 50%;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.04);
+ transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
+ backface-visibility: hidden;
+ -webkit-backface-visibility: hidden;
}
.switch-input:checked + .switch-slider {
background-color: var(--accent);
+ box-shadow: var(--switch-track-shadow);
}
.switch-input:checked + .switch-slider:before {
transform: translateX(18px);
}
+.switch-wrapper:hover .switch-slider:before {
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.22), 0 0 0 1px rgba(0, 0, 0, 0.04);
+}
+
+.switch-input:focus-visible + .switch-slider {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+}
+
/* Switch Toggle (Checkbox Style) */
input[type="checkbox"].switch {
diff --git a/src/background.ts b/src/background.ts
index c524703..24931c5 100644
--- a/src/background.ts
+++ b/src/background.ts
@@ -38,9 +38,16 @@ class BackgroundService {
private setupInstallListener(): void {
chrome.runtime.onInstalled.addListener(async (): Promise => {
- const data = await StorageService.get(['exceptionsList']);
+ const data = await StorageService.get(['exceptionsList', 'exceptionsMode']);
+ const updates: { exceptionsList?: string[]; exceptionsMode?: 'blacklist' | 'whitelist' } = {};
if (!data.exceptionsList) {
- await StorageService.update('exceptionsList', []);
+ updates.exceptionsList = [];
+ }
+ if (data.exceptionsMode !== 'blacklist' && data.exceptionsMode !== 'whitelist') {
+ updates.exceptionsMode = 'blacklist';
+ }
+ if (Object.keys(updates).length > 0) {
+ await StorageService.set(updates);
}
});
}
diff --git a/src/content/ContentScript.ts b/src/content/ContentScript.ts
index bc71a22..ab98fbf 100644
--- a/src/content/ContentScript.ts
+++ b/src/content/ContentScript.ts
@@ -1,4 +1,4 @@
-import { HighlightList, MessageData } from '../types.js';
+import { HighlightList, MessageData, ExceptionsMode } from '../types.js';
import { StorageService } from '../services/StorageService.js';
import { MessageService } from '../services/MessageService.js';
import { HighlightEngine } from './HighlightEngine.js';
@@ -7,7 +7,8 @@ export class ContentScript {
private lists: HighlightList[] = [];
private isGlobalHighlightEnabled = true;
private exceptionsList: string[] = [];
- private isCurrentSiteException = false;
+ private exceptionsMode: ExceptionsMode = 'blacklist';
+ private shouldSkipDueToExceptions = false;
private matchCase = false;
private matchWhole = false;
private highlightEngine: HighlightEngine;
@@ -26,11 +27,12 @@ export class ContentScript {
private async loadSettings(): Promise {
const data = await StorageService.get([
- 'lists',
- 'globalHighlightEnabled',
- 'matchCaseEnabled',
- 'matchWholeEnabled',
- 'exceptionsList'
+ 'lists',
+ 'globalHighlightEnabled',
+ 'matchCaseEnabled',
+ 'matchWholeEnabled',
+ 'exceptionsList',
+ 'exceptionsMode'
]);
this.lists = data.lists || [];
@@ -38,12 +40,17 @@ export class ContentScript {
this.matchCase = data.matchCaseEnabled ?? false;
this.matchWhole = data.matchWholeEnabled ?? false;
this.exceptionsList = data.exceptionsList || [];
- this.isCurrentSiteException = this.checkCurrentSiteException();
+ this.exceptionsMode = data.exceptionsMode === 'whitelist' ? 'whitelist' : 'blacklist';
+ this.shouldSkipDueToExceptions = this.computeShouldSkipDueToExceptions();
}
- private checkCurrentSiteException(): boolean {
+ private computeShouldSkipDueToExceptions(): boolean {
const currentHostname = window.location.hostname;
- return this.exceptionsList.includes(currentHostname);
+ const isInList = this.exceptionsList.includes(currentHostname);
+ if (this.exceptionsMode === 'blacklist') {
+ return isInList;
+ }
+ return !isInList;
}
private setupMessageListener(): void {
@@ -91,9 +98,10 @@ export class ContentScript {
}
private async handleExceptionsUpdate(): Promise {
- const data = await StorageService.get(['exceptionsList']);
+ const data = await StorageService.get(['exceptionsList', 'exceptionsMode']);
this.exceptionsList = data.exceptionsList || [];
- this.isCurrentSiteException = this.checkCurrentSiteException();
+ this.exceptionsMode = data.exceptionsMode === 'whitelist' ? 'whitelist' : 'blacklist';
+ this.shouldSkipDueToExceptions = this.computeShouldSkipDueToExceptions();
this.processHighlights();
}
@@ -102,7 +110,7 @@ export class ContentScript {
this.isProcessing = true;
try {
- if (!this.isGlobalHighlightEnabled || this.isCurrentSiteException) {
+ if (!this.isGlobalHighlightEnabled || this.shouldSkipDueToExceptions) {
this.highlightEngine.clearHighlights();
this.highlightEngine.stopObserving();
return;
@@ -124,13 +132,15 @@ export class ContentScript {
activeWords.push({
text: word.wordStr,
background: word.background || list.background,
- foreground: word.foreground || list.foreground
+ foreground: word.foreground || list.foreground,
+ listId: list.id,
+ listName: list.name || 'Default'
});
}
}
const highlights = this.highlightEngine.getPageHighlights(activeWords);
- sendResponse({ highlights });
+ sendResponse({ highlights, lists: this.lists.filter(l => l.active) });
}
private handleScrollToHighlight(word: string, index: number): void {
diff --git a/src/content/HighlightEngine.ts b/src/content/HighlightEngine.ts
index e8ac8f4..bea6a63 100644
--- a/src/content/HighlightEngine.ts
+++ b/src/content/HighlightEngine.ts
@@ -419,8 +419,8 @@ export class HighlightEngine {
}
}
- getPageHighlights(activeWords: ActiveWord[]): Array<{ word: string; count: number; background: string; foreground: string }> {
- const seen = new Map();
+ getPageHighlights(activeWords: ActiveWord[]): Array<{ word: string; count: number; background: string; foreground: string; listId?: number; listName?: string; listNames: string[] }> {
+ const seen = new Map();
for (const activeWord of activeWords) {
const lookup = this.currentMatchCase ? activeWord.text : activeWord.text.toLowerCase();
@@ -429,13 +429,26 @@ export class HighlightEngine {
const totalCount = (ranges?.length || 0) + (textareaMatches?.length || 0);
- if (totalCount > 0 && !seen.has(lookup)) {
- seen.set(lookup, {
- word: activeWord.text,
- count: totalCount,
- background: activeWord.background,
- foreground: activeWord.foreground
- });
+ if (totalCount > 0) {
+ const listName = activeWord.listName || 'Default';
+ const listId = activeWord.listId;
+
+ if (seen.has(lookup)) {
+ const existing = seen.get(lookup)!;
+ if (listName && !existing.listNames.includes(listName)) {
+ existing.listNames.push(listName);
+ }
+ } else {
+ seen.set(lookup, {
+ word: activeWord.text,
+ count: totalCount,
+ background: activeWord.background,
+ foreground: activeWord.foreground,
+ listId,
+ listName,
+ listNames: [listName]
+ });
+ }
}
}
diff --git a/src/popup/PopupController.ts b/src/popup/PopupController.ts
index 3c4ef11..d773d1d 100644
--- a/src/popup/PopupController.ts
+++ b/src/popup/PopupController.ts
@@ -1,4 +1,4 @@
-import { HighlightList, HighlightWord, HighlightInfo, ExportData } from '../types.js';
+import { HighlightList, HighlightWord, HighlightInfo, ExportData, ExceptionsMode } from '../types.js';
import { StorageService } from '../services/StorageService.js';
import { MessageService } from '../services/MessageService.js';
import { DOMUtils } from '../utils/DOMUtils.js';
@@ -15,9 +15,14 @@ export class PopupController {
private matchCaseEnabled = false;
private matchWholeEnabled = false;
private exceptionsList: string[] = [];
+ private exceptionsMode: ExceptionsMode = 'blacklist';
private currentTabHost = '';
private activeTab = 'lists';
- private pageHighlights: Array<{ word: string; count: number; background: string; foreground: string }> = [];
+ private pageHighlights: Array<{ word: string; count: number; background: string; foreground: string; listId?: number; listName?: string; listNames: string[] }> = [];
+ private pageHighlightsActiveLists: Array<{ id: number; name: string; background: string }> = [];
+ private pageHighlightsGroupByList = false;
+ private pageHighlightsListFilter = new Set();
+ private pageHighlightsCollapsedGroups = new Set();
private highlightIndices = new Map();
private wordMenuOpenForIndex: number | null = null;
private wordMenuCopyOnly = false;
@@ -54,6 +59,7 @@ export class PopupController {
this.matchCaseEnabled = data.matchCaseEnabled ?? false;
this.matchWholeEnabled = data.matchWholeEnabled ?? false;
this.exceptionsList = data.exceptionsList || [];
+ this.exceptionsMode = data.exceptionsMode === 'whitelist' ? 'whitelist' : 'blacklist';
if (this.lists.length === 0) {
this.lists.push({
@@ -93,6 +99,8 @@ export class PopupController {
wordSearchQuery?: string;
currentPage?: number;
scrollPositions?: Record;
+ pageHighlightsGroupByList?: boolean;
+ pageHighlightsListFilter?: number[];
};
if (typeof state.activeTab === 'string' && state.activeTab !== 'options') {
this.activeTab = state.activeTab;
@@ -109,18 +117,26 @@ export class PopupController {
if (state.scrollPositions && typeof state.scrollPositions === 'object') {
this.scrollPositions = { ...state.scrollPositions };
}
+ if (typeof state.pageHighlightsGroupByList === 'boolean') {
+ this.pageHighlightsGroupByList = state.pageHighlightsGroupByList;
+ }
+ if (Array.isArray(state.pageHighlightsListFilter)) {
+ this.pageHighlightsListFilter = new Set(state.pageHighlightsListFilter);
+ }
} catch {
// keep defaults
}
}
- private getPopupStatePayload(): { activeTab: string; currentListIndex: number; wordSearchQuery: string; currentPage: number; scrollPositions: Record } {
+ private getPopupStatePayload(): { activeTab: string; currentListIndex: number; wordSearchQuery: string; currentPage: number; scrollPositions: Record; pageHighlightsGroupByList: boolean; pageHighlightsListFilter: number[] } {
return {
activeTab: this.activeTab,
currentListIndex: this.currentListIndex,
wordSearchQuery: this.wordSearchQuery,
currentPage: this.currentPage,
- scrollPositions: this.scrollPositions
+ scrollPositions: this.scrollPositions,
+ pageHighlightsGroupByList: this.pageHighlightsGroupByList,
+ pageHighlightsListFilter: Array.from(this.pageHighlightsListFilter)
};
}
@@ -268,7 +284,8 @@ export class PopupController {
document.getElementById('exportSettingsBtn')?.addEventListener('click', () => {
const data: ExportData = {
lists: this.lists,
- exceptionsList: [...this.exceptionsList]
+ exceptionsList: [...this.exceptionsList],
+ exceptionsMode: this.exceptionsMode
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
@@ -320,6 +337,11 @@ export class PopupController {
exceptionsApplied = true;
}
+ if (obj.exceptionsMode === 'whitelist' || obj.exceptionsMode === 'blacklist') {
+ this.exceptionsMode = obj.exceptionsMode;
+ exceptionsApplied = true;
+ }
+
if (!listsApplied && !exceptionsApplied) {
alert(chrome.i18n.getMessage('invalid_import_format') || 'Invalid file format. Please select a valid export file.');
importSettingsInput.value = '';
@@ -908,14 +930,24 @@ export class PopupController {
}
private setupPageHighlights(): void {
- document.getElementById('refreshHighlightsBtn')?.addEventListener('click', async () => {
- await this.loadPageHighlights();
- });
-
document.getElementById('pageHighlightsList')?.addEventListener('click', async (e) => {
const target = e.target as HTMLElement;
- const item = target.closest('.page-highlight-item') as HTMLElement;
+ const groupHeader = target.closest('.page-highlights-group-header');
+ if (groupHeader) {
+ const section = groupHeader.closest('.page-highlights-group-section');
+ const groupKey = section?.getAttribute('data-group');
+ if (groupKey) {
+ if (this.pageHighlightsCollapsedGroups.has(groupKey)) {
+ this.pageHighlightsCollapsedGroups.delete(groupKey);
+ } else {
+ this.pageHighlightsCollapsedGroups.add(groupKey);
+ }
+ this.renderPageHighlights();
+ return;
+ }
+ }
+ const item = target.closest('.page-highlight-item') as HTMLElement;
if (!item) return;
const word = item.dataset.word;
@@ -934,6 +966,12 @@ export class PopupController {
await this.jumpToHighlight(word, currentIndex);
}
});
+
+ document.getElementById('pageHighlightsGroupByList')?.addEventListener('change', (e) => {
+ this.pageHighlightsGroupByList = (e.target as HTMLInputElement).checked;
+ this.savePopupState();
+ this.renderPageHighlights();
+ });
}
private async loadPageHighlights(): Promise {
@@ -941,15 +979,25 @@ export class PopupController {
const response = await MessageService.sendToActiveTab({ type: 'GET_PAGE_HIGHLIGHTS' });
if (response && response.highlights) {
- this.pageHighlights = response.highlights;
+ this.pageHighlights = response.highlights.map((h: { word: string; count: number; background: string; foreground: string; listId?: number; listName?: string; listNames?: string[] }) => ({
+ ...h,
+ listNames: h.listNames || (h.listName ? [h.listName] : [])
+ }));
+ this.pageHighlightsActiveLists = response.lists || [];
+ if (this.pageHighlightsListFilter.size === 0 && this.pageHighlightsActiveLists.length > 0) {
+ this.pageHighlightsListFilter = new Set(this.pageHighlightsActiveLists.map((l: { id: number }) => l.id));
+ }
this.highlightIndices.clear();
this.pageHighlights.forEach(h => this.highlightIndices.set(h.word, 0));
this.renderPageHighlights();
+ this.renderPageHighlightsFilters();
}
} catch (e) {
console.error('Error loading page highlights:', e);
this.pageHighlights = [];
+ this.pageHighlightsActiveLists = [];
this.renderPageHighlights();
+ this.renderPageHighlightsFilters();
}
}
@@ -976,70 +1024,155 @@ export class PopupController {
await this.jumpToHighlight(word, newIndex);
}
+ private passesListFilter(h: { listId?: number; listNames: string[] }): boolean {
+ if (this.pageHighlightsListFilter.size === 0) return true;
+ const wordListIds = new Set();
+ if (h.listId !== undefined) wordListIds.add(h.listId);
+ for (const name of h.listNames) {
+ const list = this.pageHighlightsActiveLists.find(l => l.name === name);
+ if (list) wordListIds.add(list.id);
+ }
+ return [...wordListIds].some(id => this.pageHighlightsListFilter.has(id));
+ }
+
+ private renderPageHighlightsItem(highlight: { word: string; count: number; background: string; foreground: string; listNames: string[] }): string {
+ const currentIndex = this.highlightIndices.get(highlight.word) || 0;
+ return `
+
+
+
+
+ ${DOMUtils.escapeHtml(highlight.word)}
+
+ ${highlight.count > 1 ? `${currentIndex + 1}/${highlight.count}` : ''}
+
+ ${highlight.count > 1 ? `
+
+
+
+
+ ` : ''}
+
+ `;
+ }
+
private renderPageHighlights(): void {
const container = document.getElementById('pageHighlightsList');
const countElement = document.getElementById('totalHighlightsCount');
if (!container || !countElement) return;
- const totalCount = this.pageHighlights.reduce((sum, h) => sum + h.count, 0);
+ const filtered = this.pageHighlights.filter(h => this.passesListFilter(h));
+ const totalCount = filtered.reduce((sum, h) => sum + h.count, 0);
countElement.textContent = totalCount.toString();
- if (this.pageHighlights.length === 0) {
+ if (filtered.length === 0) {
container.innerHTML = `${chrome.i18n.getMessage('no_highlights_on_page') || 'No highlights on this page'}
`;
return;
}
- container.innerHTML = this.pageHighlights.map(highlight => {
- const currentIndex = this.highlightIndices.get(highlight.word) || 0;
- return `
-
-
-
- ${DOMUtils.escapeHtml(highlight.word)}
-
- ${highlight.count > 1 ? `${currentIndex + 1}/${highlight.count}` : ''}
-
- ${highlight.count > 1 ? `
-
-
-
+ if (this.pageHighlightsGroupByList && this.pageHighlightsActiveLists.length > 0) {
+ const listIds = new Set(this.pageHighlightsActiveLists.map(l => l.id).filter(id => this.pageHighlightsListFilter.has(id) || this.pageHighlightsListFilter.size === 0));
+ const groupOrder = this.pageHighlightsActiveLists.filter(l => listIds.has(l.id));
+ let html = '';
+ for (const list of groupOrder) {
+ const items = filtered.filter(h => h.listId === list.id || (h.listNames && h.listNames.includes(list.name)));
+ if (items.length === 0) continue;
+ const groupKey = `list-${list.id}`;
+ const collapsed = this.pageHighlightsCollapsedGroups.has(groupKey);
+ const chevron = collapsed ? 'fa-chevron-right' : 'fa-chevron-down';
+ html += `
+
+
- ` : ''}
-
- `;
- }).join('');
+ ${collapsed ? '' : items.map(h => this.renderPageHighlightsItem(h)).join('')}
+
+ `;
+ }
+ const ungrouped = filtered.filter(h => !groupOrder.some(l => h.listId === l.id || (h.listNames && h.listNames.includes(l.name))));
+ if (ungrouped.length > 0) {
+ const groupKey = 'list-other';
+ const collapsed = this.pageHighlightsCollapsedGroups.has(groupKey);
+ const chevron = collapsed ? 'fa-chevron-right' : 'fa-chevron-down';
+ html += `
+
+
+ ${collapsed ? '' : ungrouped.map(h => this.renderPageHighlightsItem(h)).join('')}
+
+ `;
+ }
+ container.innerHTML = html;
+ } else {
+ container.innerHTML = filtered.map(h => this.renderPageHighlightsItem(h)).join('');
+ }
+
if (this.activeTab === 'page-highlights') {
requestAnimationFrame(() => this.restoreScrollPositions());
}
}
+ private renderPageHighlightsFilters(): void {
+ const container = document.getElementById('pageHighlightsListFilters');
+ if (!container) return;
+ if (this.pageHighlightsActiveLists.length <= 1) {
+ container.innerHTML = '';
+ return;
+ }
+ const allSelected = this.pageHighlightsListFilter.size === 0 || this.pageHighlightsListFilter.size === this.pageHighlightsActiveLists.length;
+ container.innerHTML = this.pageHighlightsActiveLists.map(list => {
+ const active = this.pageHighlightsListFilter.size === 0 || this.pageHighlightsListFilter.has(list.id);
+ const bg = DOMUtils.escapeHtml(list.background);
+ return `
+
+ `;
+ }).join('');
+
+ container.querySelectorAll('.page-highlights-filter-chip').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const id = Number((btn as HTMLElement).dataset.listId);
+ if (this.pageHighlightsListFilter.has(id)) {
+ this.pageHighlightsListFilter.delete(id);
+ } else {
+ this.pageHighlightsListFilter.add(id);
+ }
+ this.savePopupState();
+ this.renderPageHighlights();
+ this.renderPageHighlightsFilters();
+ });
+ });
+ }
+
private setupExceptions(): void {
- document.getElementById('toggleExceptionBtn')?.addEventListener('click', async () => {
- if (!this.currentTabHost) return;
-
- const isException = this.exceptionsList.includes(this.currentTabHost);
-
- if (isException) {
- this.exceptionsList = this.exceptionsList.filter(domain => domain !== this.currentTabHost);
- } else {
- this.exceptionsList.push(this.currentTabHost);
- }
-
- this.updateExceptionButton();
- this.renderExceptions();
- await StorageService.update('exceptionsList', this.exceptionsList);
+ document.getElementById('exceptionsModeSelect')?.addEventListener('change', async (e) => {
+ const value = (e.target as HTMLSelectElement).value;
+ this.exceptionsMode = value === 'whitelist' ? 'whitelist' : 'blacklist';
+ await StorageService.update('exceptionsMode', this.exceptionsMode);
MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' });
+ this.updateExceptionsModeLabel();
+ });
+
+ document.getElementById('addExceptionBtn')?.addEventListener('click', () => this.addExceptionFromInput());
+ (document.getElementById('exceptionDomainInput') as HTMLInputElement)?.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') this.addExceptionFromInput();
});
document.getElementById('clearExceptionsBtn')?.addEventListener('click', async () => {
if (confirm(chrome.i18n.getMessage('confirm_clear_exceptions') || 'Clear all exceptions?')) {
this.exceptionsList = [];
- this.updateExceptionButton();
this.renderExceptions();
await StorageService.update('exceptionsList', this.exceptionsList);
MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' });
@@ -1051,7 +1184,6 @@ export class PopupController {
if (button) {
const domain = (button as HTMLElement).dataset.domain!;
this.exceptionsList = this.exceptionsList.filter(d => d !== domain);
- this.updateExceptionButton();
this.renderExceptions();
await StorageService.update('exceptionsList', this.exceptionsList);
MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' });
@@ -1191,7 +1323,8 @@ export class PopupController {
globalHighlightEnabled: this.globalHighlightEnabled,
matchCaseEnabled: this.matchCaseEnabled,
matchWholeEnabled: this.matchWholeEnabled,
- exceptionsList: this.exceptionsList
+ exceptionsList: this.exceptionsList,
+ exceptionsMode: this.exceptionsMode
});
this.renderLists();
@@ -1201,7 +1334,7 @@ export class PopupController {
private setupStorageSync(): void {
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'local') return;
- if (changes.lists || changes.globalHighlightEnabled || changes.matchCaseEnabled || changes.matchWholeEnabled || changes.exceptionsList) {
+ if (changes.lists || changes.globalHighlightEnabled || changes.matchCaseEnabled || changes.matchWholeEnabled || changes.exceptionsList || changes.exceptionsMode) {
this.reloadFromStorage();
}
});
@@ -1214,6 +1347,7 @@ export class PopupController {
this.matchCaseEnabled = data.matchCaseEnabled ?? false;
this.matchWholeEnabled = data.matchWholeEnabled ?? false;
this.exceptionsList = data.exceptionsList || [];
+ this.exceptionsMode = data.exceptionsMode === 'whitelist' ? 'whitelist' : 'blacklist';
if (this.lists.length === 0) {
this.lists.push({
@@ -1238,7 +1372,8 @@ export class PopupController {
this.renderLists();
this.renderWords();
this.renderExceptions();
- this.updateExceptionButton();
+ this.updateExceptionsModeSelect();
+ this.updateExceptionsModeLabel();
this.updateFormValues();
}
@@ -1465,38 +1600,64 @@ export class PopupController {
`;
}
- private updateExceptionButton(): void {
- const toggleBtn = document.getElementById('toggleExceptionBtn');
- const btnText = document.getElementById('exceptionBtnText');
-
- if (!toggleBtn || !btnText || !this.currentTabHost) return;
-
- const isException = this.exceptionsList.includes(this.currentTabHost);
-
- if (isException) {
- btnText.textContent = chrome.i18n.getMessage('remove_exception') || 'Remove from Exceptions';
- toggleBtn.classList.add('danger');
- const icon = toggleBtn.querySelector('i');
- if (icon) icon.className = 'fa-solid fa-trash';
- } else {
- btnText.textContent = chrome.i18n.getMessage('add_exception') || 'Add to Exceptions';
- toggleBtn.classList.remove('danger');
- const icon = toggleBtn.querySelector('i');
- if (icon) icon.className = 'fa-solid fa-plus';
+ private normalizeDomain(input: string): string | null {
+ const raw = input.trim().toLowerCase();
+ if (!raw) return null;
+ try {
+ if (raw.includes('.')) {
+ const url = raw.startsWith('http') ? new URL(raw) : new URL(`https://${raw}`);
+ return url.hostname;
+ }
+ return raw;
+ } catch {
+ return raw;
}
}
+ private addExceptionFromInput(): void {
+ const input = document.getElementById('exceptionDomainInput') as HTMLInputElement;
+ if (!input) return;
+
+ const domain = this.normalizeDomain(input.value);
+ if (!domain) return;
+
+ if (this.exceptionsList.includes(domain)) {
+ input.value = '';
+ return;
+ }
+
+ this.exceptionsList.push(domain);
+ input.value = '';
+ this.renderExceptions();
+ StorageService.update('exceptionsList', this.exceptionsList).then(() => {
+ MessageService.sendToAllTabs({ type: 'EXCEPTIONS_LIST_UPDATED' });
+ });
+ }
+
+ private updateExceptionsModeSelect(): void {
+ const select = document.getElementById('exceptionsModeSelect') as HTMLSelectElement | null;
+ if (select) select.value = this.exceptionsMode;
+ }
+
+ private updateExceptionsModeLabel(): void {
+ const label = document.getElementById('exceptionsListLabel');
+ if (!label) return;
+ const key = this.exceptionsMode === 'whitelist' ? 'exceptions_list_whitelist' : 'exceptions_list_blacklist';
+ label.textContent = chrome.i18n.getMessage(key) || (this.exceptionsMode === 'whitelist' ? 'Sites to highlight (whitelist):' : 'Sites to exclude (blacklist):');
+ }
+
private renderExceptions(): void {
const container = document.getElementById('exceptionsList');
if (!container) return;
if (this.exceptionsList.length === 0) {
- container.innerHTML = `
${chrome.i18n.getMessage('no_exceptions') || 'No exceptions'}
`;
+ container.innerHTML = `
${chrome.i18n.getMessage('no_exceptions') || 'No exceptions'}
`;
return;
}
container.innerHTML = this.exceptionsList.map(domain =>
`
+
${DOMUtils.escapeHtml(domain)}