A browser extension that lets you summarize any webpage and ask questions using AI.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

UI: accent swatches, theme on options, brand-colored bolt

+384 -35
+135 -26
options/options.css
··· 5 5 } 6 6 7 7 :root { 8 + --bg: #f5f0e8; 9 + --bg-subtle: #ede8de; 10 + --surface: #fff; 11 + --border: #e0d8cc; 12 + --border-hover: #bbb; 13 + --text: #1a1a1a; 14 + --text-secondary: #666; 15 + --text-muted: #aaa; 16 + --label: #555; 17 + --icon-btn: #aaa; 18 + --icon-btn-hover: #555; 8 19 --brand: #F15B2F; 9 20 --brand-hover: #D94E27; 21 + --brand-active: #BF4522; 22 + } 23 + 24 + [data-theme="dark"] { 25 + --bg: #1a1a1a; 26 + --bg-subtle: #252525; 27 + --surface: #252525; 28 + --border: #2e2e2e; 29 + --border-hover: #444; 30 + --text: #e8e3db; 31 + --text-secondary: #bbb; 32 + --text-muted: #777; 33 + --label: #aaa; 34 + --icon-btn: #666; 35 + --icon-btn-hover: #bbb; 10 36 } 11 37 12 38 body { 13 39 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 14 - background: #f5f0e8; 15 - color: #1a1a1a; 40 + background: var(--bg); 41 + color: var(--text); 16 42 line-height: 1.6; 17 43 padding: 40px 20px; 44 + transition: background 0.15s, color 0.15s; 18 45 } 19 46 20 47 .container { ··· 42 69 width: 100%; 43 70 height: 100%; 44 71 display: block; 72 + background: var(--brand); 73 + -webkit-mask: url("../lightning.svg") center / contain no-repeat; 74 + mask: url("../lightning.svg") center / contain no-repeat; 45 75 } 46 76 47 77 .page-header h1 { 48 78 font-size: 15px; 49 79 font-weight: 600; 50 - color: #1a1a1a; 80 + color: var(--text); 51 81 letter-spacing: 0.01em; 52 82 } 53 83 84 + .icon-btn { 85 + margin-left: auto; 86 + background: transparent; 87 + border: none; 88 + cursor: pointer; 89 + color: var(--icon-btn); 90 + width: 28px; 91 + height: 28px; 92 + display: flex; 93 + align-items: center; 94 + justify-content: center; 95 + border-radius: 6px; 96 + transition: color 0.1s; 97 + } 98 + 99 + .icon-btn:hover { 100 + color: var(--icon-btn-hover); 101 + } 102 + 103 + .hidden { 104 + display: none !important; 105 + } 106 + 54 107 /* ── Form ── */ 55 108 .form-group { 56 109 margin-bottom: 20px; ··· 60 113 display: block; 61 114 font-size: 12px; 62 115 font-weight: 600; 63 - color: #555; 116 + color: var(--label); 64 117 text-transform: uppercase; 65 118 letter-spacing: 0.05em; 66 119 margin-bottom: 6px; ··· 75 128 text-transform: none; 76 129 font-weight: 500; 77 130 font-size: 13.5px; 78 - color: #1a1a1a; 131 + color: var(--text); 79 132 } 80 133 81 134 input[type="checkbox"] { ··· 92 145 select { 93 146 width: 100%; 94 147 padding: 9px 12px; 95 - border: 1px solid #e0d8cc; 148 + border: 1px solid var(--border); 96 149 border-radius: 6px; 97 - background: #fff; 98 - color: #1a1a1a; 150 + background: var(--surface); 151 + color: var(--text); 99 152 font-size: 13.5px; 100 153 font-family: inherit; 101 154 transition: border-color 0.1s; ··· 125 178 126 179 .help { 127 180 font-size: 11.5px; 128 - color: #aaa; 181 + color: var(--text-muted); 129 182 margin-top: 5px; 130 183 line-height: 1.4; 131 184 } 132 185 133 186 .help code { 134 - background: #ede8de; 187 + background: var(--bg-subtle); 135 188 padding: 1px 5px; 136 189 border-radius: 3px; 137 190 font-family: 'SF Mono', 'Cascadia Code', monospace; 138 191 font-size: 11px; 139 - color: #555; 192 + color: var(--label); 140 193 } 141 194 142 195 /* ── Buttons ── */ ··· 164 217 } 165 218 166 219 .btn-primary:hover { background: var(--brand-hover); } 220 + .btn-primary:active { background: var(--brand-active); } 221 + 222 + .accent-custom-group { 223 + margin-top: 8px; 224 + } 225 + 226 + .accent-custom-group.hidden { 227 + display: none; 228 + } 229 + 230 + .accent-swatches { 231 + display: flex; 232 + align-items: center; 233 + gap: 10px; 234 + margin-top: 4px; 235 + } 236 + 237 + .accent-swatch { 238 + width: 22px; 239 + height: 22px; 240 + border-radius: 999px; 241 + border: 1px solid transparent; 242 + padding: 0; 243 + display: inline-flex; 244 + align-items: center; 245 + justify-content: center; 246 + color: #fff; 247 + font-size: 12px; 248 + font-weight: 700; 249 + line-height: 1; 250 + cursor: pointer; 251 + transition: transform 0.08s ease, box-shadow 0.08s ease, border-color 0.08s ease; 252 + } 253 + 254 + .accent-swatch:hover { 255 + transform: scale(1.06); 256 + } 257 + 258 + .accent-swatch.selected { 259 + border-color: var(--label); 260 + box-shadow: none; 261 + } 262 + 263 + .accent-swatch[data-accent-preset="orange"] { background: #F15B2F; } 264 + .accent-swatch[data-accent-preset="blue"] { background: #2F80ED; } 265 + .accent-swatch[data-accent-preset="green"] { background: #2FA36B; } 266 + .accent-swatch[data-accent-preset="purple"] { background: #7E57C2; } 267 + .accent-swatch[data-accent-preset="teal"] { background: #14B8A6; } 268 + .accent-swatch[data-accent-preset="pink"] { background: #EC4899; } 269 + .accent-swatch[data-accent-preset="indigo"] { background: #4F46E5; } 270 + 271 + .accent-swatch.custom-swatch { 272 + background: var(--surface); 273 + color: var(--text-muted); 274 + border-color: var(--border); 275 + } 167 276 168 277 .btn-secondary { 169 278 padding: 8px 14px; 170 279 background: transparent; 171 - color: #777; 172 - border: 1px solid #e0d8cc; 280 + color: var(--text-muted); 281 + border: 1px solid var(--border); 173 282 border-radius: 6px; 174 283 font-size: 12.5px; 175 284 font-weight: 500; 176 285 } 177 286 178 287 .btn-secondary:hover { 179 - color: #333; 180 - border-color: #bbb; 288 + color: var(--text); 289 + border-color: var(--border-hover); 181 290 } 182 291 183 292 .btn-link { 184 293 padding: 8px 4px; 185 294 background: transparent; 186 295 border: none; 187 - color: #bbb; 296 + color: var(--text-muted); 188 297 font-size: 12px; 189 298 text-decoration: underline; 190 299 } 191 300 192 - .btn-link:hover { color: #777; } 301 + .btn-link:hover { color: var(--text-secondary); } 193 302 194 303 /* ── Status ── */ 195 304 .status { ··· 217 326 218 327 .status.loading { 219 328 display: block; 220 - background: #faf8f4; 221 - color: #aaa; 222 - border: 1px solid #e0d8cc; 329 + background: var(--bg-subtle); 330 + color: var(--text-muted); 331 + border: 1px solid var(--border); 223 332 } 224 333 225 334 /* ── Divider ── */ 226 335 .divider { 227 336 border: none; 228 - border-top: 1px solid #e0d8cc; 337 + border-top: 1px solid var(--border); 229 338 margin: 28px 0; 230 339 } 231 340 ··· 233 342 .info-section h2 { 234 343 font-size: 12px; 235 344 font-weight: 600; 236 - color: #555; 345 + color: var(--label); 237 346 text-transform: uppercase; 238 347 letter-spacing: 0.05em; 239 348 margin-bottom: 10px; ··· 245 354 246 355 .info-section p { 247 356 font-size: 13px; 248 - color: #666; 357 + color: var(--text-secondary); 249 358 margin-bottom: 8px; 250 359 } 251 360 ··· 253 362 .info-section ul { 254 363 margin-left: 18px; 255 364 font-size: 13px; 256 - color: #666; 365 + color: var(--text-secondary); 257 366 } 258 367 259 368 .info-section li { ··· 261 370 } 262 371 263 372 .info-section code { 264 - background: #ede8de; 373 + background: var(--bg-subtle); 265 374 padding: 1px 5px; 266 375 border-radius: 3px; 267 376 font-family: 'SF Mono', 'Cascadia Code', monospace; 268 377 font-size: 11px; 269 - color: #555; 378 + color: var(--label); 270 379 } 271 380 272 381 .info-section a {
+49 -1
options/options.html
··· 9 9 <div class="container"> 10 10 <div class="page-header"> 11 11 <div class="logo-mark"> 12 - <img src="../lightning.svg" alt="" class="brand-icon" /> 12 + <span class="brand-icon" aria-hidden="true"></span> 13 13 </div> 14 14 <h1>Settings</h1> 15 + <button id="theme-btn" class="icon-btn" title="Toggle theme"> 16 + <svg id="theme-icon-light" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 17 + <circle cx="12" cy="12" r="5"/> 18 + <line x1="12" y1="1" x2="12" y2="3"/> 19 + <line x1="12" y1="21" x2="12" y2="23"/> 20 + <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/> 21 + <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/> 22 + <line x1="1" y1="12" x2="3" y2="12"/> 23 + <line x1="21" y1="12" x2="23" y2="12"/> 24 + <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/> 25 + <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/> 26 + </svg> 27 + <svg id="theme-icon-dark" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="hidden"> 28 + <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/> 29 + </svg> 30 + <svg id="theme-icon-system" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="hidden"> 31 + <rect x="2" y="3" width="20" height="14" rx="2" ry="2"/> 32 + <line x1="8" y1="21" x2="16" y2="21"/> 33 + <line x1="12" y1="17" x2="12" y2="21"/> 34 + </svg> 35 + </button> 15 36 </div> 16 37 17 38 <form id="settings-form"> ··· 52 73 <p class="help"> 53 74 Examples: <code>gemma3:1b</code>, <code>llama3.2</code>, 54 75 <code>gpt-4o-mini</code> 76 + </p> 77 + </div> 78 + 79 + <div class="form-group"> 80 + <label>Accent Color</label> 81 + <input type="hidden" id="accent-preset" value="orange" /> 82 + <div class="accent-swatches" role="radiogroup" aria-label="Accent Color"> 83 + <button type="button" class="accent-swatch selected" data-accent-preset="orange" title="Orange (default)" aria-label="Orange (default)"></button> 84 + <button type="button" class="accent-swatch" data-accent-preset="blue" title="Blue" aria-label="Blue"></button> 85 + <button type="button" class="accent-swatch" data-accent-preset="green" title="Green" aria-label="Green"></button> 86 + <button type="button" class="accent-swatch" data-accent-preset="purple" title="Purple" aria-label="Purple"></button> 87 + <button type="button" class="accent-swatch" data-accent-preset="teal" title="Teal" aria-label="Teal"></button> 88 + <button type="button" class="accent-swatch" data-accent-preset="pink" title="Pink" aria-label="Pink"></button> 89 + <button type="button" class="accent-swatch" data-accent-preset="indigo" title="Indigo" aria-label="Indigo"></button> 90 + <button type="button" class="accent-swatch custom-swatch" data-accent-preset="custom" title="Custom color" aria-label="Custom color">+</button> 91 + </div> 92 + <div id="accent-custom-group" class="accent-custom-group hidden"> 93 + <input 94 + type="text" 95 + id="accent-custom" 96 + placeholder="#F15B2F" 97 + maxlength="7" 98 + /> 99 + </div> 100 + <p class="help"> 101 + Used for primary actions and accent highlights in the UI. 102 + Custom must be a 6-digit hex code like <code>#F15B2F</code>. 55 103 </p> 56 104 </div> 57 105
+151 -4
options/options.js
··· 4 4 const apiModeInput = document.getElementById("api-mode"); 5 5 const apiBaseUrlInput = document.getElementById("api-base-url"); 6 6 const modelInput = document.getElementById("model"); 7 + const accentPresetInput = document.getElementById("accent-preset"); 8 + const accentSwatchButtons = Array.from(document.querySelectorAll(".accent-swatch")); 9 + const accentCustomGroup = document.getElementById("accent-custom-group"); 10 + const accentCustomInput = document.getElementById("accent-custom"); 11 + const themeBtn = document.getElementById("theme-btn"); 7 12 const apiKeyInput = document.getElementById("api-key"); 8 13 const disableThinkingInput = document.getElementById("disable-thinking"); 9 14 const thinkingModeGroup = document.getElementById("thinking-mode-group"); ··· 15 20 apiMode: "ollama", 16 21 apiBaseUrl: "http://localhost:11434", 17 22 model: "gpt-oss:20b-cloud", 23 + accentPreset: "orange", 24 + accentColor: "#F15B2F", 18 25 apiKey: "", 19 26 disableThinking: false, 20 27 }; 21 28 22 - // Load settings on page load 23 - document.addEventListener("DOMContentLoaded", loadSettings); 29 + const ACCENT_PRESETS = { 30 + orange: "#F15B2F", 31 + blue: "#2F80ED", 32 + green: "#2FA36B", 33 + purple: "#7E57C2", 34 + teal: "#14B8A6", 35 + pink: "#EC4899", 36 + indigo: "#4F46E5", 37 + }; 38 + 39 + // Load settings/theme on page load 40 + document.addEventListener("DOMContentLoaded", initializePage); 41 + 42 + const THEMES = ["light", "dark", "system"]; 43 + let currentTheme = "system"; 24 44 25 45 // Update URL placeholder and thinking mode visibility when mode changes 26 46 apiModeInput.addEventListener("change", () => { ··· 33 53 } 34 54 }); 35 55 56 + accentSwatchButtons.forEach((btn) => { 57 + btn.addEventListener("click", () => { 58 + setSelectedAccentPreset(btn.dataset.accentPreset); 59 + const color = resolveAccentColor(); 60 + if (color) applyAccentColor(color); 61 + }); 62 + }); 63 + 64 + accentCustomInput.addEventListener("input", () => { 65 + const color = normalizeHexColor(accentCustomInput.value); 66 + if (accentPresetInput.value === "custom" && color) { 67 + applyAccentColor(color); 68 + } 69 + }); 70 + 71 + themeBtn.addEventListener("click", async () => { 72 + const idx = THEMES.indexOf(currentTheme); 73 + currentTheme = THEMES[(idx + 1) % THEMES.length]; 74 + applyTheme(currentTheme); 75 + await chrome.storage.sync.set({ theme: currentTheme }); 76 + }); 77 + 78 + window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { 79 + if (currentTheme === "system") { 80 + applyTheme("system"); 81 + } 82 + }); 83 + 36 84 // Save settings 37 85 form.addEventListener("submit", async (e) => { 38 86 e.preventDefault(); ··· 41 89 apiMode: apiModeInput.value, 42 90 apiBaseUrl: apiBaseUrlInput.value.trim() || defaultSettings.apiBaseUrl, 43 91 model: modelInput.value.trim() || defaultSettings.model, 92 + accentPreset: accentPresetInput.value, 93 + accentColor: resolveAccentColor(), 44 94 apiKey: apiKeyInput.value.trim(), 45 95 disableThinking: disableThinkingInput.checked, 46 96 }; 97 + 98 + if (!settings.accentColor) { 99 + showStatus("❌ Custom accent color must be a valid hex code (e.g. #F15B2F).", "error"); 100 + return; 101 + } 47 102 48 103 try { 49 104 await chrome.storage.sync.set(settings); ··· 119 174 apiModeInput.value = defaultSettings.apiMode; 120 175 apiBaseUrlInput.value = defaultSettings.apiBaseUrl; 121 176 modelInput.value = defaultSettings.model; 177 + setSelectedAccentPreset(defaultSettings.accentPreset); 178 + accentCustomInput.value = defaultSettings.accentColor; 179 + applyAccentColor(defaultSettings.accentColor); 122 180 apiKeyInput.value = defaultSettings.apiKey; 123 181 disableThinkingInput.checked = defaultSettings.disableThinking; 124 182 // Show/hide thinking mode group based on API mode ··· 144 202 function createShortcutsInfo() { 145 203 const infoDiv = document.createElement("div"); 146 204 infoDiv.id = "shortcuts-info"; 147 - infoDiv.style.cssText = "margin-top:10px;padding:10px 14px;background:#faf8f4;border:1px solid #e0d8cc;border-radius:6px;font-size:12.5px;line-height:1.5;"; 148 - infoDiv.innerHTML = 'ℹ️ To manage shortcuts in Firefox:<br>1. Type <code style="background:#ede8de;padding:1px 5px;border-radius:3px;font-size:11px;">about:addons</code> in the address bar<br>2. Click the gear icon (⚙️) → <strong>Manage Extension Shortcuts</strong>'; 205 + infoDiv.style.cssText = "margin-top:10px;padding:10px 14px;background:var(--bg-subtle);border:1px solid var(--border);border-radius:6px;font-size:12.5px;line-height:1.5;color:var(--text-secondary);"; 206 + infoDiv.innerHTML = 'ℹ️ To manage shortcuts in Firefox:<br>1. Type <code style="background:var(--bg);padding:1px 5px;border-radius:3px;font-size:11px;">about:addons</code> in the address bar<br>2. Click the gear icon (⚙️) → <strong>Manage Extension Shortcuts</strong>'; 149 207 keyboardShortcutsLink.parentNode.appendChild(infoDiv); 150 208 return infoDiv; 151 209 } 152 210 211 + async function initializePage() { 212 + const { theme } = await chrome.storage.sync.get("theme"); 213 + currentTheme = theme || "system"; 214 + applyTheme(currentTheme); 215 + await loadSettings(); 216 + } 217 + 153 218 // Load settings 154 219 async function loadSettings() { 155 220 try { ··· 158 223 apiModeInput.value = settings.apiMode; 159 224 apiBaseUrlInput.value = settings.apiBaseUrl; 160 225 modelInput.value = settings.model; 226 + let preset = settings.accentPreset || getPresetNameForColor(settings.accentColor); 227 + if (preset === "red") preset = "orange"; 228 + setSelectedAccentPreset(preset || "custom"); 229 + accentCustomInput.value = settings.accentColor || defaultSettings.accentColor; 230 + applyAccentColor(settings.accentColor || defaultSettings.accentColor); 161 231 apiKeyInput.value = settings.apiKey; 162 232 disableThinkingInput.checked = settings.disableThinking; 163 233 // Show/hide thinking mode group based on API mode ··· 165 235 } catch (error) { 166 236 showStatus("Error loading settings: " + error.message, "error"); 167 237 } 238 + } 239 + 240 + function toggleCustomAccentInput() { 241 + const isCustom = accentPresetInput.value === "custom"; 242 + accentCustomGroup.classList.toggle("hidden", !isCustom); 243 + } 244 + 245 + function setSelectedAccentPreset(preset) { 246 + accentPresetInput.value = preset; 247 + accentSwatchButtons.forEach((btn) => { 248 + btn.classList.toggle("selected", btn.dataset.accentPreset === preset); 249 + }); 250 + toggleCustomAccentInput(); 251 + } 252 + 253 + function getPresetNameForColor(color) { 254 + const normalized = normalizeHexColor(color); 255 + if (!normalized) return null; 256 + return ( 257 + Object.entries(ACCENT_PRESETS).find(([, presetColor]) => presetColor === normalized)?.[0] || 258 + null 259 + ); 260 + } 261 + 262 + function resolveAccentColor() { 263 + if (accentPresetInput.value === "custom") { 264 + return normalizeHexColor(accentCustomInput.value); 265 + } 266 + return ACCENT_PRESETS[accentPresetInput.value] || defaultSettings.accentColor; 267 + } 268 + 269 + function normalizeHexColor(value) { 270 + const raw = (value || "").trim().toUpperCase(); 271 + const withHash = raw.startsWith("#") ? raw : `#${raw}`; 272 + return /^#[0-9A-F]{6}$/.test(withHash) ? withHash : null; 273 + } 274 + 275 + function darkenHexColor(hexColor, amount) { 276 + const normalized = normalizeHexColor(hexColor); 277 + if (!normalized) return hexColor; 278 + const r = parseInt(normalized.slice(1, 3), 16); 279 + const g = parseInt(normalized.slice(3, 5), 16); 280 + const b = parseInt(normalized.slice(5, 7), 16); 281 + const factor = 1 - amount; 282 + const toHex = (n) => Math.round(Math.max(0, Math.min(255, n * factor))).toString(16).padStart(2, "0").toUpperCase(); 283 + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; 284 + } 285 + 286 + function applyAccentColor(color) { 287 + const normalized = normalizeHexColor(color) || defaultSettings.accentColor; 288 + document.documentElement.style.setProperty("--brand", normalized); 289 + document.documentElement.style.setProperty("--brand-hover", darkenHexColor(normalized, 0.1)); 290 + document.documentElement.style.setProperty("--brand-active", darkenHexColor(normalized, 0.2)); 291 + } 292 + 293 + function applyTheme(theme) { 294 + const root = document.documentElement; 295 + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; 296 + const effectiveTheme = theme === "system" ? (prefersDark ? "dark" : "light") : theme; 297 + root.setAttribute("data-theme", effectiveTheme); 298 + 299 + document 300 + .getElementById("theme-icon-light") 301 + .classList.toggle("hidden", theme !== "light"); 302 + document 303 + .getElementById("theme-icon-dark") 304 + .classList.toggle("hidden", theme !== "dark"); 305 + document 306 + .getElementById("theme-icon-system") 307 + .classList.toggle("hidden", theme !== "system"); 308 + 309 + const labels = { 310 + light: "Light mode", 311 + dark: "Dark mode", 312 + system: "System theme", 313 + }; 314 + themeBtn.title = labels[theme]; 168 315 } 169 316 170 317 // Show status message
+6
popup/popup.css
··· 134 134 width: 100%; 135 135 height: 100%; 136 136 display: block; 137 + background: var(--brand); 138 + -webkit-mask: url("../lightning.svg") center / contain no-repeat; 139 + mask: url("../lightning.svg") center / contain no-repeat; 137 140 } 138 141 139 142 .logo-text { ··· 198 201 width: 100%; 199 202 height: 100%; 200 203 display: block; 204 + background: var(--brand); 205 + -webkit-mask: url("../lightning.svg") center / contain no-repeat; 206 + mask: url("../lightning.svg") center / contain no-repeat; 201 207 } 202 208 203 209 .initial-title {
+2 -2
popup/popup.html
··· 10 10 <div class="header"> 11 11 <div class="header-left"> 12 12 <div class="logo-mark"> 13 - <img src="../lightning.svg" alt="" class="brand-icon" /> 13 + <span class="brand-icon" aria-hidden="true"></span> 14 14 </div> 15 15 <span class="logo-text">Summarize</span> 16 16 </div> ··· 52 52 <div class="content-container"> 53 53 <div id="initial-state" class="initial-state"> 54 54 <div class="initial-icon"> 55 - <img src="../lightning.svg" alt="" class="brand-icon" /> 55 + <span class="brand-icon" aria-hidden="true"></span> 56 56 </div> 57 57 <p class="initial-title">Ready to summarize</p> 58 58 <p class="initial-sub">Get an AI-powered summary of the page you're reading.</p>
+41 -2
popup/popup.js
··· 69 69 model: "gpt-oss:20b-cloud", 70 70 apiKey: "", 71 71 disableThinking: false, 72 + accentColor: "#F15B2F", 72 73 }; 73 74 74 75 async function getApiSettings() { ··· 109 110 themeBtn.title = labels[theme]; 110 111 } 111 112 113 + function normalizeHexColor(value) { 114 + const raw = (value || "").trim().toUpperCase(); 115 + const withHash = raw.startsWith("#") ? raw : `#${raw}`; 116 + return /^#[0-9A-F]{6}$/.test(withHash) ? withHash : null; 117 + } 118 + 119 + function darkenHexColor(hexColor, amount) { 120 + const normalized = normalizeHexColor(hexColor); 121 + if (!normalized) return hexColor; 122 + const r = parseInt(normalized.slice(1, 3), 16); 123 + const g = parseInt(normalized.slice(3, 5), 16); 124 + const b = parseInt(normalized.slice(5, 7), 16); 125 + const factor = 1 - amount; 126 + const toHex = (n) => 127 + Math.round(Math.max(0, Math.min(255, n * factor))) 128 + .toString(16) 129 + .padStart(2, "0") 130 + .toUpperCase(); 131 + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; 132 + } 133 + 134 + function applyAccentColor(color) { 135 + const normalized = normalizeHexColor(color) || API_SETTINGS_DEFAULTS.accentColor; 136 + document.documentElement.style.setProperty("--brand", normalized); 137 + document.documentElement.style.setProperty("--brand-hover", darkenHexColor(normalized, 0.1)); 138 + document.documentElement.style.setProperty("--brand-active", darkenHexColor(normalized, 0.2)); 139 + } 140 + 112 141 let currentTheme = "system"; 113 142 114 143 themeBtn.addEventListener("click", async () => { ··· 236 265 } 237 266 238 267 document.addEventListener("DOMContentLoaded", async () => { 239 - // Load saved theme before rendering anything 240 - currentTheme = (await chrome.storage.sync.get("theme")).theme || "system"; 268 + // Load saved theme and accent before rendering anything 269 + const { theme, accentColor } = await chrome.storage.sync.get(["theme", "accentColor"]); 270 + currentTheme = theme || "system"; 271 + applyAccentColor(accentColor || API_SETTINGS_DEFAULTS.accentColor); 241 272 applyTheme(currentTheme); 242 273 243 274 // Check if we have a target tab from background script (for Firefox popup window) ··· 349 380 await generateQuickSummary(); 350 381 } 351 382 } 383 + } 384 + }); 385 + 386 + // Apply accent changes immediately if settings are updated while popup is open 387 + chrome.storage.onChanged.addListener((changes, areaName) => { 388 + if (areaName !== "sync") return; 389 + if (changes.accentColor?.newValue) { 390 + applyAccentColor(changes.accentColor.newValue); 352 391 } 353 392 }); 354 393