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 let currentSelection = null; 8 9 const OVERLAY_STYLES = ` 10 - :host { all: initial; } 11 .margin-overlay { 12 position: absolute; 13 top: 0; ··· 20 .margin-popover { 21 position: absolute; 22 width: 320px; 23 - background: #09090b; 24 - border: 1px solid #27272a; 25 border-radius: 12px; 26 padding: 0; 27 box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.2); ··· 30 pointer-events: auto; 31 z-index: 2147483647; 32 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 33 - color: #e4e4e7; 34 opacity: 0; 35 transform: scale(0.95); 36 animation: popover-in 0.15s forwards; ··· 40 @keyframes popover-in { to { opacity: 1; transform: scale(1); } } 41 .popover-header { 42 padding: 12px 16px; 43 - border-bottom: 1px solid #27272a; 44 display: flex; 45 justify-content: space-between; 46 align-items: center; 47 - background: #0f0f12; 48 border-radius: 12px 12px 0 0; 49 font-weight: 600; 50 font-size: 13px; 51 } 52 .popover-scroll-area { 53 overflow-y: auto; 54 max-height: 400px; 55 } 56 .popover-item-block { 57 - border-bottom: 1px solid #27272a; 58 margin-bottom: 0; 59 animation: fade-in 0.2s; 60 } ··· 68 gap: 8px; 69 } 70 .popover-avatar { 71 - width: 24px; height: 24px; border-radius: 50%; background: #27272a; 72 display: flex; align-items: center; justify-content: center; 73 - font-size: 10px; color: #a1a1aa; 74 } 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; } 79 .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; 83 } 84 .popover-actions { 85 padding: 8px 16px; 86 display: flex; justify-content: flex-end; gap: 8px; 87 } 88 .btn-action { 89 - background: none; border: 1px solid #27272a; border-radius: 4px; 90 - padding: 4px 8px; color: #a1a1aa; font-size: 11px; cursor: pointer; 91 } 92 - .btn-action:hover { background: #27272a; color: #e4e4e7; } 93 94 .margin-selection-popup { 95 position: fixed; 96 display: flex; 97 gap: 4px; 98 padding: 6px; 99 - background: #09090b; 100 - border: 1px solid #27272a; 101 border-radius: 8px; 102 box-shadow: 0 8px 16px rgba(0,0,0,0.4); 103 z-index: 2147483647; ··· 113 background: transparent; 114 border: none; 115 border-radius: 6px; 116 - color: #e4e4e7; 117 font-size: 12px; 118 font-weight: 500; 119 cursor: pointer; 120 transition: background 0.15s; 121 } 122 .selection-btn:hover { 123 - background: #27272a; 124 } 125 .selection-btn svg { 126 width: 14px; ··· 130 position: fixed; 131 width: 340px; 132 max-width: calc(100vw - 40px); 133 - background: #09090b; 134 - border: 1px solid #27272a; 135 border-radius: 12px; 136 padding: 16px; 137 box-sizing: border-box; ··· 139 z-index: 2147483647; 140 pointer-events: auto; 141 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 142 - color: #e4e4e7; 143 animation: popover-in 0.15s forwards; 144 overflow: hidden; 145 } ··· 148 } 149 .inline-compose-quote { 150 padding: 8px 12px; 151 - background: #18181b; 152 - border-left: 3px solid #6366f1; 153 border-radius: 4px; 154 font-size: 12px; 155 - color: #a1a1aa; 156 font-style: italic; 157 margin-bottom: 12px; 158 max-height: 60px; ··· 163 width: 100%; 164 min-height: 80px; 165 padding: 10px 12px; 166 - background: #18181b; 167 - border: 1px solid #27272a; 168 border-radius: 8px; 169 - color: #e4e4e7; 170 font-family: inherit; 171 font-size: 13px; 172 resize: vertical; ··· 175 } 176 .inline-compose-textarea:focus { 177 outline: none; 178 - border-color: #6366f1; 179 } 180 .inline-compose-actions { 181 display: flex; ··· 185 .btn-cancel { 186 padding: 8px 16px; 187 background: transparent; 188 - border: 1px solid #27272a; 189 border-radius: 6px; 190 - color: #a1a1aa; 191 font-size: 13px; 192 cursor: pointer; 193 } 194 .btn-cancel:hover { 195 - background: #27272a; 196 - color: #e4e4e7; 197 } 198 .btn-submit { 199 padding: 8px 16px; 200 - background: #6366f1; 201 border: none; 202 border-radius: 6px; 203 color: white; ··· 206 cursor: pointer; 207 } 208 .btn-submit:hover { 209 - background: #4f46e5; 210 } 211 .btn-submit:disabled { 212 opacity: 0.5; 213 cursor: not-allowed; 214 } 215 .reply-section { 216 - border-top: 1px solid #27272a; 217 padding: 12px 16px; 218 - background: #0f0f12; 219 border-radius: 0 0 12px 12px; 220 } 221 .reply-textarea { 222 width: 100%; 223 min-height: 60px; 224 padding: 8px 10px; 225 - background: #18181b; 226 - border: 1px solid #27272a; 227 border-radius: 6px; 228 - color: #e4e4e7; 229 font-family: inherit; 230 font-size: 12px; 231 resize: none; ··· 233 } 234 .reply-textarea:focus { 235 outline: none; 236 - border-color: #6366f1; 237 } 238 .reply-submit { 239 padding: 6px 12px; 240 - background: #6366f1; 241 border: none; 242 border-radius: 4px; 243 color: white; ··· 251 } 252 .reply-item { 253 padding: 8px 0; 254 - border-top: 1px solid #27272a; 255 } 256 .reply-item:first-child { 257 border-top: none; ··· 259 .reply-author { 260 font-size: 11px; 261 font-weight: 600; 262 - color: #a1a1aa; 263 margin-bottom: 4px; 264 } 265 .reply-text { 266 font-size: 12px; 267 - color: #e4e4e7; 268 line-height: 1.4; 269 } 270 `; ··· 422 } 423 } 424 425 function initOverlay() { 426 sidebarHost = document.createElement("div"); 427 sidebarHost.id = "margin-overlay-host"; ··· 456 if (document.documentElement) observer.observe(document.documentElement); 457 458 if (typeof chrome !== "undefined" && chrome.storage) { 459 - chrome.storage.local.get(["showOverlay"], (result) => { 460 if (result.showOverlay === false) { 461 sidebarHost.style.display = "none"; 462 } else { ··· 469 470 document.addEventListener("mousemove", handleMouseMove); 471 document.addEventListener("click", handleDocumentClick, true); 472 } 473 474 function showInlineComposeModal() {
··· 7 let currentSelection = null; 8 9 const OVERLAY_STYLES = ` 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 + 43 .margin-overlay { 44 position: absolute; 45 top: 0; ··· 52 .margin-popover { 53 position: absolute; 54 width: 320px; 55 + background: var(--bg-card); 56 + border: 1px solid var(--border); 57 border-radius: 12px; 58 padding: 0; 59 box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 10px 10px -5px rgba(0, 0, 0, 0.2); ··· 62 pointer-events: auto; 63 z-index: 2147483647; 64 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 65 + color: var(--text-primary); 66 opacity: 0; 67 transform: scale(0.95); 68 animation: popover-in 0.15s forwards; ··· 72 @keyframes popover-in { to { opacity: 1; transform: scale(1); } } 73 .popover-header { 74 padding: 12px 16px; 75 + border-bottom: 1px solid var(--border); 76 display: flex; 77 justify-content: space-between; 78 align-items: center; 79 + background: var(--bg-secondary); 80 border-radius: 12px 12px 0 0; 81 font-weight: 600; 82 font-size: 13px; 83 + color: var(--text-primary); 84 } 85 .popover-scroll-area { 86 overflow-y: auto; 87 max-height: 400px; 88 } 89 .popover-item-block { 90 + border-bottom: 1px solid var(--border); 91 margin-bottom: 0; 92 animation: fade-in 0.2s; 93 } ··· 101 gap: 8px; 102 } 103 .popover-avatar { 104 + width: 24px; height: 24px; border-radius: 50%; background: var(--bg-hover); 105 display: flex; align-items: center; justify-content: center; 106 + font-size: 10px; color: var(--text-secondary); 107 } 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); } 112 .popover-quote { 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; 116 } 117 .popover-actions { 118 padding: 8px 16px; 119 display: flex; justify-content: flex-end; gap: 8px; 120 } 121 .btn-action { 122 + background: none; border: 1px solid var(--border); border-radius: 4px; 123 + padding: 4px 8px; color: var(--text-secondary); font-size: 11px; cursor: pointer; 124 } 125 + .btn-action:hover { background: var(--bg-hover); color: var(--text-primary); } 126 127 .margin-selection-popup { 128 position: fixed; 129 display: flex; 130 gap: 4px; 131 padding: 6px; 132 + background: var(--bg-card); 133 + border: 1px solid var(--border); 134 border-radius: 8px; 135 box-shadow: 0 8px 16px rgba(0,0,0,0.4); 136 z-index: 2147483647; ··· 146 background: transparent; 147 border: none; 148 border-radius: 6px; 149 + color: var(--text-primary); 150 font-size: 12px; 151 font-weight: 500; 152 cursor: pointer; 153 transition: background 0.15s; 154 } 155 .selection-btn:hover { 156 + background: var(--bg-hover); 157 } 158 .selection-btn svg { 159 width: 14px; ··· 163 position: fixed; 164 width: 340px; 165 max-width: calc(100vw - 40px); 166 + background: var(--bg-card); 167 + border: 1px solid var(--border); 168 border-radius: 12px; 169 padding: 16px; 170 box-sizing: border-box; ··· 172 z-index: 2147483647; 173 pointer-events: auto; 174 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; 175 + color: var(--text-primary); 176 animation: popover-in 0.15s forwards; 177 overflow: hidden; 178 } ··· 181 } 182 .inline-compose-quote { 183 padding: 8px 12px; 184 + background: var(--bg-tertiary); 185 + border-left: 3px solid var(--accent); 186 border-radius: 4px; 187 font-size: 12px; 188 + color: var(--text-secondary); 189 font-style: italic; 190 margin-bottom: 12px; 191 max-height: 60px; ··· 196 width: 100%; 197 min-height: 80px; 198 padding: 10px 12px; 199 + background: var(--bg-elevated); 200 + border: 1px solid var(--border); 201 border-radius: 8px; 202 + color: var(--text-primary); 203 font-family: inherit; 204 font-size: 13px; 205 resize: vertical; ··· 208 } 209 .inline-compose-textarea:focus { 210 outline: none; 211 + border-color: var(--accent); 212 } 213 .inline-compose-actions { 214 display: flex; ··· 218 .btn-cancel { 219 padding: 8px 16px; 220 background: transparent; 221 + border: 1px solid var(--border); 222 border-radius: 6px; 223 + color: var(--text-secondary); 224 font-size: 13px; 225 cursor: pointer; 226 } 227 .btn-cancel:hover { 228 + background: var(--bg-hover); 229 + color: var(--text-primary); 230 } 231 .btn-submit { 232 padding: 8px 16px; 233 + background: var(--accent); 234 border: none; 235 border-radius: 6px; 236 color: white; ··· 239 cursor: pointer; 240 } 241 .btn-submit:hover { 242 + background: var(--accent-hover); 243 } 244 .btn-submit:disabled { 245 opacity: 0.5; 246 cursor: not-allowed; 247 } 248 .reply-section { 249 + border-top: 1px solid var(--border); 250 padding: 12px 16px; 251 + background: var(--bg-secondary); 252 border-radius: 0 0 12px 12px; 253 } 254 .reply-textarea { 255 width: 100%; 256 min-height: 60px; 257 padding: 8px 10px; 258 + background: var(--bg-elevated); 259 + border: 1px solid var(--border); 260 border-radius: 6px; 261 + color: var(--text-primary); 262 font-family: inherit; 263 font-size: 12px; 264 resize: none; ··· 266 } 267 .reply-textarea:focus { 268 outline: none; 269 + border-color: var(--accent); 270 } 271 .reply-submit { 272 padding: 6px 12px; 273 + background: var(--accent); 274 border: none; 275 border-radius: 4px; 276 color: white; ··· 284 } 285 .reply-item { 286 padding: 8px 0; 287 + border-top: 1px solid var(--border); 288 } 289 .reply-item:first-child { 290 border-top: none; ··· 292 .reply-author { 293 font-size: 11px; 294 font-weight: 600; 295 + color: var(--text-secondary); 296 margin-bottom: 4px; 297 } 298 .reply-text { 299 font-size: 12px; 300 + color: var(--text-primary); 301 line-height: 1.4; 302 } 303 `; ··· 455 } 456 } 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 + 484 function initOverlay() { 485 sidebarHost = document.createElement("div"); 486 sidebarHost.id = "margin-overlay-host"; ··· 515 if (document.documentElement) observer.observe(document.documentElement); 516 517 if (typeof chrome !== "undefined" && chrome.storage) { 518 + chrome.storage.local.get(["showOverlay", "theme"], (result) => { 519 + applyTheme(result.theme); 520 if (result.showOverlay === false) { 521 sidebarHost.style.display = "none"; 522 } else { ··· 529 530 document.addEventListener("mousemove", handleMouseMove); 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 + }); 548 } 549 550 function showInlineComposeModal() {
+105
extension/popup/popup.css
··· 29 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 30 } 31 32 * { 33 box-sizing: border-box; 34 margin: 0; ··· 710 outline: none; 711 border-color: var(--accent); 712 }
··· 29 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); 30 } 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 + 105 * { 106 box-sizing: border-box; 107 margin: 0; ··· 783 outline: none; 784 border-color: var(--accent); 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 /> 248 <p class="setting-help">Enter your backend URL</p> 249 </div> 250 <button id="save-settings" class="btn btn-primary" style="width: 100%"> 251 Save 252 </button>
··· 247 /> 248 <p class="setting-help">Enter your backend URL</p> 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> 258 <button id="save-settings" class="btn btn-primary" style="width: 100%"> 259 Save 260 </button>
+36 -1
extension/popup/popup.js
··· 40 collectionLoading: document.getElementById("collection-loading"), 41 collectionsEmpty: document.getElementById("collections-empty"), 42 overlayToggle: document.getElementById("overlay-toggle"), 43 }; 44 45 let currentTab = null; ··· 48 let pendingSelector = null; 49 // let _activeAnnotationUriForCollection = null; 50 51 - const storage = await browserAPI.storage.local.get(["apiUrl", "showOverlay"]); 52 if (storage.apiUrl) { 53 apiUrl = storage.apiUrl; 54 } ··· 57 if (els.overlayToggle) { 58 els.overlayToggle.checked = storage.showOverlay !== false; 59 } 60 61 try { 62 const [tab] = await browserAPI.tabs.query({ ··· 240 241 views.settings.style.display = "none"; 242 checkSession(); 243 }); 244 245 els.closeCollectionSelector?.addEventListener("click", () => { ··· 781 }); 782 } 783 });
··· 40 collectionLoading: document.getElementById("collection-loading"), 41 collectionsEmpty: document.getElementById("collections-empty"), 42 overlayToggle: document.getElementById("overlay-toggle"), 43 + themeBtns: document.querySelectorAll(".theme-btn"), 44 }; 45 46 let currentTab = null; ··· 49 let pendingSelector = null; 50 // let _activeAnnotationUriForCollection = null; 51 52 + const storage = await browserAPI.storage.local.get([ 53 + "apiUrl", 54 + "showOverlay", 55 + "theme", 56 + ]); 57 if (storage.apiUrl) { 58 apiUrl = storage.apiUrl; 59 } ··· 62 if (els.overlayToggle) { 63 els.overlayToggle.checked = storage.showOverlay !== false; 64 } 65 + 66 + const currentTheme = storage.theme || "system"; 67 + applyTheme(currentTheme); 68 + updateThemeUI(currentTheme); 69 70 try { 71 const [tab] = await browserAPI.tabs.query({ ··· 249 250 views.settings.style.display = "none"; 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 + }); 261 }); 262 263 els.closeCollectionSelector?.addEventListener("click", () => { ··· 799 }); 800 } 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 --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 32 } 33 34 * { 35 margin: 0; 36 padding: 0; ··· 929 transform: translateX(20px); 930 background-color: white; 931 }
··· 31 --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1); 32 } 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 + 111 * { 112 margin: 0; 113 padding: 0; ··· 1006 transform: translateX(20px); 1007 background-color: white; 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 /> 280 <p class="setting-help">Enter your Margin backend URL</p> 281 </div> 282 <button id="save-settings" class="btn btn-primary" style="width: 100%"> 283 Save 284 </button>
··· 279 /> 280 <p class="setting-help">Enter your Margin backend URL</p> 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> 290 <button id="save-settings" class="btn btn-primary" style="width: 100%"> 291 Save 292 </button>
+48 -6
extension/sidepanel/sidepanel.js
··· 58 } 59 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(); 66 } 67 }); 68 69 try { ··· 264 const newUrl = els.apiUrlInput.value.replace(/\/$/, ""); 265 const showOverlay = els.overlayToggle?.checked ?? true; 266 267 - await chrome.storage.local.set({ apiUrl: newUrl, showOverlay }); 268 if (newUrl) { 269 apiUrl = newUrl; 270 } ··· 909 resolve(response); 910 } 911 }); 912 }); 913 } 914 });
··· 58 } 59 60 chrome.storage.onChanged.addListener((changes, area) => { 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 + } 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 + }); 89 }); 90 91 try { ··· 286 const newUrl = els.apiUrlInput.value.replace(/\/$/, ""); 287 const showOverlay = els.overlayToggle?.checked ?? true; 288 289 + await chrome.storage.local.set({ 290 + apiUrl: newUrl, 291 + showOverlay, 292 + }); 293 if (newUrl) { 294 apiUrl = newUrl; 295 } ··· 934 resolve(response); 935 } 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 + } 954 }); 955 } 956 });
+8 -5
web/src/App.jsx
··· 18 import Privacy from "./pages/Privacy"; 19 import Terms from "./pages/Terms"; 20 import ScrollToTop from "./components/ScrollToTop"; 21 22 function AppContent() { 23 const { user } = useAuth(); ··· 77 78 export default function App() { 79 return ( 80 - <AuthProvider> 81 - <Routes> 82 - <Route path="/*" element={<AppContent />} /> 83 - </Routes> 84 - </AuthProvider> 85 ); 86 }
··· 18 import Privacy from "./pages/Privacy"; 19 import Terms from "./pages/Terms"; 20 import ScrollToTop from "./components/ScrollToTop"; 21 + import { ThemeProvider } from "./context/ThemeContext"; 22 23 function AppContent() { 24 const { user } = useAuth(); ··· 78 79 export default function App() { 80 return ( 81 + <ThemeProvider> 82 + <AuthProvider> 83 + <Routes> 84 + <Route path="/*" element={<AppContent />} /> 85 + </Routes> 86 + </AuthProvider> 87 + </ThemeProvider> 88 ); 89 }
+25 -4
web/src/components/RightSidebar.jsx
··· 1 import { useState, useEffect } from "react"; 2 import { Link } from "react-router-dom"; 3 - import { ExternalLink } from "lucide-react"; 4 import { 5 SiFirefox, 6 SiGooglechrome, ··· 12 } from "react-icons/si"; 13 import { FaEdge } from "react-icons/fa"; 14 import { useAuth } from "../context/AuthContext"; 15 import { getTrendingTags } from "../api/client"; 16 17 const isFirefox = ··· 58 } 59 60 export default function RightSidebar() { 61 const { isAuthenticated } = useAuth(); 62 const ext = getExtensionInfo(); 63 const ExtIcon = ext.icon; ··· 196 </div> 197 198 <div className="right-footer"> 199 - <Link to="/privacy">Privacy</Link> 200 - <span>·</span> 201 - <Link to="/terms">Terms</Link> 202 </div> 203 </aside> 204 );
··· 1 import { useState, useEffect } from "react"; 2 import { Link } from "react-router-dom"; 3 + import { ExternalLink, Sun, Moon, Monitor } from "lucide-react"; 4 import { 5 SiFirefox, 6 SiGooglechrome, ··· 12 } from "react-icons/si"; 13 import { FaEdge } from "react-icons/fa"; 14 import { useAuth } from "../context/AuthContext"; 15 + import { useTheme } from "../context/ThemeContext"; 16 import { getTrendingTags } from "../api/client"; 17 18 const isFirefox = ··· 59 } 60 61 export default function RightSidebar() { 62 + const { theme, setTheme } = useTheme(); 63 const { isAuthenticated } = useAuth(); 64 const ext = getExtensionInfo(); 65 const ExtIcon = ext.icon; ··· 198 </div> 199 200 <div className="right-footer"> 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> 223 </div> 224 </aside> 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 "JetBrains Mono", source-code-pro, Menlo, Monaco, Consolas, monospace; 33 } 34 35 * { 36 margin: 0; 37 padding: 0;
··· 32 "JetBrains Mono", source-code-pro, Menlo, Monaco, Consolas, monospace; 33 } 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 + 59 * { 60 margin: 0; 61 padding: 0;
+33 -5
web/src/css/layout.css
··· 359 .right-footer { 360 margin-top: auto; 361 display: flex; 362 - flex-wrap: wrap; 363 - gap: 12px; 364 - font-size: 0.75rem; 365 color: var(--text-tertiary); 366 } 367 368 - .right-footer a { 369 color: var(--text-tertiary); 370 } 371 372 - .right-footer a:hover { 373 color: var(--text-secondary); 374 } 375 376 .mobile-nav {
··· 359 .right-footer { 360 margin-top: auto; 361 display: flex; 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; 373 color: var(--text-tertiary); 374 } 375 376 + .footer-links a { 377 color: var(--text-tertiary); 378 + text-decoration: none; 379 } 380 381 + .footer-links a:hover { 382 + text-decoration: underline; 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); 402 } 403 404 .mobile-nav {