tangled
alpha
login
or
join now
margin.at
/
margin
Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
33
fork
atom
overview
issues
2
pulls
pipelines
Implement Light Mode
scanash.com
1 day ago
21cb43f0
300af0f8
+605
-71
12 changed files
expand all
collapse all
unified
split
extension
content
content.js
popup
popup.css
popup.html
popup.js
sidepanel
sidepanel.css
sidepanel.html
sidepanel.js
web
src
App.jsx
components
RightSidebar.jsx
context
ThemeContext.jsx
css
base.css
layout.css
+126
-50
extension/content/content.js
···
7
7
let currentSelection = null;
8
8
9
9
const OVERLAY_STYLES = `
10
10
-
:host { all: initial; }
10
10
+
:host {
11
11
+
all: initial;
12
12
+
--bg-primary: #09090b;
13
13
+
--bg-secondary: #0f0f12;
14
14
+
--bg-tertiary: #18181b;
15
15
+
--bg-card: #09090b;
16
16
+
--bg-elevated: #18181b;
17
17
+
--bg-hover: #27272a;
18
18
+
19
19
+
--text-primary: #e4e4e7;
20
20
+
--text-secondary: #a1a1aa;
21
21
+
--border: #27272a;
22
22
+
23
23
+
--accent: #6366f1;
24
24
+
--accent-hover: #4f46e5;
25
25
+
}
26
26
+
27
27
+
:host(.light) {
28
28
+
--bg-primary: #ffffff;
29
29
+
--bg-secondary: #f4f4f5;
30
30
+
--bg-tertiary: #e4e4e7;
31
31
+
--bg-card: #ffffff;
32
32
+
--bg-elevated: #f4f4f5;
33
33
+
--bg-hover: #e4e4e7;
34
34
+
35
35
+
--text-primary: #18181b;
36
36
+
--text-secondary: #52525b;
37
37
+
--border: #e4e4e7;
38
38
+
39
39
+
--accent: #4f46e5;
40
40
+
--accent-hover: #4338ca;
41
41
+
}
42
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
23
-
background: #09090b;
24
24
-
border: 1px solid #27272a;
55
55
+
background: var(--bg-card);
56
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
33
-
color: #e4e4e7;
65
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
43
-
border-bottom: 1px solid #27272a;
75
75
+
border-bottom: 1px solid var(--border);
44
76
display: flex;
45
77
justify-content: space-between;
46
78
align-items: center;
47
47
-
background: #0f0f12;
79
79
+
background: var(--bg-secondary);
48
80
border-radius: 12px 12px 0 0;
49
81
font-weight: 600;
50
82
font-size: 13px;
83
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
57
-
border-bottom: 1px solid #27272a;
90
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
71
-
width: 24px; height: 24px; border-radius: 50%; background: #27272a;
104
104
+
width: 24px; height: 24px; border-radius: 50%; background: var(--bg-hover);
72
105
display: flex; align-items: center; justify-content: center;
73
73
-
font-size: 10px; color: #a1a1aa;
106
106
+
font-size: 10px; color: var(--text-secondary);
74
107
}
75
75
-
.popover-handle { font-size: 12px; font-weight: 600; color: #e4e4e7; }
76
76
-
.popover-close { background: none; border: none; color: #71717a; cursor: pointer; padding: 4px; }
77
77
-
.popover-close:hover { color: #e4e4e7; }
78
78
-
.popover-content { padding: 4px 16px 12px; font-size: 13px; line-height: 1.5; color: #e4e4e7; }
108
108
+
.popover-handle { font-size: 12px; font-weight: 600; color: var(--text-primary); }
109
109
+
.popover-close { background: none; border: none; color: var(--text-secondary); cursor: pointer; padding: 4px; }
110
110
+
.popover-close:hover { color: var(--text-primary); }
111
111
+
.popover-content { padding: 4px 16px 12px; font-size: 13px; line-height: 1.5; color: var(--text-primary); }
79
112
.popover-quote {
80
80
-
margin-top: 8px; padding: 6px 10px; background: #18181b;
81
81
-
border-left: 2px solid #6366f1; border-radius: 4px;
82
82
-
font-size: 11px; color: #a1a1aa; font-style: italic;
113
113
+
margin-top: 8px; padding: 6px 10px; background: var(--bg-tertiary);
114
114
+
border-left: 2px solid var(--accent); border-radius: 4px;
115
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
89
-
background: none; border: 1px solid #27272a; border-radius: 4px;
90
90
-
padding: 4px 8px; color: #a1a1aa; font-size: 11px; cursor: pointer;
122
122
+
background: none; border: 1px solid var(--border); border-radius: 4px;
123
123
+
padding: 4px 8px; color: var(--text-secondary); font-size: 11px; cursor: pointer;
91
124
}
92
92
-
.btn-action:hover { background: #27272a; color: #e4e4e7; }
125
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
99
-
background: #09090b;
100
100
-
border: 1px solid #27272a;
132
132
+
background: var(--bg-card);
133
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
116
-
color: #e4e4e7;
149
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
123
-
background: #27272a;
156
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
133
-
background: #09090b;
134
134
-
border: 1px solid #27272a;
166
166
+
background: var(--bg-card);
167
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
142
-
color: #e4e4e7;
175
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
151
-
background: #18181b;
152
152
-
border-left: 3px solid #6366f1;
184
184
+
background: var(--bg-tertiary);
185
185
+
border-left: 3px solid var(--accent);
153
186
border-radius: 4px;
154
187
font-size: 12px;
155
155
-
color: #a1a1aa;
188
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
166
-
background: #18181b;
167
167
-
border: 1px solid #27272a;
199
199
+
background: var(--bg-elevated);
200
200
+
border: 1px solid var(--border);
168
201
border-radius: 8px;
169
169
-
color: #e4e4e7;
202
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
178
-
border-color: #6366f1;
211
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
188
-
border: 1px solid #27272a;
221
221
+
border: 1px solid var(--border);
189
222
border-radius: 6px;
190
190
-
color: #a1a1aa;
223
223
+
color: var(--text-secondary);
191
224
font-size: 13px;
192
225
cursor: pointer;
193
226
}
194
227
.btn-cancel:hover {
195
195
-
background: #27272a;
196
196
-
color: #e4e4e7;
228
228
+
background: var(--bg-hover);
229
229
+
color: var(--text-primary);
197
230
}
198
231
.btn-submit {
199
232
padding: 8px 16px;
200
200
-
background: #6366f1;
233
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
209
-
background: #4f46e5;
242
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
216
-
border-top: 1px solid #27272a;
249
249
+
border-top: 1px solid var(--border);
217
250
padding: 12px 16px;
218
218
-
background: #0f0f12;
251
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
225
-
background: #18181b;
226
226
-
border: 1px solid #27272a;
258
258
+
background: var(--bg-elevated);
259
259
+
border: 1px solid var(--border);
227
260
border-radius: 6px;
228
228
-
color: #e4e4e7;
261
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
236
-
border-color: #6366f1;
269
269
+
border-color: var(--accent);
237
270
}
238
271
.reply-submit {
239
272
padding: 6px 12px;
240
240
-
background: #6366f1;
273
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
254
-
border-top: 1px solid #27272a;
287
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
262
-
color: #a1a1aa;
295
295
+
color: var(--text-secondary);
263
296
margin-bottom: 4px;
264
297
}
265
298
.reply-text {
266
299
font-size: 12px;
267
267
-
color: #e4e4e7;
300
300
+
color: var(--text-primary);
268
301
line-height: 1.4;
269
302
}
270
303
`;
···
422
455
}
423
456
}
424
457
458
458
+
function applyTheme(theme) {
459
459
+
if (!sidebarHost) return;
460
460
+
sidebarHost.classList.remove("light", "dark");
461
461
+
if (theme === "system" || !theme) {
462
462
+
if (window.matchMedia("(prefers-color-scheme: light)").matches) {
463
463
+
sidebarHost.classList.add("light");
464
464
+
}
465
465
+
} else {
466
466
+
sidebarHost.classList.add(theme);
467
467
+
}
468
468
+
}
469
469
+
470
470
+
window
471
471
+
.matchMedia("(prefers-color-scheme: light)")
472
472
+
.addEventListener("change", (e) => {
473
473
+
chrome.storage.local.get(["theme"], (result) => {
474
474
+
if (!result.theme || result.theme === "system") {
475
475
+
if (e.matches) {
476
476
+
sidebarHost?.classList.add("light");
477
477
+
} else {
478
478
+
sidebarHost?.classList.remove("light");
479
479
+
}
480
480
+
}
481
481
+
});
482
482
+
});
483
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
459
-
chrome.storage.local.get(["showOverlay"], (result) => {
518
518
+
chrome.storage.local.get(["showOverlay", "theme"], (result) => {
519
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
532
+
533
533
+
chrome.storage.onChanged.addListener((changes, area) => {
534
534
+
if (area === "local") {
535
535
+
if (changes.theme) {
536
536
+
applyTheme(changes.theme.newValue);
537
537
+
}
538
538
+
if (changes.showOverlay) {
539
539
+
if (changes.showOverlay.newValue === false) {
540
540
+
sidebarHost.style.display = "none";
541
541
+
} else {
542
542
+
sidebarHost.style.display = "";
543
543
+
fetchAnnotations();
544
544
+
}
545
545
+
}
546
546
+
}
547
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
32
+
@media (prefers-color-scheme: light) {
33
33
+
:root {
34
34
+
--bg-primary: #ffffff;
35
35
+
--bg-secondary: #f4f4f5;
36
36
+
--bg-tertiary: #e4e4e7;
37
37
+
--bg-card: #ffffff;
38
38
+
--bg-elevated: #f4f4f5;
39
39
+
--bg-hover: #e4e4e7;
40
40
+
41
41
+
--text-primary: #18181b;
42
42
+
--text-secondary: #52525b;
43
43
+
--text-tertiary: #71717a;
44
44
+
--border: #e4e4e7;
45
45
+
--border-hover: #d4d4d8;
46
46
+
47
47
+
--accent: #4f46e5;
48
48
+
--accent-hover: #4338ca;
49
49
+
--accent-text: #4f46e5;
50
50
+
--accent-subtle: rgba(79, 70, 229, 0.1);
51
51
+
52
52
+
--success: #059669;
53
53
+
--error: #dc2626;
54
54
+
--warning: #d97706;
55
55
+
}
56
56
+
}
57
57
+
58
58
+
body.light {
59
59
+
--bg-primary: #ffffff;
60
60
+
--bg-secondary: #f4f4f5;
61
61
+
--bg-tertiary: #e4e4e7;
62
62
+
--bg-card: #ffffff;
63
63
+
--bg-elevated: #f4f4f5;
64
64
+
--bg-hover: #e4e4e7;
65
65
+
66
66
+
--text-primary: #18181b;
67
67
+
--text-secondary: #52525b;
68
68
+
--text-tertiary: #71717a;
69
69
+
--border: #e4e4e7;
70
70
+
--border-hover: #d4d4d8;
71
71
+
72
72
+
--accent: #4f46e5;
73
73
+
--accent-hover: #4338ca;
74
74
+
--accent-text: #4f46e5;
75
75
+
--accent-subtle: rgba(79, 70, 229, 0.1);
76
76
+
77
77
+
--success: #059669;
78
78
+
--error: #dc2626;
79
79
+
--warning: #d97706;
80
80
+
}
81
81
+
82
82
+
body.dark {
83
83
+
--bg-primary: #09090b;
84
84
+
--bg-secondary: #0f0f12;
85
85
+
--bg-tertiary: #18181b;
86
86
+
--bg-card: #09090b;
87
87
+
--bg-elevated: #18181b;
88
88
+
--bg-hover: #27272a;
89
89
+
90
90
+
--text-primary: #e4e4e7;
91
91
+
--text-secondary: #a1a1aa;
92
92
+
--text-tertiary: #71717a;
93
93
+
--border: #27272a;
94
94
+
--border-hover: #3f3f46;
95
95
+
96
96
+
--accent: #6366f1;
97
97
+
--accent-hover: #4f46e5;
98
98
+
--accent-subtle: rgba(99, 102, 241, 0.1);
99
99
+
--accent-text: #818cf8;
100
100
+
--success: #10b981;
101
101
+
--error: #ef4444;
102
102
+
--warning: #f59e0b;
103
103
+
}
104
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
786
+
.theme-toggle-group {
787
787
+
display: flex;
788
788
+
background: var(--bg-tertiary);
789
789
+
padding: 4px;
790
790
+
border-radius: var(--radius-md);
791
791
+
gap: 2px;
792
792
+
margin-top: 8px;
793
793
+
}
794
794
+
795
795
+
.theme-btn {
796
796
+
flex: 1;
797
797
+
padding: 6px;
798
798
+
border: none;
799
799
+
background: transparent;
800
800
+
color: var(--text-secondary);
801
801
+
font-size: 12px;
802
802
+
font-weight: 500;
803
803
+
border-radius: var(--radius-sm);
804
804
+
cursor: pointer;
805
805
+
transition: all 0.15s ease;
806
806
+
}
807
807
+
808
808
+
.theme-btn:hover {
809
809
+
color: var(--text-primary);
810
810
+
background: rgba(128, 128, 128, 0.1);
811
811
+
}
812
812
+
813
813
+
.theme-btn.active {
814
814
+
background: var(--bg-card);
815
815
+
color: var(--text-primary);
816
816
+
box-shadow: var(--shadow-sm);
817
817
+
}
+8
extension/popup/popup.html
···
247
247
/>
248
248
<p class="setting-help">Enter your backend URL</p>
249
249
</div>
250
250
+
<div class="setting-item">
251
251
+
<label for="theme-select">Theme</label>
252
252
+
<div class="theme-toggle-group">
253
253
+
<button class="theme-btn active" data-theme="system">Auto</button>
254
254
+
<button class="theme-btn" data-theme="light">Light</button>
255
255
+
<button class="theme-btn" data-theme="dark">Dark</button>
256
256
+
</div>
257
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
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
51
-
const storage = await browserAPI.storage.local.get(["apiUrl", "showOverlay"]);
52
52
+
const storage = await browserAPI.storage.local.get([
53
53
+
"apiUrl",
54
54
+
"showOverlay",
55
55
+
"theme",
56
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
65
+
66
66
+
const currentTheme = storage.theme || "system";
67
67
+
applyTheme(currentTheme);
68
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
252
+
});
253
253
+
254
254
+
els.themeBtns.forEach((btn) => {
255
255
+
btn.addEventListener("click", async () => {
256
256
+
const theme = btn.getAttribute("data-theme");
257
257
+
await browserAPI.storage.local.set({ theme });
258
258
+
applyTheme(theme);
259
259
+
updateThemeUI(theme);
260
260
+
});
243
261
});
244
262
245
263
els.closeCollectionSelector?.addEventListener("click", () => {
···
781
799
});
782
800
}
783
801
});
802
802
+
803
803
+
function applyTheme(theme) {
804
804
+
document.body.classList.remove("light", "dark");
805
805
+
if (theme === "system") return;
806
806
+
document.body.classList.add(theme);
807
807
+
}
808
808
+
809
809
+
function updateThemeUI(theme) {
810
810
+
const btns = document.querySelectorAll(".theme-btn");
811
811
+
btns.forEach((btn) => {
812
812
+
if (btn.getAttribute("data-theme") === theme) {
813
813
+
btn.classList.add("active");
814
814
+
} else {
815
815
+
btn.classList.remove("active");
816
816
+
}
817
817
+
});
818
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
34
+
@media (prefers-color-scheme: light) {
35
35
+
:root {
36
36
+
--bg-primary: #ffffff;
37
37
+
--bg-secondary: #f4f4f5;
38
38
+
--bg-tertiary: #e4e4e7;
39
39
+
--bg-card: #ffffff;
40
40
+
--bg-hover: #e4e4e7;
41
41
+
--bg-elevated: #f4f4f5;
42
42
+
43
43
+
--text-primary: #18181b;
44
44
+
--text-secondary: #52525b;
45
45
+
--text-tertiary: #71717a;
46
46
+
47
47
+
--accent: #4f46e5;
48
48
+
--accent-hover: #4338ca;
49
49
+
--accent-subtle: rgba(79, 70, 229, 0.1);
50
50
+
--accent-text: #4f46e5;
51
51
+
52
52
+
--border: #e4e4e7;
53
53
+
--border-hover: #d4d4d8;
54
54
+
55
55
+
--success: #059669;
56
56
+
--error: #dc2626;
57
57
+
--warning: #d97706;
58
58
+
}
59
59
+
}
60
60
+
61
61
+
body.light {
62
62
+
--bg-primary: #ffffff;
63
63
+
--bg-secondary: #f4f4f5;
64
64
+
--bg-tertiary: #e4e4e7;
65
65
+
--bg-card: #ffffff;
66
66
+
--bg-hover: #e4e4e7;
67
67
+
--bg-elevated: #f4f4f5;
68
68
+
69
69
+
--text-primary: #18181b;
70
70
+
--text-secondary: #52525b;
71
71
+
--text-tertiary: #71717a;
72
72
+
73
73
+
--accent: #4f46e5;
74
74
+
--accent-hover: #4338ca;
75
75
+
--accent-subtle: rgba(79, 70, 229, 0.1);
76
76
+
--accent-text: #4f46e5;
77
77
+
78
78
+
--border: #e4e4e7;
79
79
+
--border-hover: #d4d4d8;
80
80
+
81
81
+
--success: #059669;
82
82
+
--error: #dc2626;
83
83
+
--warning: #d97706;
84
84
+
}
85
85
+
86
86
+
body.dark {
87
87
+
--bg-primary: #09090b;
88
88
+
--bg-secondary: #0f0f12;
89
89
+
--bg-tertiary: #18181b;
90
90
+
--bg-card: #09090b;
91
91
+
--bg-hover: #18181b;
92
92
+
--bg-elevated: #18181b;
93
93
+
94
94
+
--text-primary: #e4e4e7;
95
95
+
--text-secondary: #a1a1aa;
96
96
+
--text-tertiary: #71717a;
97
97
+
98
98
+
--accent: #6366f1;
99
99
+
--accent-hover: #4f46e5;
100
100
+
--accent-subtle: rgba(99, 102, 241, 0.1);
101
101
+
--accent-text: #818cf8;
102
102
+
103
103
+
--border: #27272a;
104
104
+
--border-hover: #3f3f46;
105
105
+
106
106
+
--success: #10b981;
107
107
+
--error: #ef4444;
108
108
+
--warning: #f59e0b;
109
109
+
}
110
110
+
34
111
* {
35
112
margin: 0;
36
113
padding: 0;
···
929
1006
transform: translateX(20px);
930
1007
background-color: white;
931
1008
}
1009
1009
+
.theme-toggle-group {
1010
1010
+
display: flex;
1011
1011
+
background: var(--bg-tertiary);
1012
1012
+
padding: 4px;
1013
1013
+
border-radius: var(--radius-md);
1014
1014
+
gap: 2px;
1015
1015
+
margin-top: 8px;
1016
1016
+
}
1017
1017
+
1018
1018
+
.theme-btn {
1019
1019
+
flex: 1;
1020
1020
+
padding: 6px;
1021
1021
+
border: none;
1022
1022
+
background: transparent;
1023
1023
+
color: var(--text-secondary);
1024
1024
+
font-size: 12px;
1025
1025
+
font-weight: 500;
1026
1026
+
border-radius: var(--radius-sm);
1027
1027
+
cursor: pointer;
1028
1028
+
transition: all 0.15s ease;
1029
1029
+
}
1030
1030
+
1031
1031
+
.theme-btn:hover {
1032
1032
+
color: var(--text-primary);
1033
1033
+
background: rgba(128, 128, 128, 0.1);
1034
1034
+
}
1035
1035
+
1036
1036
+
.theme-btn.active {
1037
1037
+
background: var(--bg-card);
1038
1038
+
color: var(--text-primary);
1039
1039
+
box-shadow: var(--shadow-sm);
1040
1040
+
}
+8
extension/sidepanel/sidepanel.html
···
279
279
/>
280
280
<p class="setting-help">Enter your Margin backend URL</p>
281
281
</div>
282
282
+
<div class="setting-item">
283
283
+
<label for="theme-select">Theme</label>
284
284
+
<div class="theme-toggle-group">
285
285
+
<button class="theme-btn active" data-theme="system">Auto</button>
286
286
+
<button class="theme-btn" data-theme="light">Light</button>
287
287
+
<button class="theme-btn" data-theme="dark">Dark</button>
288
288
+
</div>
289
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
61
-
if (area === "local" && changes.apiUrl) {
62
62
-
apiUrl = changes.apiUrl.newValue || "";
63
63
-
64
64
-
els.apiUrlInput.value = apiUrl;
65
65
-
checkSession();
61
61
+
if (area === "local") {
62
62
+
if (changes.apiUrl) {
63
63
+
apiUrl = changes.apiUrl.newValue || "";
64
64
+
els.apiUrlInput.value = apiUrl;
65
65
+
checkSession();
66
66
+
}
67
67
+
if (changes.theme) {
68
68
+
const newTheme = changes.theme.newValue || "system";
69
69
+
applyTheme(newTheme);
70
70
+
updateThemeUI(newTheme);
71
71
+
}
66
72
}
73
73
+
});
74
74
+
75
75
+
chrome.storage.local.get(["theme"], (result) => {
76
76
+
const currentTheme = result.theme || "system";
77
77
+
applyTheme(currentTheme);
78
78
+
updateThemeUI(currentTheme);
79
79
+
});
80
80
+
81
81
+
const themeBtns = document.querySelectorAll(".theme-btn");
82
82
+
themeBtns.forEach((btn) => {
83
83
+
btn.addEventListener("click", () => {
84
84
+
const theme = btn.getAttribute("data-theme");
85
85
+
chrome.storage.local.set({ theme });
86
86
+
applyTheme(theme);
87
87
+
updateThemeUI(theme);
88
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
267
-
await chrome.storage.local.set({ apiUrl: newUrl, showOverlay });
289
289
+
await chrome.storage.local.set({
290
290
+
apiUrl: newUrl,
291
291
+
showOverlay,
292
292
+
});
268
293
if (newUrl) {
269
294
apiUrl = newUrl;
270
295
}
···
909
934
resolve(response);
910
935
}
911
936
});
937
937
+
});
938
938
+
}
939
939
+
940
940
+
function applyTheme(theme) {
941
941
+
document.body.classList.remove("light", "dark");
942
942
+
if (theme === "system") return;
943
943
+
document.body.classList.add(theme);
944
944
+
}
945
945
+
946
946
+
function updateThemeUI(theme) {
947
947
+
const btns = document.querySelectorAll(".theme-btn");
948
948
+
btns.forEach((btn) => {
949
949
+
if (btn.getAttribute("data-theme") === theme) {
950
950
+
btn.classList.add("active");
951
951
+
} else {
952
952
+
btn.classList.remove("active");
953
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
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
80
-
<AuthProvider>
81
81
-
<Routes>
82
82
-
<Route path="/*" element={<AppContent />} />
83
83
-
</Routes>
84
84
-
</AuthProvider>
81
81
+
<ThemeProvider>
82
82
+
<AuthProvider>
83
83
+
<Routes>
84
84
+
<Route path="/*" element={<AppContent />} />
85
85
+
</Routes>
86
86
+
</AuthProvider>
87
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
3
-
import { ExternalLink } from "lucide-react";
3
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
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
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
199
-
<Link to="/privacy">Privacy</Link>
200
200
-
<span>·</span>
201
201
-
<Link to="/terms">Terms</Link>
201
201
+
<div className="footer-links">
202
202
+
<Link to="/privacy">Privacy</Link>
203
203
+
<span>·</span>
204
204
+
<Link to="/terms">Terms</Link>
205
205
+
</div>
206
206
+
<button
207
207
+
onClick={() => {
208
208
+
const next =
209
209
+
theme === "system"
210
210
+
? "light"
211
211
+
: theme === "light"
212
212
+
? "dark"
213
213
+
: "system";
214
214
+
setTheme(next);
215
215
+
}}
216
216
+
className="theme-toggle-mini"
217
217
+
title={`Theme: ${theme}`}
218
218
+
>
219
219
+
{theme === "system" && <Monitor size={14} />}
220
220
+
{theme === "light" && <Sun size={14} />}
221
221
+
{theme === "dark" && <Moon size={14} />}
222
222
+
</button>
202
223
</div>
203
224
</aside>
204
225
);
+75
web/src/context/ThemeContext.jsx
···
1
1
+
import { createContext, useContext, useEffect, useState } from "react";
2
2
+
3
3
+
const ThemeContext = createContext({
4
4
+
theme: "system",
5
5
+
setTheme: () => null,
6
6
+
});
7
7
+
8
8
+
export function ThemeProvider({ children }) {
9
9
+
const [theme, setTheme] = useState(() => {
10
10
+
return localStorage.getItem("theme") || "system";
11
11
+
});
12
12
+
13
13
+
useEffect(() => {
14
14
+
localStorage.setItem("theme", theme);
15
15
+
16
16
+
const root = window.document.documentElement;
17
17
+
root.classList.remove("light", "dark");
18
18
+
19
19
+
delete root.dataset.theme;
20
20
+
21
21
+
if (theme === "system") {
22
22
+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
23
23
+
.matches
24
24
+
? "dark"
25
25
+
: "light";
26
26
+
27
27
+
if (systemTheme === "light") {
28
28
+
root.dataset.theme = "light";
29
29
+
} else {
30
30
+
root.dataset.theme = "dark";
31
31
+
}
32
32
+
return;
33
33
+
}
34
34
+
35
35
+
if (theme === "light") {
36
36
+
root.dataset.theme = "light";
37
37
+
}
38
38
+
}, [theme]);
39
39
+
40
40
+
useEffect(() => {
41
41
+
if (theme !== "system") return;
42
42
+
43
43
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
44
44
+
const handleChange = () => {
45
45
+
const root = window.document.documentElement;
46
46
+
if (mediaQuery.matches) {
47
47
+
delete root.dataset.theme;
48
48
+
} else {
49
49
+
root.dataset.theme = "light";
50
50
+
}
51
51
+
};
52
52
+
53
53
+
mediaQuery.addEventListener("change", handleChange);
54
54
+
return () => mediaQuery.removeEventListener("change", handleChange);
55
55
+
}, [theme]);
56
56
+
57
57
+
const value = {
58
58
+
theme,
59
59
+
setTheme: (newTheme) => {
60
60
+
setTheme(newTheme);
61
61
+
},
62
62
+
};
63
63
+
64
64
+
return (
65
65
+
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
66
66
+
);
67
67
+
}
68
68
+
69
69
+
// eslint-disable-next-line react-refresh/only-export-components
70
70
+
export function useTheme() {
71
71
+
const context = useContext(ThemeContext);
72
72
+
if (context === undefined)
73
73
+
throw new Error("useTheme must be used within a ThemeProvider");
74
74
+
return context;
75
75
+
}
+24
web/src/css/base.css
···
32
32
"JetBrains Mono", source-code-pro, Menlo, Monaco, Consolas, monospace;
33
33
}
34
34
35
35
+
[data-theme="light"] {
36
36
+
--bg-primary: #ffffff;
37
37
+
--bg-secondary: #f4f4f5;
38
38
+
--bg-tertiary: #e4e4e7;
39
39
+
--bg-card: #ffffff;
40
40
+
--bg-elevated: #f4f4f5;
41
41
+
--text-primary: #18181b;
42
42
+
--text-secondary: #52525b;
43
43
+
--text-tertiary: #71717a;
44
44
+
--border: #e4e4e7;
45
45
+
--border-hover: #d4d4d8;
46
46
+
--accent: #4f46e5;
47
47
+
--accent-hover: #4338ca;
48
48
+
--accent-subtle: rgba(79, 70, 229, 0.1);
49
49
+
--accent-text: #4f46e5;
50
50
+
--success: #059669;
51
51
+
--error: #dc2626;
52
52
+
--warning: #d97706;
53
53
+
--info: #2563eb;
54
54
+
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
55
55
+
--shadow-md:
56
56
+
0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
57
57
+
}
58
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
362
-
flex-wrap: wrap;
363
363
-
gap: 12px;
364
364
-
font-size: 0.75rem;
362
362
+
align-items: center;
363
363
+
justify-content: space-between;
364
364
+
padding-top: 16px;
365
365
+
border-top: 1px solid var(--border);
366
366
+
}
367
367
+
368
368
+
.footer-links {
369
369
+
display: flex;
370
370
+
align-items: center;
371
371
+
gap: 8px;
372
372
+
font-size: 12px;
365
373
color: var(--text-tertiary);
366
374
}
367
375
368
368
-
.right-footer a {
376
376
+
.footer-links a {
369
377
color: var(--text-tertiary);
378
378
+
text-decoration: none;
370
379
}
371
380
372
372
-
.right-footer a:hover {
381
381
+
.footer-links a:hover {
382
382
+
text-decoration: underline;
373
383
color: var(--text-secondary);
384
384
+
}
385
385
+
386
386
+
.theme-toggle-mini {
387
387
+
background: none;
388
388
+
border: none;
389
389
+
cursor: pointer;
390
390
+
padding: 4px;
391
391
+
color: var(--text-tertiary);
392
392
+
display: flex;
393
393
+
align-items: center;
394
394
+
justify-content: center;
395
395
+
border-radius: 4px;
396
396
+
transition: all 0.2s;
397
397
+
}
398
398
+
399
399
+
.theme-toggle-mini:hover {
400
400
+
color: var(--text-primary);
401
401
+
background: var(--bg-hover);
374
402
}
375
403
376
404
.mobile-nav {