(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
98
fork

Configure Feed

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

Implement Light Mode

scanash00 21cb43f0 300af0f8

+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 {