Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments

Implement Light Mode

+605 -71
+126 -50
extension/content/content.js
··· 7 7 let currentSelection = null; 8 8 9 9 const OVERLAY_STYLES = ` 10 - :host { all: initial; } 10 + :host { 11 + all: initial; 12 + --bg-primary: #09090b; 13 + --bg-secondary: #0f0f12; 14 + --bg-tertiary: #18181b; 15 + --bg-card: #09090b; 16 + --bg-elevated: #18181b; 17 + --bg-hover: #27272a; 18 + 19 + --text-primary: #e4e4e7; 20 + --text-secondary: #a1a1aa; 21 + --border: #27272a; 22 + 23 + --accent: #6366f1; 24 + --accent-hover: #4f46e5; 25 + } 26 + 27 + :host(.light) { 28 + --bg-primary: #ffffff; 29 + --bg-secondary: #f4f4f5; 30 + --bg-tertiary: #e4e4e7; 31 + --bg-card: #ffffff; 32 + --bg-elevated: #f4f4f5; 33 + --bg-hover: #e4e4e7; 34 + 35 + --text-primary: #18181b; 36 + --text-secondary: #52525b; 37 + --border: #e4e4e7; 38 + 39 + --accent: #4f46e5; 40 + --accent-hover: #4338ca; 41 + } 42 + 11 43 .margin-overlay { 12 44 position: absolute; 13 45 top: 0; ··· 20 52 .margin-popover { 21 53 position: absolute; 22 54 width: 320px; 23 - background: #09090b; 24 - border: 1px solid #27272a; 55 + background: var(--bg-card); 56 + border: 1px solid var(--border); 25 57 border-radius: 12px; 26 58 padding: 0; 27 59 box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.2); ··· 30 62 pointer-events: auto; 31 63 z-index: 2147483647; 32 64 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 33 - color: #e4e4e7; 65 + color: var(--text-primary); 34 66 opacity: 0; 35 67 transform: scale(0.95); 36 68 animation: popover-in 0.15s forwards; ··· 40 72 @keyframes popover-in { to { opacity: 1; transform: scale(1); } } 41 73 .popover-header { 42 74 padding: 12px 16px; 43 - border-bottom: 1px solid #27272a; 75 + border-bottom: 1px solid var(--border); 44 76 display: flex; 45 77 justify-content: space-between; 46 78 align-items: center; 47 - background: #0f0f12; 79 + background: var(--bg-secondary); 48 80 border-radius: 12px 12px 0 0; 49 81 font-weight: 600; 50 82 font-size: 13px; 83 + color: var(--text-primary); 51 84 } 52 85 .popover-scroll-area { 53 86 overflow-y: auto; 54 87 max-height: 400px; 55 88 } 56 89 .popover-item-block { 57 - border-bottom: 1px solid #27272a; 90 + border-bottom: 1px solid var(--border); 58 91 margin-bottom: 0; 59 92 animation: fade-in 0.2s; 60 93 } ··· 68 101 gap: 8px; 69 102 } 70 103 .popover-avatar { 71 - width: 24px; height: 24px; border-radius: 50%; background: #27272a; 104 + width: 24px; height: 24px; border-radius: 50%; background: var(--bg-hover); 72 105 display: flex; align-items: center; justify-content: center; 73 - font-size: 10px; color: #a1a1aa; 106 + font-size: 10px; color: var(--text-secondary); 74 107 } 75 - .popover-handle { font-size: 12px; font-weight: 600; color: #e4e4e7; } 76 - .popover-close { background: none; border: none; color: #71717a; cursor: pointer; padding: 4px; } 77 - .popover-close:hover { color: #e4e4e7; } 78 - .popover-content { padding: 4px 16px 12px; font-size: 13px; line-height: 1.5; color: #e4e4e7; } 108 + .popover-handle { font-size: 12px; font-weight: 600; color: var(--text-primary); } 109 + .popover-close { background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 4px; } 110 + .popover-close:hover { color: var(--text-primary); } 111 + .popover-content { padding: 4px 16px 12px; font-size: 13px; line-height: 1.5; color: var(--text-primary); } 79 112 .popover-quote { 80 - margin-top: 8px; padding: 6px 10px; background: #18181b; 81 - border-left: 2px solid #6366f1; border-radius: 4px; 82 - font-size: 11px; color: #a1a1aa; font-style: italic; 113 + margin-top: 8px; padding: 6px 10px; background: var(--bg-tertiary); 114 + border-left: 2px solid var(--accent); border-radius: 4px; 115 + font-size: 11px; color: var(--text-secondary); font-style: italic; 83 116 } 84 117 .popover-actions { 85 118 padding: 8px 16px; 86 119 display: flex; justify-content: flex-end; gap: 8px; 87 120 } 88 121 .btn-action { 89 - background: none; border: 1px solid #27272a; border-radius: 4px; 90 - padding: 4px 8px; color: #a1a1aa; font-size: 11px; cursor: pointer; 122 + background: none; border: 1px solid var(--border); border-radius: 4px; 123 + padding: 4px 8px; color: var(--text-secondary); font-size: 11px; cursor: pointer; 91 124 } 92 - .btn-action:hover { background: #27272a; color: #e4e4e7; } 125 + .btn-action:hover { background: var(--bg-hover); color: var(--text-primary); } 93 126 94 127 .margin-selection-popup { 95 128 position: fixed; 96 129 display: flex; 97 130 gap: 4px; 98 131 padding: 6px; 99 - background: #09090b; 100 - border: 1px solid #27272a; 132 + background: var(--bg-card); 133 + border: 1px solid var(--border); 101 134 border-radius: 8px; 102 135 box-shadow: 0 8px 16px rgba(0,0,0,0.4); 103 136 z-index: 2147483647; ··· 113 146 background: transparent; 114 147 border: none; 115 148 border-radius: 6px; 116 - color: #e4e4e7; 149 + color: var(--text-primary); 117 150 font-size: 12px; 118 151 font-weight: 500; 119 152 cursor: pointer; 120 153 transition: background 0.15s; 121 154 } 122 155 .selection-btn:hover { 123 - background: #27272a; 156 + background: var(--bg-hover); 124 157 } 125 158 .selection-btn svg { 126 159 width: 14px; ··· 130 163 position: fixed; 131 164 width: 340px; 132 165 max-width: calc(100vw - 40px); 133 - background: #09090b; 134 - border: 1px solid #27272a; 166 + background: var(--bg-card); 167 + border: 1px solid var(--border); 135 168 border-radius: 12px; 136 169 padding: 16px; 137 170 box-sizing: border-box; ··· 139 172 z-index: 2147483647; 140 173 pointer-events: auto; 141 174 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 142 - color: #e4e4e7; 175 + color: var(--text-primary); 143 176 animation: popover-in 0.15s forwards; 144 177 overflow: hidden; 145 178 } ··· 148 181 } 149 182 .inline-compose-quote { 150 183 padding: 8px 12px; 151 - background: #18181b; 152 - border-left: 3px solid #6366f1; 184 + background: var(--bg-tertiary); 185 + border-left: 3px solid var(--accent); 153 186 border-radius: 4px; 154 187 font-size: 12px; 155 - color: #a1a1aa; 188 + color: var(--text-secondary); 156 189 font-style: italic; 157 190 margin-bottom: 12px; 158 191 max-height: 60px; ··· 163 196 width: 100%; 164 197 min-height: 80px; 165 198 padding: 10px 12px; 166 - background: #18181b; 167 - border: 1px solid #27272a; 199 + background: var(--bg-elevated); 200 + border: 1px solid var(--border); 168 201 border-radius: 8px; 169 - color: #e4e4e7; 202 + color: var(--text-primary); 170 203 font-family: inherit; 171 204 font-size: 13px; 172 205 resize: vertical; ··· 175 208 } 176 209 .inline-compose-textarea:focus { 177 210 outline: none; 178 - border-color: #6366f1; 211 + border-color: var(--accent); 179 212 } 180 213 .inline-compose-actions { 181 214 display: flex; ··· 185 218 .btn-cancel { 186 219 padding: 8px 16px; 187 220 background: transparent; 188 - border: 1px solid #27272a; 221 + border: 1px solid var(--border); 189 222 border-radius: 6px; 190 - color: #a1a1aa; 223 + color: var(--text-secondary); 191 224 font-size: 13px; 192 225 cursor: pointer; 193 226 } 194 227 .btn-cancel:hover { 195 - background: #27272a; 196 - color: #e4e4e7; 228 + background: var(--bg-hover); 229 + color: var(--text-primary); 197 230 } 198 231 .btn-submit { 199 232 padding: 8px 16px; 200 - background: #6366f1; 233 + background: var(--accent); 201 234 border: none; 202 235 border-radius: 6px; 203 236 color: white; ··· 206 239 cursor: pointer; 207 240 } 208 241 .btn-submit:hover { 209 - background: #4f46e5; 242 + background: var(--accent-hover); 210 243 } 211 244 .btn-submit:disabled { 212 245 opacity: 0.5; 213 246 cursor: not-allowed; 214 247 } 215 248 .reply-section { 216 - border-top: 1px solid #27272a; 249 + border-top: 1px solid var(--border); 217 250 padding: 12px 16px; 218 - background: #0f0f12; 251 + background: var(--bg-secondary); 219 252 border-radius: 0 0 12px 12px; 220 253 } 221 254 .reply-textarea { 222 255 width: 100%; 223 256 min-height: 60px; 224 257 padding: 8px 10px; 225 - background: #18181b; 226 - border: 1px solid #27272a; 258 + background: var(--bg-elevated); 259 + border: 1px solid var(--border); 227 260 border-radius: 6px; 228 - color: #e4e4e7; 261 + color: var(--text-primary); 229 262 font-family: inherit; 230 263 font-size: 12px; 231 264 resize: none; ··· 233 266 } 234 267 .reply-textarea:focus { 235 268 outline: none; 236 - border-color: #6366f1; 269 + border-color: var(--accent); 237 270 } 238 271 .reply-submit { 239 272 padding: 6px 12px; 240 - background: #6366f1; 273 + background: var(--accent); 241 274 border: none; 242 275 border-radius: 4px; 243 276 color: white; ··· 251 284 } 252 285 .reply-item { 253 286 padding: 8px 0; 254 - border-top: 1px solid #27272a; 287 + border-top: 1px solid var(--border); 255 288 } 256 289 .reply-item:first-child { 257 290 border-top: none; ··· 259 292 .reply-author { 260 293 font-size: 11px; 261 294 font-weight: 600; 262 - color: #a1a1aa; 295 + color: var(--text-secondary); 263 296 margin-bottom: 4px; 264 297 } 265 298 .reply-text { 266 299 font-size: 12px; 267 - color: #e4e4e7; 300 + color: var(--text-primary); 268 301 line-height: 1.4; 269 302 } 270 303 `; ··· 422 455 } 423 456 } 424 457 458 + function applyTheme(theme) { 459 + if (!sidebarHost) return; 460 + sidebarHost.classList.remove("light", "dark"); 461 + if (theme === "system" || !theme) { 462 + if (window.matchMedia("(prefers-color-scheme: light)").matches) { 463 + sidebarHost.classList.add("light"); 464 + } 465 + } else { 466 + sidebarHost.classList.add(theme); 467 + } 468 + } 469 + 470 + window 471 + .matchMedia("(prefers-color-scheme: light)") 472 + .addEventListener("change", (e) => { 473 + chrome.storage.local.get(["theme"], (result) => { 474 + if (!result.theme || result.theme === "system") { 475 + if (e.matches) { 476 + sidebarHost?.classList.add("light"); 477 + } else { 478 + sidebarHost?.classList.remove("light"); 479 + } 480 + } 481 + }); 482 + }); 483 + 425 484 function initOverlay() { 426 485 sidebarHost = document.createElement("div"); 427 486 sidebarHost.id = "margin-overlay-host"; ··· 456 515 if (document.documentElement) observer.observe(document.documentElement); 457 516 458 517 if (typeof chrome !== "undefined" && chrome.storage) { 459 - chrome.storage.local.get(["showOverlay"], (result) => { 518 + chrome.storage.local.get(["showOverlay", "theme"], (result) => { 519 + applyTheme(result.theme); 460 520 if (result.showOverlay === false) { 461 521 sidebarHost.style.display = "none"; 462 522 } else { ··· 469 529 470 530 document.addEventListener("mousemove", handleMouseMove); 471 531 document.addEventListener("click", handleDocumentClick, true); 532 + 533 + chrome.storage.onChanged.addListener((changes, area) => { 534 + if (area === "local") { 535 + if (changes.theme) { 536 + applyTheme(changes.theme.newValue); 537 + } 538 + if (changes.showOverlay) { 539 + if (changes.showOverlay.newValue === false) { 540 + sidebarHost.style.display = "none"; 541 + } else { 542 + sidebarHost.style.display = ""; 543 + fetchAnnotations(); 544 + } 545 + } 546 + } 547 + }); 472 548 } 473 549 474 550 function showInlineComposeModal() {
+105
extension/popup/popup.css
··· 29 29 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 30 30 } 31 31 32 + @media (prefers-color-scheme: light) { 33 + :root { 34 + --bg-primary: #ffffff; 35 + --bg-secondary: #f4f4f5; 36 + --bg-tertiary: #e4e4e7; 37 + --bg-card: #ffffff; 38 + --bg-elevated: #f4f4f5; 39 + --bg-hover: #e4e4e7; 40 + 41 + --text-primary: #18181b; 42 + --text-secondary: #52525b; 43 + --text-tertiary: #71717a; 44 + --border: #e4e4e7; 45 + --border-hover: #d4d4d8; 46 + 47 + --accent: #4f46e5; 48 + --accent-hover: #4338ca; 49 + --accent-text: #4f46e5; 50 + --accent-subtle: rgba(79, 70, 229, 0.1); 51 + 52 + --success: #059669; 53 + --error: #dc2626; 54 + --warning: #d97706; 55 + } 56 + } 57 + 58 + body.light { 59 + --bg-primary: #ffffff; 60 + --bg-secondary: #f4f4f5; 61 + --bg-tertiary: #e4e4e7; 62 + --bg-card: #ffffff; 63 + --bg-elevated: #f4f4f5; 64 + --bg-hover: #e4e4e7; 65 + 66 + --text-primary: #18181b; 67 + --text-secondary: #52525b; 68 + --text-tertiary: #71717a; 69 + --border: #e4e4e7; 70 + --border-hover: #d4d4d8; 71 + 72 + --accent: #4f46e5; 73 + --accent-hover: #4338ca; 74 + --accent-text: #4f46e5; 75 + --accent-subtle: rgba(79, 70, 229, 0.1); 76 + 77 + --success: #059669; 78 + --error: #dc2626; 79 + --warning: #d97706; 80 + } 81 + 82 + body.dark { 83 + --bg-primary: #09090b; 84 + --bg-secondary: #0f0f12; 85 + --bg-tertiary: #18181b; 86 + --bg-card: #09090b; 87 + --bg-elevated: #18181b; 88 + --bg-hover: #27272a; 89 + 90 + --text-primary: #e4e4e7; 91 + --text-secondary: #a1a1aa; 92 + --text-tertiary: #71717a; 93 + --border: #27272a; 94 + --border-hover: #3f3f46; 95 + 96 + --accent: #6366f1; 97 + --accent-hover: #4f46e5; 98 + --accent-subtle: rgba(99, 102, 241, 0.1); 99 + --accent-text: #818cf8; 100 + --success: #10b981; 101 + --error: #ef4444; 102 + --warning: #f59e0b; 103 + } 104 + 32 105 * { 33 106 box-sizing: border-box; 34 107 margin: 0; ··· 710 783 outline: none; 711 784 border-color: var(--accent); 712 785 } 786 + .theme-toggle-group { 787 + display: flex; 788 + background: var(--bg-tertiary); 789 + padding: 4px; 790 + border-radius: var(--radius-md); 791 + gap: 2px; 792 + margin-top: 8px; 793 + } 794 + 795 + .theme-btn { 796 + flex: 1; 797 + padding: 6px; 798 + border: none; 799 + background: transparent; 800 + color: var(--text-secondary); 801 + font-size: 12px; 802 + font-weight: 500; 803 + border-radius: var(--radius-sm); 804 + cursor: pointer; 805 + transition: all 0.15s ease; 806 + } 807 + 808 + .theme-btn:hover { 809 + color: var(--text-primary); 810 + background: rgba(128, 128, 128, 0.1); 811 + } 812 + 813 + .theme-btn.active { 814 + background: var(--bg-card); 815 + color: var(--text-primary); 816 + box-shadow: var(--shadow-sm); 817 + }
+8
extension/popup/popup.html
··· 247 247 /> 248 248 <p class="setting-help">Enter your backend URL</p> 249 249 </div> 250 + <div class="setting-item"> 251 + <label for="theme-select">Theme</label> 252 + <div class="theme-toggle-group"> 253 + <button class="theme-btn active" data-theme="system">Auto</button> 254 + <button class="theme-btn" data-theme="light">Light</button> 255 + <button class="theme-btn" data-theme="dark">Dark</button> 256 + </div> 257 + </div> 250 258 <button id="save-settings" class="btn btn-primary" style="width: 100%"> 251 259 Save 252 260 </button>
+36 -1
extension/popup/popup.js
··· 40 40 collectionLoading: document.getElementById("collection-loading"), 41 41 collectionsEmpty: document.getElementById("collections-empty"), 42 42 overlayToggle: document.getElementById("overlay-toggle"), 43 + themeBtns: document.querySelectorAll(".theme-btn"), 43 44 }; 44 45 45 46 let currentTab = null; ··· 48 49 let pendingSelector = null; 49 50 // let _activeAnnotationUriForCollection = null; 50 51 51 - const storage = await browserAPI.storage.local.get(["apiUrl", "showOverlay"]); 52 + const storage = await browserAPI.storage.local.get([ 53 + "apiUrl", 54 + "showOverlay", 55 + "theme", 56 + ]); 52 57 if (storage.apiUrl) { 53 58 apiUrl = storage.apiUrl; 54 59 } ··· 57 62 if (els.overlayToggle) { 58 63 els.overlayToggle.checked = storage.showOverlay !== false; 59 64 } 65 + 66 + const currentTheme = storage.theme || "system"; 67 + applyTheme(currentTheme); 68 + updateThemeUI(currentTheme); 60 69 61 70 try { 62 71 const [tab] = await browserAPI.tabs.query({ ··· 240 249 241 250 views.settings.style.display = "none"; 242 251 checkSession(); 252 + }); 253 + 254 + els.themeBtns.forEach((btn) => { 255 + btn.addEventListener("click", async () => { 256 + const theme = btn.getAttribute("data-theme"); 257 + await browserAPI.storage.local.set({ theme }); 258 + applyTheme(theme); 259 + updateThemeUI(theme); 260 + }); 243 261 }); 244 262 245 263 els.closeCollectionSelector?.addEventListener("click", () => { ··· 781 799 }); 782 800 } 783 801 }); 802 + 803 + function applyTheme(theme) { 804 + document.body.classList.remove("light", "dark"); 805 + if (theme === "system") return; 806 + document.body.classList.add(theme); 807 + } 808 + 809 + function updateThemeUI(theme) { 810 + const btns = document.querySelectorAll(".theme-btn"); 811 + btns.forEach((btn) => { 812 + if (btn.getAttribute("data-theme") === theme) { 813 + btn.classList.add("active"); 814 + } else { 815 + btn.classList.remove("active"); 816 + } 817 + }); 818 + }
+109
extension/sidepanel/sidepanel.css
··· 31 31 --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 32 32 } 33 33 34 + @media (prefers-color-scheme: light) { 35 + :root { 36 + --bg-primary: #ffffff; 37 + --bg-secondary: #f4f4f5; 38 + --bg-tertiary: #e4e4e7; 39 + --bg-card: #ffffff; 40 + --bg-hover: #e4e4e7; 41 + --bg-elevated: #f4f4f5; 42 + 43 + --text-primary: #18181b; 44 + --text-secondary: #52525b; 45 + --text-tertiary: #71717a; 46 + 47 + --accent: #4f46e5; 48 + --accent-hover: #4338ca; 49 + --accent-subtle: rgba(79, 70, 229, 0.1); 50 + --accent-text: #4f46e5; 51 + 52 + --border: #e4e4e7; 53 + --border-hover: #d4d4d8; 54 + 55 + --success: #059669; 56 + --error: #dc2626; 57 + --warning: #d97706; 58 + } 59 + } 60 + 61 + body.light { 62 + --bg-primary: #ffffff; 63 + --bg-secondary: #f4f4f5; 64 + --bg-tertiary: #e4e4e7; 65 + --bg-card: #ffffff; 66 + --bg-hover: #e4e4e7; 67 + --bg-elevated: #f4f4f5; 68 + 69 + --text-primary: #18181b; 70 + --text-secondary: #52525b; 71 + --text-tertiary: #71717a; 72 + 73 + --accent: #4f46e5; 74 + --accent-hover: #4338ca; 75 + --accent-subtle: rgba(79, 70, 229, 0.1); 76 + --accent-text: #4f46e5; 77 + 78 + --border: #e4e4e7; 79 + --border-hover: #d4d4d8; 80 + 81 + --success: #059669; 82 + --error: #dc2626; 83 + --warning: #d97706; 84 + } 85 + 86 + body.dark { 87 + --bg-primary: #09090b; 88 + --bg-secondary: #0f0f12; 89 + --bg-tertiary: #18181b; 90 + --bg-card: #09090b; 91 + --bg-hover: #18181b; 92 + --bg-elevated: #18181b; 93 + 94 + --text-primary: #e4e4e7; 95 + --text-secondary: #a1a1aa; 96 + --text-tertiary: #71717a; 97 + 98 + --accent: #6366f1; 99 + --accent-hover: #4f46e5; 100 + --accent-subtle: rgba(99, 102, 241, 0.1); 101 + --accent-text: #818cf8; 102 + 103 + --border: #27272a; 104 + --border-hover: #3f3f46; 105 + 106 + --success: #10b981; 107 + --error: #ef4444; 108 + --warning: #f59e0b; 109 + } 110 + 34 111 * { 35 112 margin: 0; 36 113 padding: 0; ··· 929 1006 transform: translateX(20px); 930 1007 background-color: white; 931 1008 } 1009 + .theme-toggle-group { 1010 + display: flex; 1011 + background: var(--bg-tertiary); 1012 + padding: 4px; 1013 + border-radius: var(--radius-md); 1014 + gap: 2px; 1015 + margin-top: 8px; 1016 + } 1017 + 1018 + .theme-btn { 1019 + flex: 1; 1020 + padding: 6px; 1021 + border: none; 1022 + background: transparent; 1023 + color: var(--text-secondary); 1024 + font-size: 12px; 1025 + font-weight: 500; 1026 + border-radius: var(--radius-sm); 1027 + cursor: pointer; 1028 + transition: all 0.15s ease; 1029 + } 1030 + 1031 + .theme-btn:hover { 1032 + color: var(--text-primary); 1033 + background: rgba(128, 128, 128, 0.1); 1034 + } 1035 + 1036 + .theme-btn.active { 1037 + background: var(--bg-card); 1038 + color: var(--text-primary); 1039 + box-shadow: var(--shadow-sm); 1040 + }
+8
extension/sidepanel/sidepanel.html
··· 279 279 /> 280 280 <p class="setting-help">Enter your Margin backend URL</p> 281 281 </div> 282 + <div class="setting-item"> 283 + <label for="theme-select">Theme</label> 284 + <div class="theme-toggle-group"> 285 + <button class="theme-btn active" data-theme="system">Auto</button> 286 + <button class="theme-btn" data-theme="light">Light</button> 287 + <button class="theme-btn" data-theme="dark">Dark</button> 288 + </div> 289 + </div> 282 290 <button id="save-settings" class="btn btn-primary" style="width: 100%"> 283 291 Save 284 292 </button>
+48 -6
extension/sidepanel/sidepanel.js
··· 58 58 } 59 59 60 60 chrome.storage.onChanged.addListener((changes, area) => { 61 - if (area === "local" && changes.apiUrl) { 62 - apiUrl = changes.apiUrl.newValue || ""; 63 - 64 - els.apiUrlInput.value = apiUrl; 65 - checkSession(); 61 + if (area === "local") { 62 + if (changes.apiUrl) { 63 + apiUrl = changes.apiUrl.newValue || ""; 64 + els.apiUrlInput.value = apiUrl; 65 + checkSession(); 66 + } 67 + if (changes.theme) { 68 + const newTheme = changes.theme.newValue || "system"; 69 + applyTheme(newTheme); 70 + updateThemeUI(newTheme); 71 + } 66 72 } 73 + }); 74 + 75 + chrome.storage.local.get(["theme"], (result) => { 76 + const currentTheme = result.theme || "system"; 77 + applyTheme(currentTheme); 78 + updateThemeUI(currentTheme); 79 + }); 80 + 81 + const themeBtns = document.querySelectorAll(".theme-btn"); 82 + themeBtns.forEach((btn) => { 83 + btn.addEventListener("click", () => { 84 + const theme = btn.getAttribute("data-theme"); 85 + chrome.storage.local.set({ theme }); 86 + applyTheme(theme); 87 + updateThemeUI(theme); 88 + }); 67 89 }); 68 90 69 91 try { ··· 264 286 const newUrl = els.apiUrlInput.value.replace(/\/$/, ""); 265 287 const showOverlay = els.overlayToggle?.checked ?? true; 266 288 267 - await chrome.storage.local.set({ apiUrl: newUrl, showOverlay }); 289 + await chrome.storage.local.set({ 290 + apiUrl: newUrl, 291 + showOverlay, 292 + }); 268 293 if (newUrl) { 269 294 apiUrl = newUrl; 270 295 } ··· 909 934 resolve(response); 910 935 } 911 936 }); 937 + }); 938 + } 939 + 940 + function applyTheme(theme) { 941 + document.body.classList.remove("light", "dark"); 942 + if (theme === "system") return; 943 + document.body.classList.add(theme); 944 + } 945 + 946 + function updateThemeUI(theme) { 947 + const btns = document.querySelectorAll(".theme-btn"); 948 + btns.forEach((btn) => { 949 + if (btn.getAttribute("data-theme") === theme) { 950 + btn.classList.add("active"); 951 + } else { 952 + btn.classList.remove("active"); 953 + } 912 954 }); 913 955 } 914 956 });
+8 -5
web/src/App.jsx
··· 18 18 import Privacy from "./pages/Privacy"; 19 19 import Terms from "./pages/Terms"; 20 20 import ScrollToTop from "./components/ScrollToTop"; 21 + import { ThemeProvider } from "./context/ThemeContext"; 21 22 22 23 function AppContent() { 23 24 const { user } = useAuth(); ··· 77 78 78 79 export default function App() { 79 80 return ( 80 - <AuthProvider> 81 - <Routes> 82 - <Route path="/*" element={<AppContent />} /> 83 - </Routes> 84 - </AuthProvider> 81 + <ThemeProvider> 82 + <AuthProvider> 83 + <Routes> 84 + <Route path="/*" element={<AppContent />} /> 85 + </Routes> 86 + </AuthProvider> 87 + </ThemeProvider> 85 88 ); 86 89 }
+25 -4
web/src/components/RightSidebar.jsx
··· 1 1 import { useState, useEffect } from "react"; 2 2 import { Link } from "react-router-dom"; 3 - import { ExternalLink } from "lucide-react"; 3 + import { ExternalLink, Sun, Moon, Monitor } from "lucide-react"; 4 4 import { 5 5 SiFirefox, 6 6 SiGooglechrome, ··· 12 12 } from "react-icons/si"; 13 13 import { FaEdge } from "react-icons/fa"; 14 14 import { useAuth } from "../context/AuthContext"; 15 + import { useTheme } from "../context/ThemeContext"; 15 16 import { getTrendingTags } from "../api/client"; 16 17 17 18 const isFirefox = ··· 58 59 } 59 60 60 61 export default function RightSidebar() { 62 + const { theme, setTheme } = useTheme(); 61 63 const { isAuthenticated } = useAuth(); 62 64 const ext = getExtensionInfo(); 63 65 const ExtIcon = ext.icon; ··· 196 198 </div> 197 199 198 200 <div className="right-footer"> 199 - <Link to="/privacy">Privacy</Link> 200 - <span>·</span> 201 - <Link to="/terms">Terms</Link> 201 + <div className="footer-links"> 202 + <Link to="/privacy">Privacy</Link> 203 + <span>·</span> 204 + <Link to="/terms">Terms</Link> 205 + </div> 206 + <button 207 + onClick={() => { 208 + const next = 209 + theme === "system" 210 + ? "light" 211 + : theme === "light" 212 + ? "dark" 213 + : "system"; 214 + setTheme(next); 215 + }} 216 + className="theme-toggle-mini" 217 + title={`Theme: ${theme}`} 218 + > 219 + {theme === "system" && <Monitor size={14} />} 220 + {theme === "light" && <Sun size={14} />} 221 + {theme === "dark" && <Moon size={14} />} 222 + </button> 202 223 </div> 203 224 </aside> 204 225 );
+75
web/src/context/ThemeContext.jsx
··· 1 + import { createContext, useContext, useEffect, useState } from "react"; 2 + 3 + const ThemeContext = createContext({ 4 + theme: "system", 5 + setTheme: () => null, 6 + }); 7 + 8 + export function ThemeProvider({ children }) { 9 + const [theme, setTheme] = useState(() => { 10 + return localStorage.getItem("theme") || "system"; 11 + }); 12 + 13 + useEffect(() => { 14 + localStorage.setItem("theme", theme); 15 + 16 + const root = window.document.documentElement; 17 + root.classList.remove("light", "dark"); 18 + 19 + delete root.dataset.theme; 20 + 21 + if (theme === "system") { 22 + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") 23 + .matches 24 + ? "dark" 25 + : "light"; 26 + 27 + if (systemTheme === "light") { 28 + root.dataset.theme = "light"; 29 + } else { 30 + root.dataset.theme = "dark"; 31 + } 32 + return; 33 + } 34 + 35 + if (theme === "light") { 36 + root.dataset.theme = "light"; 37 + } 38 + }, [theme]); 39 + 40 + useEffect(() => { 41 + if (theme !== "system") return; 42 + 43 + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); 44 + const handleChange = () => { 45 + const root = window.document.documentElement; 46 + if (mediaQuery.matches) { 47 + delete root.dataset.theme; 48 + } else { 49 + root.dataset.theme = "light"; 50 + } 51 + }; 52 + 53 + mediaQuery.addEventListener("change", handleChange); 54 + return () => mediaQuery.removeEventListener("change", handleChange); 55 + }, [theme]); 56 + 57 + const value = { 58 + theme, 59 + setTheme: (newTheme) => { 60 + setTheme(newTheme); 61 + }, 62 + }; 63 + 64 + return ( 65 + <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider> 66 + ); 67 + } 68 + 69 + // eslint-disable-next-line react-refresh/only-export-components 70 + export function useTheme() { 71 + const context = useContext(ThemeContext); 72 + if (context === undefined) 73 + throw new Error("useTheme must be used within a ThemeProvider"); 74 + return context; 75 + }
+24
web/src/css/base.css
··· 32 32 "JetBrains Mono", source-code-pro, Menlo, Monaco, Consolas, monospace; 33 33 } 34 34 35 + [data-theme="light"] { 36 + --bg-primary: #ffffff; 37 + --bg-secondary: #f4f4f5; 38 + --bg-tertiary: #e4e4e7; 39 + --bg-card: #ffffff; 40 + --bg-elevated: #f4f4f5; 41 + --text-primary: #18181b; 42 + --text-secondary: #52525b; 43 + --text-tertiary: #71717a; 44 + --border: #e4e4e7; 45 + --border-hover: #d4d4d8; 46 + --accent: #4f46e5; 47 + --accent-hover: #4338ca; 48 + --accent-subtle: rgba(79, 70, 229, 0.1); 49 + --accent-text: #4f46e5; 50 + --success: #059669; 51 + --error: #dc2626; 52 + --warning: #d97706; 53 + --info: #2563eb; 54 + --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05); 55 + --shadow-md: 56 + 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 57 + } 58 + 35 59 * { 36 60 margin: 0; 37 61 padding: 0;
+33 -5
web/src/css/layout.css
··· 359 359 .right-footer { 360 360 margin-top: auto; 361 361 display: flex; 362 - flex-wrap: wrap; 363 - gap: 12px; 364 - font-size: 0.75rem; 362 + align-items: center; 363 + justify-content: space-between; 364 + padding-top: 16px; 365 + border-top: 1px solid var(--border); 366 + } 367 + 368 + .footer-links { 369 + display: flex; 370 + align-items: center; 371 + gap: 8px; 372 + font-size: 12px; 365 373 color: var(--text-tertiary); 366 374 } 367 375 368 - .right-footer a { 376 + .footer-links a { 369 377 color: var(--text-tertiary); 378 + text-decoration: none; 370 379 } 371 380 372 - .right-footer a:hover { 381 + .footer-links a:hover { 382 + text-decoration: underline; 373 383 color: var(--text-secondary); 384 + } 385 + 386 + .theme-toggle-mini { 387 + background: none; 388 + border: none; 389 + cursor: pointer; 390 + padding: 4px; 391 + color: var(--text-tertiary); 392 + display: flex; 393 + align-items: center; 394 + justify-content: center; 395 + border-radius: 4px; 396 + transition: all 0.2s; 397 + } 398 + 399 + .theme-toggle-mini:hover { 400 + color: var(--text-primary); 401 + background: var(--bg-hover); 374 402 } 375 403 376 404 .mobile-nav {