diff --git a/_locales/de/messages.json b/_locales/de/messages.json
index 4b56a84..2651c07 100644
--- a/_locales/de/messages.json
+++ b/_locales/de/messages.json
@@ -81,6 +81,26 @@
"message": "Label:",
"description": "Label für das Eingabefeld des Webhook-Labels."
},
+ "optionsEmojiLabel": {
+ "message": "Emoji (optional):",
+ "description": "Beschriftung für das Emoji-Eingabefeld."
+ },
+ "optionsEmojiPlaceholder": {
+ "message": "Emoji wählen, z. B. 🔔",
+ "description": "Platzhalter für das Emoji-Eingabefeld."
+ },
+ "optionsEmojiChooseButton": {
+ "message": "Emoji auswählen",
+ "description": "Schaltflächentext zum Öffnen des Emoji-Pickers."
+ },
+ "optionsEmojiClearButton": {
+ "message": "Leeren",
+ "description": "Schaltflächentext zum Leeren des Emoji-Feldes."
+ },
+ "optionsEmojiSearchPlaceholder": {
+ "message": "Emoji suchen...",
+ "description": "Platzhalter für das Emoji-Suchfeld."
+ },
"optionsLabelInputPlaceholder": {
"message": "z.B. 'An Slack senden'",
"description": "Platzhalter für die Eingabe des Webhook-Labels."
@@ -242,7 +262,7 @@
"description": "Hinweistext neben dem Import-Button."
},
"optionsTestButton": {
- "message": "Testen",
+ "message": "🧪 Testen",
"description": "Text für den Test-Button."
},
"optionsTestSuccess": {
@@ -252,5 +272,33 @@
"optionsTestError": {
"message": "Fehler beim Senden des Test-Webhooks: ",
"description": "Fehlermeldung für den Test-Webhook."
+ },
+ "optionsAppearanceHeader": {
+ "message": "Erscheinungsbild",
+ "description": "Überschrift für den Bereich Erscheinungsbild."
+ },
+ "optionsThemeLabel": {
+ "message": "Theme",
+ "description": "Beschriftung für die Theme-Auswahl."
+ },
+ "optionsThemeSystem": {
+ "message": "Systemstandard",
+ "description": "Option für das systemweite Standard-Theme."
+ },
+ "optionsThemeLight": {
+ "message": "Hell",
+ "description": "Option für das helle Theme."
+ },
+ "optionsThemeDark": {
+ "message": "Dunkel",
+ "description": "Option für das dunkle Theme."
+ },
+ "optionsManageAppearanceButton": {
+ "message": "Erscheinungsbild",
+ "description": "Schaltfläche zum Öffnen des Erscheinungsbild-Dialogs."
+ },
+ "optionsCloseAppearanceButton": {
+ "message": "Schließen",
+ "description": "Schaltfläche zum Schließen des Erscheinungsbild-Dialogs."
}
}
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 5a48c53..3d28615 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -81,6 +81,26 @@
"message": "Label:",
"description": "Label for the webhook label input field."
},
+ "optionsEmojiLabel": {
+ "message": "Emoji (optional):",
+ "description": "Label for the webhook emoji input field."
+ },
+ "optionsEmojiPlaceholder": {
+ "message": "Pick an emoji, e.g. 🔔",
+ "description": "Placeholder for the webhook emoji input."
+ },
+ "optionsEmojiChooseButton": {
+ "message": "Choose Emoji",
+ "description": "Button text to open the emoji picker."
+ },
+ "optionsEmojiClearButton": {
+ "message": "Clear",
+ "description": "Button text to clear the emoji."
+ },
+ "optionsEmojiSearchPlaceholder": {
+ "message": "Search emoji...",
+ "description": "Placeholder for the emoji search input."
+ },
"optionsLabelInputPlaceholder": {
"message": "e.g. 'Send to Slack'",
"description": "Placeholder for the webhook label input."
@@ -242,7 +262,7 @@
"description": "Information text shown next to the import button."
},
"optionsTestButton": {
- "message": "Test",
+ "message": "🧪 Test",
"description": "Text for the test button."
},
"optionsTestSuccess": {
@@ -253,4 +273,33 @@
"message": "Error sending test webhook: ",
"description": "Error message for the test webhook."
}
+ ,
+ "optionsAppearanceHeader": {
+ "message": "Appearance",
+ "description": "Header for the appearance section."
+ },
+ "optionsThemeLabel": {
+ "message": "Theme",
+ "description": "Label for the theme selector."
+ },
+ "optionsThemeSystem": {
+ "message": "System default",
+ "description": "System default theme option."
+ },
+ "optionsThemeLight": {
+ "message": "Light",
+ "description": "Light theme option."
+ },
+ "optionsThemeDark": {
+ "message": "Dark",
+ "description": "Dark theme option."
+ },
+ "optionsManageAppearanceButton": {
+ "message": "Appearance",
+ "description": "Button to open the appearance modal."
+ },
+ "optionsCloseAppearanceButton": {
+ "message": "Close",
+ "description": "Button to close the appearance modal."
+ }
}
diff --git a/options/options.css b/options/options.css
index 9f50b9d..64538fc 100644
--- a/options/options.css
+++ b/options/options.css
@@ -277,6 +277,46 @@ button:hover {
display: none !important;
}
+/* Emoji picker */
+.emoji-input-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.emoji-picker {
+ margin-top: 8px;
+ border: 1px solid #d1d5db;
+ border-radius: 8px;
+ padding: 12px;
+ background: #fff;
+}
+
+.emoji-picker input[type="text"] {
+ margin-bottom: 8px;
+}
+
+.emoji-grid {
+ display: grid;
+ grid-template-columns: repeat(8, 1fr);
+ gap: 6px;
+}
+
+.emoji-item {
+ font-size: 20px;
+ line-height: 1;
+ padding: 6px;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ background: #f9fafb;
+ cursor: pointer;
+}
+
+.emoji-item:hover {
+ background: #eef2ff;
+ border-color: #c7d2fe;
+}
+
#no-webhooks-message {
color: #6b7280;
padding: 20px;
@@ -410,7 +450,7 @@ button:hover {
position: relative;
}
-.close, .close-manage-groups {
+.close, .close-manage-groups, .close-manage-appearance {
position: absolute;
right: 15px;
top: 15px;
@@ -420,7 +460,7 @@ button:hover {
color: #6b7280;
}
-.close:hover, .close-manage-groups:hover {
+.close:hover, .close-manage-groups:hover, .close-manage-appearance:hover {
color: #1f2937;
}
@@ -520,3 +560,44 @@ button:hover {
.delete-group:hover {
background-color: #dc2626;
}
+
+/* Emoji icons for options buttons (non-intrusive: does not change textContent) */
+#add-new-webhook-btn::before,
+#manage-groups-btn::before,
+#manage-appearance-btn::before,
+#cancel-edit-btn::before,
+#close-manage-groups-btn::before,
+#close-manage-appearance-btn::before,
+#add-webhook-form button[type="submit"]::before,
+#add-header-btn::before,
+#add-group-btn::before,
+#export-webhooks-btn::before,
+#import-webhooks-btn::before,
+.edit-btn::before,
+.duplicate-btn::before,
+.delete-btn::before,
+.remove-header-btn::before,
+.save-group-name::before,
+.delete-group::before,
+.edit-group-btn::before {
+ display: inline-block;
+ margin-right: 6px;
+}
+
+#add-new-webhook-btn::before { content: "➕"; }
+#manage-groups-btn::before { content: "🗂️"; }
+#add-header-btn::before { content: "➕"; }
+#add-group-btn::before { content: "➕"; }
+#export-webhooks-btn::before { content: "📤"; }
+#import-webhooks-btn::before { content: "📥"; }
+#manage-appearance-btn::before { content: "🌓"; }
+#add-webhook-form button[type="submit"]::before { content: "💾"; }
+#cancel-edit-btn::before { content: "↩️"; }
+#close-manage-groups-btn::before, #close-manage-appearance-btn::before { content: "✖️"; }
+.edit-btn::before { content: "✏️"; }
+.duplicate-btn::before { content: "📄"; }
+.delete-btn::before { content: "🗑️"; }
+.remove-header-btn::before { content: "❌"; }
+.save-group-name::before { content: "💾"; }
+.delete-group::before { content: "🗑️"; }
+.edit-group-btn::before { content: "✏️"; }
diff --git a/options/options.html b/options/options.html
index 0a739ed..8a55e52 100644
--- a/options/options.html
+++ b/options/options.html
@@ -2,6 +2,7 @@
+
__MSG_optionsTitle__
@@ -10,6 +11,8 @@
__MSG_optionsPageHeader__
__MSG_optionsPageDescription__
+
+
@@ -28,6 +31,23 @@
__MSG_optionsAddWebhookHeader__
required
/>
+
+
+
+
+
×
+
__MSG_optionsAppearanceHeader__
+
+
+
+
+
+
+
+
+
+
diff --git a/options/options.js b/options/options.js
index f161d0d..395c925 100644
--- a/options/options.js
+++ b/options/options.js
@@ -72,7 +72,7 @@ const loadWebhooks = async () => {
const labelSpan = document.createElement("span");
labelSpan.classList.add("label");
- labelSpan.textContent = webhook.label;
+ labelSpan.textContent = `${webhook.emoji ? webhook.emoji + ' ' : ''}${webhook.label}`;
const urlSpan = document.createElement("span");
urlSpan.classList.add("url");
@@ -130,7 +130,7 @@ const loadWebhooks = async () => {
const labelSpan = document.createElement("span");
labelSpan.classList.add("label");
- labelSpan.textContent = webhook.label;
+ labelSpan.textContent = `${webhook.emoji ? webhook.emoji + ' ' : ''}${webhook.label}`;
const urlSpan = document.createElement("span");
urlSpan.classList.add("url");
@@ -374,6 +374,7 @@ const labelInput = document.getElementById("webhook-label");
const urlInput = document.getElementById("webhook-url");
const methodSelect = document.getElementById("webhook-method");
const identifierInput = document.getElementById("webhook-identifier");
+const emojiInput = document.getElementById("webhook-emoji");
const headersListDiv = document.getElementById("headers-list");
const headerKeyInput = document.getElementById("header-key");
const headerValueInput = document.getElementById("header-value");
@@ -383,6 +384,7 @@ const showAddWebhookBtn = document.getElementById("add-new-webhook-btn");
const testWebhookBtn = document.getElementById("test-webhook-btn");
const formStatusMessage = document.getElementById("form-status-message");
const manageGroupsBtn = document.getElementById("manage-groups-btn");
+const manageAppearanceBtn = document.getElementById("manage-appearance-btn");
const customPayloadInput = document.getElementById("webhook-custom-payload");
const variablesAutocomplete = document.getElementById("variables-autocomplete");
const toggleCustomPayloadBtn = document.getElementById("toggle-custom-payload");
@@ -395,11 +397,20 @@ const importWebhooksBtn = document.getElementById("import-webhooks-btn");
const importWebhooksInput = document.getElementById("import-webhooks-input");
const groupSelect = document.getElementById("webhook-group");
const manageGroupsModal = document.getElementById("manage-groups-modal");
+const manageAppearanceModal = document.getElementById("manage-appearance-modal");
const newGroupNameInput = document.getElementById("new-group-name");
const addGroupBtn = document.getElementById("add-group-btn");
const groupsList = document.getElementById("groups-list");
const closeManageGroupsBtn = document.getElementById("close-manage-groups-btn");
const closeManageGroups = document.querySelector("#manage-groups-modal .close-manage-groups");
+const closeManageAppearanceBtn = document.getElementById("close-manage-appearance-btn");
+const closeManageAppearance = document.querySelector("#manage-appearance-modal .close-manage-appearance");
+const themeSelect = document.getElementById("theme-select");
+// Emoji picker elements
+const openEmojiPickerBtn = document.getElementById('open-emoji-picker-btn');
+const clearEmojiBtn = document.getElementById('clear-emoji-btn');
+const emojiPicker = document.getElementById('emoji-picker');
+const emojiGrid = document.getElementById('emoji-grid');
let headers = [];
async function exportWebhooks() {
@@ -423,7 +434,9 @@ async function handleImport(event) {
const data = JSON.parse(text);
const hooks = Array.isArray(data) ? data : data.webhooks;
if (Array.isArray(hooks)) {
- await saveWebhooks(hooks);
+ // Backward compatibility: ensure emoji field exists
+ const normalized = hooks.map(h => ({ ...h, emoji: h.emoji || "" }));
+ await saveWebhooks(normalized);
loadWebhooks();
}
} catch (e) {
@@ -456,8 +469,10 @@ const importData = async (file) => {
reader.onload = async (event) => {
try {
const data = JSON.parse(event.target.result);
- const webhooks = Array.isArray(data.webhooks) ? data.webhooks : [];
+ let webhooks = Array.isArray(data.webhooks) ? data.webhooks : [];
const groups = Array.isArray(data.groups) ? data.groups : [];
+ // Ensure emoji field exists
+ webhooks = webhooks.map(h => ({ ...h, emoji: h.emoji || "" }));
await browser.storage.sync.set({ webhooks, groups });
await loadWebhooks();
if (typeof renderGroups === 'function') await renderGroups();
@@ -496,8 +511,10 @@ if (importWebhooksBtn && importWebhooksInput) {
reader.onload = async (event) => {
try {
const data = JSON.parse(event.target.result);
- const webhooks = Array.isArray(data.webhooks) ? data.webhooks : [];
+ let webhooks = Array.isArray(data.webhooks) ? data.webhooks : [];
const groups = Array.isArray(data.groups) ? data.groups : [];
+ // Ensure emoji field exists
+ webhooks = webhooks.map(h => ({ ...h, emoji: h.emoji || "" }));
await browser.storage.sync.set({ webhooks, groups });
await loadWebhooks();
if (typeof renderGroups === 'function') await renderGroups();
@@ -528,6 +545,16 @@ manageGroupsBtn.addEventListener('click', async () => {
await renderGroups();
});
+// Manage appearance button
+if (manageAppearanceBtn) {
+ manageAppearanceBtn.addEventListener('click', async () => {
+ manageAppearanceModal.classList.remove('hidden');
+ // Initialize theme select when opening
+ const { theme } = await browser.storage.sync.get('theme');
+ themeSelect.value = theme || 'system';
+ });
+}
+
// Close modal functions
const closeManageGroupsModalFunc = () => {
manageGroupsModal.classList.add('hidden');
@@ -537,6 +564,13 @@ const closeManageGroupsModalFunc = () => {
closeManageGroupsBtn.addEventListener('click', closeManageGroupsModalFunc);
closeManageGroups.addEventListener('click', closeManageGroupsModalFunc);
+// Close appearance modal
+const closeManageAppearanceModalFunc = () => {
+ manageAppearanceModal.classList.add('hidden');
+};
+if (closeManageAppearanceBtn) closeManageAppearanceBtn.addEventListener('click', closeManageAppearanceModalFunc);
+if (closeManageAppearance) closeManageAppearance.addEventListener('click', closeManageAppearanceModalFunc);
+
// Close modals when clicking outside
manageGroupsModal.addEventListener('click', (e) => {
if (e.target === manageGroupsModal) {
@@ -544,6 +578,15 @@ manageGroupsModal.addEventListener('click', (e) => {
}
});
+// Close appearance modal when clicking outside
+if (manageAppearanceModal) {
+ manageAppearanceModal.addEventListener('click', (e) => {
+ if (e.target === manageAppearanceModal) {
+ closeManageAppearanceModalFunc();
+ }
+ });
+}
+
// Add group
addGroupBtn.addEventListener('click', async () => {
const groupName = newGroupNameInput.value.trim();
@@ -751,6 +794,7 @@ form.addEventListener("submit", async (e) => {
const url = urlInput.value.trim();
const method = methodSelect.value;
const identifier = identifierInput.value.trim();
+ const emoji = emojiInput ? emojiInput.value.trim() : "";
const urlFilter = urlFilterInput.value.trim();
const customPayload = customPayloadInput.value.trim();
const groupId = groupSelect.value || null;
@@ -768,7 +812,8 @@ form.addEventListener("submit", async (e) => {
identifier,
customPayload: customPayload || null,
urlFilter: urlFilter || "",
- groupId
+ groupId,
+ emoji: emoji || ""
} : wh
);
editWebhookId = null;
@@ -784,7 +829,8 @@ form.addEventListener("submit", async (e) => {
identifier,
customPayload: customPayload || null,
urlFilter: urlFilter || "",
- groupId
+ groupId,
+ emoji: emoji || ""
};
webhooks.push(newWebhook);
}
@@ -794,6 +840,7 @@ form.addEventListener("submit", async (e) => {
urlInput.value = "";
methodSelect.value = "POST";
identifierInput.value = "";
+ if (emojiInput) emojiInput.value = "";
urlFilterInput.value = "";
customPayloadInput.value = "";
headerKeyInput.value = "";
@@ -892,6 +939,7 @@ webhookList.addEventListener("click", async (e) => {
urlFilterInput.value = webhook.urlFilter || "";
customPayloadInput.value = webhook.customPayload || "";
groupSelect.value = webhook.groupId || "";
+ if (emojiInput) emojiInput.value = webhook.emoji || "";
headers = Array.isArray(webhook.headers) ? [...webhook.headers] : [];
renderHeaders();
cancelEditBtn.classList.remove("hidden");
@@ -918,6 +966,7 @@ webhookList.addEventListener("click", async (e) => {
urlFilterInput.value = webhook.urlFilter || "";
customPayloadInput.value = webhook.customPayload || "";
groupSelect.value = webhook.groupId || "";
+ if (emojiInput) emojiInput.value = webhook.emoji || "";
headers = Array.isArray(webhook.headers) ? [...webhook.headers] : [];
renderHeaders();
cancelEditBtn.classList.remove("hidden");
@@ -944,6 +993,7 @@ cancelEditBtn.addEventListener("click", () => {
headerKeyInput.value = "";
headerValueInput.value = "";
groupSelect.value = "";
+ if (emojiInput) emojiInput.value = "";
headers = [];
renderHeaders();
cancelEditBtn.classList.add("hidden");
@@ -1072,6 +1122,59 @@ document.addEventListener("DOMContentLoaded", () => {
// Load webhooks
loadWebhooks();
+
+ // Initialize theme select
+ if (themeSelect) {
+ browser.storage.sync.get("theme").then((res) => {
+ const value = res && res.theme ? res.theme : "system";
+ themeSelect.value = value;
+ });
+
+ themeSelect.addEventListener("change", () => {
+ const value = themeSelect.value;
+ browser.storage.sync.set({ theme: value });
+ });
+ }
+
+ // Initialize emoji picker if present
+ if (openEmojiPickerBtn && emojiPicker && emojiGrid && emojiInput) {
+ const EMOJIS = [
+ '🔔','✅','🚀','⚠️','❗','ℹ️','⭐','🔥','🛠️','🧪','📦','📣','📝','🧰','💡','🖥️','📱','🌐','🔒','🔑','🛡️','💾','📤','📥','🔄','⏱️','🕒','🧭','📊','📈','🧹','🧼','🧯','🧩','🧠','🛎️','🚨','🎉','🧵','🔍','🧰','🔗','✉️','📫','📬','🛰️','🌟','🎯','🪄','🧲','🧪','🧱','🪵'
+ ];
+
+ function renderEmojiGrid() {
+ emojiGrid.textContent = '';
+ EMOJIS.forEach(e => {
+ const btn = document.createElement('button');
+ btn.type = 'button';
+ btn.className = 'emoji-item';
+ btn.textContent = e;
+ btn.addEventListener('click', () => {
+ emojiInput.value = e;
+ emojiPicker.classList.add('hidden');
+ emojiInput.focus();
+ });
+ emojiGrid.appendChild(btn);
+ });
+ }
+
+ openEmojiPickerBtn.addEventListener('click', () => {
+ if (emojiPicker.classList.contains('hidden')) {
+ renderEmojiGrid();
+ emojiPicker.classList.remove('hidden');
+ } else {
+ emojiPicker.classList.add('hidden');
+ }
+ });
+
+ if (clearEmojiBtn) {
+ clearEmojiBtn.addEventListener('click', () => {
+ emojiInput.value = '';
+ emojiInput.focus();
+ });
+ }
+
+ }
});
// Export functions for testing in Node environment
diff --git a/popup/popup.css b/popup/popup.css
index b73b68e..a3bf0d5 100644
--- a/popup/popup.css
+++ b/popup/popup.css
@@ -1,107 +1,181 @@
+/* Theme variables */
+:root {
+ --bg: #f7f8fa;
+ --card-bg: #ffffff;
+ --text: #1f2937;
+ --muted: #6b7280;
+ --primary: #2563eb;
+ --primary-contrast: #ffffff;
+ --border: #e5e7eb;
+ --header-grad: linear-gradient(135deg, #6366f1, #2563eb);
+ --btn-bg: linear-gradient(135deg, #4f46e5, #2563eb);
+ --btn-bg-hover: linear-gradient(135deg, #4338ca, #1d4ed8);
+ --success-bg: #dcfce7;
+ --success-text: #166534;
+ --error-bg: #fee2e2;
+ --error-text: #991b1b;
+ --shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: #0b0f14;
+ --card-bg: #0f172a;
+ --text: #e5e7eb;
+ --muted: #9ca3af;
+ --primary: #60a5fa;
+ --primary-contrast: #0b0f14;
+ --border: #223045;
+ --header-grad: linear-gradient(135deg, #0ea5e9, #6366f1);
+ --btn-bg: linear-gradient(135deg, #0284c7, #4f46e5);
+ --btn-bg-hover: linear-gradient(135deg, #0369a1, #4338ca);
+ --success-bg: #064e3b;
+ --success-text: #a7f3d0;
+ --error-bg: #7f1d1d;
+ --error-text: #fecaca;
+ --shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
+ }
+}
+
+/* Explicit overrides */
+[data-theme="light"] {
+ --bg: #f7f8fa;
+ --card-bg: #ffffff;
+ --text: #1f2937;
+ --muted: #6b7280;
+ --primary: #2563eb;
+ --primary-contrast: #ffffff;
+ --border: #e5e7eb;
+ --header-grad: linear-gradient(135deg, #6366f1, #2563eb);
+ --btn-bg: linear-gradient(135deg, #4f46e5, #2563eb);
+ --btn-bg-hover: linear-gradient(135deg, #4338ca, #1d4ed8);
+ --success-bg: #dcfce7;
+ --success-text: #166534;
+ --error-bg: #fee2e2;
+ --error-text: #991b1b;
+ --shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
+}
+
+[data-theme="dark"] {
+ --bg: #0b0f14;
+ --card-bg: #0f172a;
+ --text: #e5e7eb;
+ --muted: #9ca3af;
+ --primary: #60a5fa;
+ --primary-contrast: #0b0f14;
+ --border: #223045;
+ --header-grad: linear-gradient(135deg, #0ea5e9, #6366f1);
+ --btn-bg: linear-gradient(135deg, #0284c7, #4f46e5);
+ --btn-bg-hover: linear-gradient(135deg, #0369a1, #4338ca);
+ --success-bg: #064e3b;
+ --success-text: #a7f3d0;
+ --error-bg: #7f1d1d;
+ --error-text: #fecaca;
+ --shadow: 0 8px 20px rgba(0, 0, 0, 0.35);
+}
+
+/* Base layout */
body {
- font-family:
- -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
- sans-serif;
- width: 300px;
- padding: 0;
- margin: 0;
+ font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial,
+ sans-serif;
+ width: 320px;
+ padding: 0;
+ margin: 0;
+ background: var(--bg);
+ color: var(--text);
}
.container {
- padding: 15px;
-}
-
-.header {
- text-align: center;
- border-bottom: 1px solid #ccc;
- padding-bottom: 10px;
- margin-bottom: 15px;
-}
-
-.header h3 {
- margin: 0;
- color: #333;
+ padding: 14px 14px 12px 14px;
+ background: transparent;
+ box-shadow: none;
}
#buttons-container {
- display: flex;
- flex-direction: column;
- gap: 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
}
.group-header {
- font-weight: bold;
- color: #555;
- margin-top: 10px;
- margin-bottom: 5px;
- padding-bottom: 5px;
- border-bottom: 1px solid #eee;
+ display: block;
+ font-weight: 700;
+ color: var(--text);
+ margin: 16px 0 6px 0;
+ padding-left: 10px;
+ border-left: 3px solid var(--primary);
+ font-size: 13px;
+ letter-spacing: 0.2px;
}
.webhook-btn {
- width: 100%;
- padding: 12px;
- font-size: 1em;
- color: #fff;
- background-color: #007bff;
- border: none;
- border-radius: 5px;
- cursor: pointer;
- transition:
- background-color 0.2s ease-in-out,
- transform 0.1s ease;
+ width: 100%;
+ padding: 12px 12px;
+ font-size: 14px;
+ color: var(--primary-contrast);
+ background: var(--btn-bg);
+ border: 1px solid transparent;
+ border-radius: 10px;
+ cursor: pointer;
+ transition: transform 0.06s ease, box-shadow 0.2s ease, background 0.2s ease;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.1);
}
+
.webhook-btn:hover {
- background-color: #0056b3;
+ background: var(--btn-bg-hover);
+ box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}
.webhook-btn:active {
- transform: scale(0.98);
+ transform: translateY(1px);
}
.webhook-btn:disabled {
- background-color: #6c757d;
- cursor: not-allowed;
+ opacity: 0.6;
+ cursor: not-allowed;
}
.no-hooks-msg {
- color: #666;
- text-align: center;
- padding: 10px;
+ color: var(--muted);
+ text-align: center;
+ padding: 10px;
}
#status-message {
- margin-top: 15px;
- padding: 8px;
- text-align: center;
- border-radius: 4px;
- font-size: 0.9em;
+ margin-top: 12px;
+ padding: 8px 10px;
+ text-align: center;
+ border-radius: 8px;
+ font-size: 12px;
+ border: 1px solid var(--border);
}
#status-message.success {
- background-color: #d4edda;
- color: #155724;
+ background-color: var(--success-bg);
+ color: var(--success-text);
+ border-color: transparent;
}
#status-message.error {
- background-color: #f8d7da;
- color: #721c24;
+ background-color: var(--error-bg);
+ color: var(--error-text);
+ border-color: transparent;
}
.footer {
- margin-top: 15px;
- padding-top: 10px;
- border-top: 1px solid #ccc;
- text-align: center;
+ margin-top: 12px;
+ padding-top: 2px;
+ text-align: center;
}
.footer a {
- color: #007bff;
- text-decoration: none;
- font-size: 0.9em;
+ color: var(--primary);
+ text-decoration: none;
+ font-size: 12px;
}
.footer a:hover {
- text-decoration: underline;
+ text-decoration: underline;
}
diff --git a/popup/popup.html b/popup/popup.html
index d2325c3..e030844 100644
--- a/popup/popup.html
+++ b/popup/popup.html
@@ -2,14 +2,12 @@
+
__MSG_popupTitle__
-
diff --git a/popup/popup.js b/popup/popup.js
index e6c0e53..e53f944 100644
--- a/popup/popup.js
+++ b/popup/popup.js
@@ -7,6 +7,20 @@ document.addEventListener("DOMContentLoaded", async () => {
const buttonsContainer = document.getElementById("buttons-container");
+ // Apply theme preference (system | light | dark)
+ try {
+ const themeResult = await browserAPI.storage.sync.get("theme");
+ const theme = themeResult && themeResult.theme ? themeResult.theme : "system";
+ const root = document.documentElement;
+ if (theme === "light" || theme === "dark") {
+ root.setAttribute("data-theme", theme);
+ } else {
+ root.removeAttribute("data-theme");
+ }
+ } catch (_) {
+ // ignore theme errors, fallback to system
+ }
+
// Load webhooks and groups from storage
const { webhooks = [], groups = [] } = await browserAPI.storage.sync.get(["webhooks", "groups"]);
const tabs = await browserAPI.tabs.query({ active: true, currentWindow: true });
@@ -51,9 +65,10 @@ document.addEventListener("DOMContentLoaded", async () => {
// Create a button for each webhook in this group
groupWebhooks.forEach((webhook) => {
const button = document.createElement("button");
- button.textContent = webhook.label;
+ const displayLabel = `${webhook.emoji ? webhook.emoji + ' ' : ''}${webhook.label}`;
+ button.textContent = displayLabel;
button.dataset.url = webhook.url;
- button.dataset.label = webhook.label;
+ button.dataset.label = displayLabel;
button.dataset.webhookId = webhook.id;
button.classList.add("webhook-btn");
buttonsContainer.appendChild(button);
@@ -73,9 +88,10 @@ document.addEventListener("DOMContentLoaded", async () => {
// Create a button for each ungrouped webhook
ungroupedWebhooks.forEach((webhook) => {
const button = document.createElement("button");
- button.textContent = webhook.label;
+ const displayLabel = `${webhook.emoji ? webhook.emoji + ' ' : ''}${webhook.label}`;
+ button.textContent = displayLabel;
button.dataset.url = webhook.url;
- button.dataset.label = webhook.label;
+ button.dataset.label = displayLabel;
button.dataset.webhookId = webhook.id;
button.classList.add("webhook-btn");
buttonsContainer.appendChild(button);
diff --git a/tests/exportImport.test.js b/tests/exportImport.test.js
index ecabcaf..9f551c9 100644
--- a/tests/exportImport.test.js
+++ b/tests/exportImport.test.js
@@ -155,7 +155,7 @@ describe('export and import logic', () => {
await handleImport(event);
- expect(global.browser.storage.sync.set).toHaveBeenCalledWith({ webhooks: hooks });
+ expect(global.browser.storage.sync.set).toHaveBeenCalledWith({ webhooks: hooks.map(h => ({...h, emoji: ''})) });
expect(event.target.value).toBe('');
});
});
diff --git a/tests/options.test.js b/tests/options.test.js
index c348b90..2a0c08f 100644
--- a/tests/options.test.js
+++ b/tests/options.test.js
@@ -208,7 +208,8 @@ describe('options page', () => {
identifier: 'test-identifier',
customPayload,
urlFilter: 'example.com',
- groupId: null
+ groupId: null,
+ emoji: ''
}]
});
});
diff --git a/utils/utils.js b/utils/utils.js
index 79e9635..98dd95d 100644
--- a/utils/utils.js
+++ b/utils/utils.js
@@ -32,6 +32,15 @@ const getBrowserAPI = () => {
function replaceI18nPlaceholders() {
const browserAPI = getBrowserAPI();
+ // Helper to replace __MSG_key__ tokens within a string
+ const replaceTokens = (value) => {
+ if (!value || typeof value !== 'string' || !value.includes('__MSG_')) return value;
+ return value.replace(/__MSG_([^_]+)__/g, (match, key) => {
+ const msg = browserAPI.i18n?.getMessage ? browserAPI.i18n.getMessage(key) : '';
+ return msg || match;
+ });
+ };
+
// Replace textContent of elements with data-i18n attribute
const elements = document.querySelectorAll('[data-i18n]');
elements.forEach(element => {
@@ -49,37 +58,28 @@ function replaceI18nPlaceholders() {
if (element.childNodes && element.childNodes.length > 0) {
element.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE && node.nodeValue && node.nodeValue.includes('__MSG_')) {
- const matches = node.nodeValue.match(/__MSG_([^_]+)__/g);
- if (matches) {
- let newValue = node.nodeValue;
- matches.forEach(match => {
- const key = match.replace('__MSG_', '').replace('__', '');
- const message = browserAPI.i18n.getMessage(key);
- if (message) {
- newValue = newValue.replace(match, message);
- }
- });
- node.nodeValue = newValue;
- }
+ node.nodeValue = replaceTokens(node.nodeValue);
}
});
}
});
+ // Replace __MSG_...__ patterns in common attributes (e.g., placeholder, title, aria-label)
+ document.querySelectorAll('*').forEach(el => {
+ if (!el.attributes) return;
+ Array.from(el.attributes).forEach(attr => {
+ if (typeof attr.value === 'string' && attr.value.includes('__MSG_')) {
+ const newVal = replaceTokens(attr.value);
+ if (newVal !== attr.value) {
+ el.setAttribute(attr.name, newVal);
+ }
+ }
+ });
+ });
+
// Replace __MSG_...__ patterns in the document title
if (document.title && document.title.includes('__MSG_')) {
- const matches = document.title.match(/__MSG_([^_]+)__/g);
- if (matches) {
- let newTitle = document.title;
- matches.forEach(match => {
- const key = match.replace('__MSG_', '').replace('__', '');
- const message = browserAPI.i18n.getMessage(key);
- if (message) {
- newTitle = newTitle.replace(match, message);
- }
- });
- document.title = newTitle;
- }
+ document.title = replaceTokens(document.title);
}
}