tangled
alpha
login
or
join now
margin.at
/
margin
86
fork
atom
Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
86
fork
atom
overview
issues
4
pulls
1
pipelines
UI redesign + cleanup
scanash.com
1 month ago
3920646b
a71a8a08
+3187
-2886
36 changed files
expand all
collapse all
unified
split
backend
internal
api
og.go
extension
background
service-worker.js
content
content.css
content.js
popup
popup.css
sidepanel
sidepanel.css
web
index.html
src
App.jsx
components
AnnotationCard.jsx
BookmarkCard.jsx
CollectionItemCard.jsx
CollectionModal.jsx
IOSInstallBanner.jsx
MobileNav.jsx
RightSidebar.jsx
Sidebar.jsx
TopNav.jsx
css
annotations.css
base.css
buttons.css
cards.css
collections.css
feed.css
layout.css
modals.css
skeleton.css
utilities.css
index.css
pages
Bookmarks.jsx
CollectionDetail.jsx
Collections.jsx
Feed.jsx
Highlights.jsx
Profile.jsx
Url.jsx
UserUrl.jsx
+16
-16
backend/internal/api/og.go
···
875
height := 630
876
padding := 100
877
878
-
bgPrimary := color.RGBA{12, 10, 20, 255}
879
-
accent := color.RGBA{168, 85, 247, 255}
880
-
textPrimary := color.RGBA{244, 240, 255, 255}
881
-
textSecondary := color.RGBA{168, 158, 200, 255}
882
-
border := color.RGBA{45, 38, 64, 255}
883
884
img := image.NewRGBA(image.Rect(0, 0, width, height))
885
···
1118
height := 630
1119
padding := 120
1120
1121
-
bgPrimary := color.RGBA{12, 10, 20, 255}
1122
-
accent := color.RGBA{168, 85, 247, 255}
1123
-
textPrimary := color.RGBA{244, 240, 255, 255}
1124
-
textSecondary := color.RGBA{168, 158, 200, 255}
1125
-
textTertiary := color.RGBA{107, 95, 138, 255}
1126
-
border := color.RGBA{45, 38, 64, 255}
1127
1128
img := image.NewRGBA(image.Rect(0, 0, width, height))
1129
···
1220
height := 630
1221
padding := 100
1222
1223
-
bgPrimary := color.RGBA{12, 10, 20, 255}
1224
-
accent := color.RGBA{250, 204, 21, 255}
1225
-
textPrimary := color.RGBA{244, 240, 255, 255}
1226
-
textSecondary := color.RGBA{168, 158, 200, 255}
1227
-
border := color.RGBA{45, 38, 64, 255}
1228
1229
img := image.NewRGBA(image.Rect(0, 0, width, height))
1230
···
875
height := 630
876
padding := 100
877
878
+
bgPrimary := color.RGBA{10, 10, 13, 255}
879
+
accent := color.RGBA{149, 122, 134, 255}
880
+
textPrimary := color.RGBA{234, 234, 238, 255}
881
+
textSecondary := color.RGBA{168, 164, 171, 255}
882
+
border := color.RGBA{42, 40, 46, 255}
883
884
img := image.NewRGBA(image.Rect(0, 0, width, height))
885
···
1118
height := 630
1119
padding := 120
1120
1121
+
bgPrimary := color.RGBA{10, 10, 13, 255}
1122
+
accent := color.RGBA{149, 122, 134, 255}
1123
+
textPrimary := color.RGBA{234, 234, 238, 255}
1124
+
textSecondary := color.RGBA{168, 164, 171, 255}
1125
+
textTertiary := color.RGBA{107, 103, 112, 255}
1126
+
border := color.RGBA{42, 40, 46, 255}
1127
1128
img := image.NewRGBA(image.Rect(0, 0, width, height))
1129
···
1220
height := 630
1221
padding := 100
1222
1223
+
bgPrimary := color.RGBA{10, 10, 13, 255}
1224
+
accent := color.RGBA{149, 122, 134, 255}
1225
+
textPrimary := color.RGBA{234, 234, 238, 255}
1226
+
textSecondary := color.RGBA{168, 164, 171, 255}
1227
+
border := color.RGBA{42, 40, 46, 255}
1228
1229
img := image.NewRGBA(image.Rect(0, 0, width, height))
1230
+1
-1
extension/background/service-worker.js
···
398
sendResponse({ success: true, data: allItems });
399
400
if (sender.tab) {
401
-
const count = items.length;
402
chrome.action
403
.setBadgeText({
404
text: count > 0 ? count.toString() : "",
···
398
sendResponse({ success: true, data: allItems });
399
400
if (sender.tab) {
401
+
const count = allItems.length;
402
chrome.action
403
.setBadgeText({
404
text: count > 0 ? count.toString() : "",
+3
-3
extension/content/content.css
···
1
::highlight(margin-highlight-preview) {
2
-
background-color: rgba(168, 85, 247, 0.3);
3
color: inherit;
4
}
5
6
::highlight(margin-scroll-highlight) {
7
-
background-color: rgba(99, 102, 241, 0.4);
8
color: inherit;
9
}
10
11
::highlight(margin-page-highlights) {
12
-
background-color: rgba(252, 211, 77, 0.3);
13
color: inherit;
14
}
15
···
1
::highlight(margin-highlight-preview) {
2
+
background-color: rgba(149, 122, 134, 0.3);
3
color: inherit;
4
}
5
6
::highlight(margin-scroll-highlight) {
7
+
background-color: rgba(149, 122, 134, 0.5);
8
color: inherit;
9
}
10
11
::highlight(margin-page-highlights) {
12
+
background-color: rgba(149, 122, 134, 0.25);
13
color: inherit;
14
}
15
+149
-110
extension/content/content.js
···
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;
0
22
23
-
--accent: #6366f1;
24
-
--accent-hover: #4f46e5;
0
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;
0
38
39
-
--accent: #4f46e5;
40
-
--accent-hover: #4338ca;
0
41
}
42
43
.margin-overlay {
···
51
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);
60
display: flex;
61
flex-direction: column;
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;
69
-
max-height: 480px;
70
overflow: hidden;
71
}
72
-
@keyframes popover-in { to { opacity: 1; transform: scale(1); } }
0
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);
0
0
0
0
0
0
0
0
0
0
0
0
0
84
}
0
0
85
.popover-scroll-area {
86
overflow-y: auto;
87
-
max-height: 400px;
88
}
89
-
.popover-item-block {
0
0
90
border-bottom: 1px solid var(--border);
91
-
margin-bottom: 0;
92
-
animation: fade-in 0.2s;
93
}
94
-
.popover-item-block:last-child {
95
border-bottom: none;
96
}
97
-
.popover-item-header {
98
-
padding: 12px 16px 4px;
99
display: flex;
100
align-items: center;
101
gap: 8px;
0
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);
0
0
0
0
0
0
0
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;
0
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;
0
0
0
124
}
125
-
.btn-action:hover { background: var(--bg-hover); color: var(--text-primary); }
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
126
127
.margin-selection-popup {
128
position: fixed;
···
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;
137
pointer-events: auto;
138
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
139
animation: popover-in 0.15s forwards;
140
}
141
.selection-btn {
···
168
border-radius: 12px;
169
padding: 16px;
170
box-sizing: border-box;
171
-
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
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;
···
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);
···
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);
···
887
.join(",");
888
popoverEl.dataset.itemIds = ids;
889
890
-
const popWidth = 320;
891
const screenWidth = window.innerWidth;
892
let finalLeft = left;
893
if (left + popWidth > screenWidth) finalLeft = screenWidth - popWidth - 20;
···
895
popoverEl.style.top = `${top + 20}px`;
896
popoverEl.style.left = `${finalLeft}px`;
897
898
-
const hasHighlights = items.some((item) => item.type === "Highlight");
899
-
const hasAnnotations = items.some((item) => item.type !== "Highlight");
900
-
let title;
901
-
if (items.length > 1) {
902
-
if (hasHighlights && hasAnnotations) {
903
-
title = `${items.length} Items`;
904
-
} else if (hasHighlights) {
905
-
title = `${items.length} Highlights`;
906
-
} else {
907
-
title = `${items.length} Annotations`;
908
-
}
909
-
} else {
910
-
title = items[0]?.type === "Highlight" ? "Highlight" : "Annotation";
911
-
}
912
913
let contentHtml = items
914
.map((item) => {
···
916
const handle = author.handle || "User";
917
const avatar = author.avatar;
918
const text = item.body?.value || item.text || "";
919
-
const quote =
920
-
item.target?.selector?.exact || item.selector?.exact || "";
921
const id = item.id || item.uri;
0
922
923
-
let avatarHtml = `<div class="popover-avatar">${handle[0]?.toUpperCase() || "U"}</div>`;
924
if (avatar) {
925
-
avatarHtml = `<img src="${avatar}" class="popover-avatar" style="object-fit: cover;">`;
926
}
927
928
-
const isHighlight = item.type === "Highlight";
929
-
930
let bodyHtml = "";
931
-
if (isHighlight) {
932
-
bodyHtml = `<div class="popover-text" style="font-style: italic; color: #a1a1aa;">"${quote}"</div>`;
933
} else {
934
-
bodyHtml = `<div class="popover-text">${text}</div>`;
935
-
if (quote) {
936
-
bodyHtml += `<div class="popover-quote">"${quote}"</div>`;
937
-
}
938
}
939
940
return `
941
-
<div class="popover-item-block">
942
-
<div class="popover-item-header">
943
-
<div class="popover-author">
944
-
${avatarHtml}
945
-
<span class="popover-handle">@${handle}</span>
946
-
</div>
947
-
</div>
948
-
<div class="popover-content">
949
-
${bodyHtml}
950
-
</div>
951
-
<div class="popover-actions">
952
-
${!isHighlight ? `<button class="btn-action btn-reply" data-id="${id}">Reply</button>` : ""}
953
-
<button class="btn-action btn-share" data-id="${id}" data-text="${text}" data-quote="${quote}">Share</button>
954
-
</div>
955
</div>
956
`;
957
})
···
992
btn.addEventListener("click", async () => {
993
const id = btn.getAttribute("data-id");
994
const text = btn.getAttribute("data-text");
995
-
const quote = btn.getAttribute("data-quote");
996
const u = `https://margin.at/annotation/${encodeURIComponent(id)}`;
997
-
const shareText = `${text ? text + "\n" : ""}${quote ? `"${quote}"\n` : ""}${u}`;
998
999
try {
1000
await navigator.clipboard.writeText(shareText);
···
9
const OVERLAY_STYLES = `
10
:host {
11
all: initial;
12
+
--bg-primary: #0a0a0d;
13
+
--bg-secondary: #121216;
14
+
--bg-tertiary: #1a1a1f;
15
+
--bg-card: #0f0f13;
16
+
--bg-elevated: #18181d;
17
+
--bg-hover: #1e1e24;
18
19
+
--text-primary: #eaeaee;
20
+
--text-secondary: #b7b6c5;
21
+
--text-tertiary: #6e6d7a;
22
+
--border: rgba(183, 182, 197, 0.12);
23
24
+
--accent: #957a86;
25
+
--accent-hover: #a98d98;
26
+
--accent-subtle: rgba(149, 122, 134, 0.15);
27
}
28
29
:host(.light) {
30
+
--bg-primary: #f8f8fa;
31
+
--bg-secondary: #ffffff;
32
+
--bg-tertiary: #f0f0f4;
33
--bg-card: #ffffff;
34
+
--bg-elevated: #ffffff;
35
+
--bg-hover: #eeeef2;
36
37
+
--text-primary: #18171c;
38
+
--text-secondary: #5c495a;
39
+
--text-tertiary: #8a8494;
40
+
--border: rgba(92, 73, 90, 0.12);
41
42
+
--accent: #7a5f6d;
43
+
--accent-hover: #664e5b;
44
+
--accent-subtle: rgba(149, 122, 134, 0.12);
45
}
46
47
.margin-overlay {
···
55
56
.margin-popover {
57
position: absolute;
58
+
width: 300px;
59
background: var(--bg-card);
60
border: 1px solid var(--border);
61
border-radius: 12px;
62
padding: 0;
63
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
64
display: flex;
65
flex-direction: column;
66
pointer-events: auto;
67
z-index: 2147483647;
68
+
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif;
69
color: var(--text-primary);
70
opacity: 0;
71
+
transform: translateY(-4px);
72
animation: popover-in 0.15s forwards;
73
+
max-height: 400px;
74
overflow: hidden;
75
}
76
+
@keyframes popover-in { to { opacity: 1; transform: translateY(0); } }
77
+
78
.popover-header {
79
+
padding: 10px 14px;
80
border-bottom: 1px solid var(--border);
81
display: flex;
82
justify-content: space-between;
83
align-items: center;
84
+
background: var(--bg-primary);
85
border-radius: 12px 12px 0 0;
86
+
font-weight: 500;
87
+
font-size: 11px;
88
+
color: var(--text-tertiary);
89
+
text-transform: uppercase;
90
+
letter-spacing: 0.5px;
91
+
}
92
+
.popover-close {
93
+
background: none;
94
+
border: none;
95
+
color: var(--text-tertiary);
96
+
cursor: pointer;
97
+
padding: 2px;
98
+
font-size: 16px;
99
+
line-height: 1;
100
+
opacity: 0.6;
101
+
transition: opacity 0.15s;
102
}
103
+
.popover-close:hover { opacity: 1; }
104
+
105
.popover-scroll-area {
106
overflow-y: auto;
107
+
max-height: 340px;
108
}
109
+
110
+
.comment-item {
111
+
padding: 12px 14px;
112
border-bottom: 1px solid var(--border);
0
0
113
}
114
+
.comment-item:last-child {
115
border-bottom: none;
116
}
117
+
118
+
.comment-header {
119
display: flex;
120
align-items: center;
121
gap: 8px;
122
+
margin-bottom: 6px;
123
}
124
+
.comment-avatar {
125
+
width: 22px;
126
+
height: 22px;
127
+
border-radius: 50%;
128
+
background: var(--accent);
129
+
display: flex;
130
+
align-items: center;
131
+
justify-content: center;
132
+
font-size: 9px;
133
+
font-weight: 600;
134
+
color: white;
135
}
136
+
.comment-handle {
137
+
font-size: 12px;
138
+
font-weight: 600;
139
+
color: var(--text-primary);
0
0
0
0
140
}
141
+
.comment-time {
142
+
font-size: 11px;
143
+
color: var(--text-tertiary);
144
+
margin-left: auto;
145
}
146
+
147
+
.comment-text {
148
+
font-size: 13px;
149
+
line-height: 1.5;
150
+
color: var(--text-primary);
151
+
margin-bottom: 8px;
152
}
153
+
154
+
.highlight-only-badge {
155
+
display: inline-flex;
156
+
align-items: center;
157
+
gap: 4px;
158
+
font-size: 11px;
159
+
color: var(--text-tertiary);
160
+
font-style: italic;
161
+
}
162
+
163
+
.comment-actions {
164
+
display: flex;
165
+
gap: 8px;
166
+
margin-top: 8px;
167
+
}
168
+
.highlight-only-badge {
169
+
font-size: 11px;
170
+
color: var(--text-tertiary);
171
+
font-style: italic;
172
+
opacity: 0.7;
173
+
}
174
+
.comment-action-btn {
175
+
background: none;
176
+
border: none;
177
+
padding: 4px 8px;
178
+
color: var(--text-tertiary);
179
+
font-size: 11px;
180
+
cursor: pointer;
181
+
border-radius: 4px;
182
+
transition: all 0.15s;
183
+
}
184
+
.comment-action-btn:hover {
185
+
background: var(--bg-hover);
186
+
color: var(--text-secondary);
187
+
}
188
189
.margin-selection-popup {
190
position: fixed;
···
194
background: var(--bg-card);
195
border: 1px solid var(--border);
196
border-radius: 8px;
197
+
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
198
z-index: 2147483647;
199
pointer-events: auto;
200
+
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif;
201
animation: popover-in 0.15s forwards;
202
}
203
.selection-btn {
···
230
border-radius: 12px;
231
padding: 16px;
232
box-sizing: border-box;
233
+
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
234
z-index: 2147483647;
235
pointer-events: auto;
236
+
font-family: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif;
237
color: var(--text-primary);
238
animation: popover-in 0.15s forwards;
239
overflow: hidden;
···
243
}
244
.inline-compose-quote {
245
padding: 8px 12px;
246
+
background: var(--accent-subtle);
247
+
border-left: 2px solid var(--accent);
248
border-radius: 4px;
249
font-size: 12px;
250
color: var(--text-secondary);
···
309
}
310
.reply-section {
311
border-top: 1px solid var(--border);
312
+
padding: 10px 14px;
313
+
background: var(--bg-primary);
314
border-radius: 0 0 12px 12px;
315
}
316
.reply-textarea {
317
width: 100%;
318
+
min-height: 50px;
319
padding: 8px 10px;
320
background: var(--bg-elevated);
321
border: 1px solid var(--border);
···
949
.join(",");
950
popoverEl.dataset.itemIds = ids;
951
952
+
const popWidth = 300;
953
const screenWidth = window.innerWidth;
954
let finalLeft = left;
955
if (left + popWidth > screenWidth) finalLeft = screenWidth - popWidth - 20;
···
957
popoverEl.style.top = `${top + 20}px`;
958
popoverEl.style.left = `${finalLeft}px`;
959
960
+
const count = items.length;
961
+
const title = count === 1 ? "1 Comment" : `${count} Comments`;
0
0
0
0
0
0
0
0
0
0
0
0
962
963
let contentHtml = items
964
.map((item) => {
···
966
const handle = author.handle || "User";
967
const avatar = author.avatar;
968
const text = item.body?.value || item.text || "";
0
0
969
const id = item.id || item.uri;
970
+
const isHighlight = item.type === "Highlight";
971
972
+
let avatarHtml = `<div class="comment-avatar">${handle[0]?.toUpperCase() || "U"}</div>`;
973
if (avatar) {
974
+
avatarHtml = `<img src="${avatar}" class="comment-avatar" style="object-fit: cover;">`;
975
}
976
0
0
977
let bodyHtml = "";
978
+
if (isHighlight && !text) {
979
+
bodyHtml = `<div class="highlight-only-badge">Highlighted</div>`;
980
} else {
981
+
bodyHtml = `<div class="comment-text">${text}</div>`;
0
0
0
982
}
983
984
return `
985
+
<div class="comment-item">
986
+
<div class="comment-header">
987
+
${avatarHtml}
988
+
<span class="comment-handle">@${handle}</span>
989
+
</div>
990
+
${bodyHtml}
991
+
<div class="comment-actions">
992
+
${!isHighlight ? `<button class="comment-action-btn btn-reply" data-id="${id}">Reply</button>` : ""}
993
+
<button class="comment-action-btn btn-share" data-id="${id}" data-text="${text}">Share</button>
994
+
</div>
0
0
0
0
995
</div>
996
`;
997
})
···
1032
btn.addEventListener("click", async () => {
1033
const id = btn.getAttribute("data-id");
1034
const text = btn.getAttribute("data-text");
0
1035
const u = `https://margin.at/annotation/${encodeURIComponent(id)}`;
1036
+
const shareText = text ? `${text}\n${u}` : u;
1037
1038
try {
1039
await navigator.clipboard.writeText(shareText);
+166
-166
extension/popup/popup.css
···
0
0
1
:root {
2
-
--bg-primary: #09090b;
3
-
--bg-secondary: #0f0f12;
4
-
--bg-tertiary: #18181b;
5
-
--bg-card: #09090b;
6
-
--bg-elevated: #18181b;
7
-
--bg-hover: #27272a;
8
9
-
--text-primary: #e4e4e7;
10
-
--text-secondary: #a1a1aa;
11
-
--text-tertiary: #71717a;
12
-
--border: #27272a;
13
-
--border-hover: #3f3f46;
14
15
-
--accent: #6366f1;
16
-
--accent-hover: #4f46e5;
17
-
--accent-subtle: rgba(99, 102, 241, 0.1);
18
-
--accent-text: #818cf8;
19
-
--success: #10b981;
20
-
--error: #ef4444;
21
-
--warning: #f59e0b;
0
0
0
0
22
23
-
--radius-sm: 4px;
24
-
--radius-md: 6px;
25
-
--radius-lg: 8px;
26
--radius-full: 9999px;
27
-
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
28
-
--shadow-md:
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;
0
0
0
0
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);
0
0
0
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;
0
0
0
0
0
0
0
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
* {
···
111
body {
112
width: 380px;
113
height: 520px;
114
-
font-family: "Inter", sans-serif;
0
0
0
0
115
color: var(--text-primary);
116
background-color: var(--bg-primary);
117
overflow: hidden;
0
118
}
119
120
.popup {
···
129
display: flex;
130
justify-content: space-between;
131
align-items: center;
132
-
background: var(--bg-secondary);
133
-
z-index: 10;
134
}
135
136
.popup-brand {
···
145
146
.popup-title {
147
font-weight: 600;
148
-
font-size: 16px;
149
color: var(--text-primary);
0
150
}
151
152
.user-info {
···
159
font-size: 12px;
160
color: var(--text-secondary);
161
background: var(--bg-tertiary);
162
-
padding: 4px 8px;
163
-
border-radius: var(--radius-sm);
164
}
165
166
.tabs {
167
display: flex;
168
border-bottom: 1px solid var(--border);
169
-
background: var(--bg-tertiary);
170
-
padding: 4px;
171
gap: 4px;
172
}
173
···
178
border: none;
179
font-size: 12px;
180
font-weight: 500;
181
-
color: var(--text-secondary);
182
cursor: pointer;
183
border-radius: var(--radius-sm);
184
transition: all 0.15s;
185
}
186
187
.tab-btn:hover {
188
-
color: var(--text-primary);
189
background: var(--bg-hover);
190
}
191
192
.tab-btn.active {
193
color: var(--text-primary);
194
-
background: var(--bg-card);
195
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
196
}
197
198
.tab-content {
···
220
align-items: center;
221
justify-content: center;
222
height: 100%;
223
-
color: var(--text-secondary);
224
gap: 12px;
225
}
226
227
.spinner {
228
-
width: 24px;
229
-
height: 24px;
230
-
border: 3px solid var(--border);
231
border-top-color: var(--accent);
232
border-radius: 50%;
233
animation: spin 1s linear infinite;
···
251
}
252
253
.login-at-logo {
254
-
font-size: 4rem;
255
-
font-weight: 800;
256
color: var(--accent);
257
line-height: 1;
258
}
259
260
.login-title {
261
-
font-size: 1.1rem;
262
font-weight: 600;
263
color: var(--text-primary);
264
}
265
266
.login-text {
267
-
font-size: 14px;
268
color: var(--text-secondary);
269
line-height: 1.5;
270
}
···
272
.quick-actions {
273
padding: 12px 16px;
274
border-bottom: 1px solid var(--border);
275
-
background: var(--bg-secondary);
276
}
277
278
.create-form {
279
padding: 16px;
280
border-bottom: 1px solid var(--border);
281
-
background: var(--bg-secondary);
282
}
283
284
.form-header {
···
289
}
290
291
.form-title {
292
-
font-size: 13px;
293
font-weight: 600;
294
color: var(--text-primary);
0
295
}
296
297
.current-url {
···
312
font-size: 13px;
313
resize: none;
314
margin-bottom: 10px;
315
-
background: var(--bg-tertiary);
316
color: var(--text-primary);
317
-
transition:
318
-
border-color 0.15s,
319
-
box-shadow 0.15s;
320
}
321
322
.annotation-input::placeholder {
···
326
.annotation-input:focus {
327
outline: none;
328
border-color: var(--accent);
329
-
box-shadow: 0 0 0 3px var(--accent-subtle);
330
}
331
332
.form-actions {
···
338
margin-bottom: 12px;
339
padding: 10px 12px;
340
background: var(--accent-subtle);
341
-
border: 1px solid var(--accent);
342
border-radius: var(--radius-sm);
343
}
344
···
351
font-weight: 600;
352
text-transform: uppercase;
353
letter-spacing: 0.5px;
354
-
color: var(--accent);
355
}
356
357
.quote-preview-clear {
···
371
.quote-preview-text {
372
font-size: 12px;
373
font-style: italic;
374
-
color: var(--text-primary);
375
line-height: 1.4;
376
max-height: 60px;
377
overflow: hidden;
···
386
justify-content: space-between;
387
align-items: center;
388
padding: 14px 16px;
389
-
background: var(--bg-secondary);
390
}
391
392
.section-title {
···
401
font-size: 11px;
402
background: var(--bg-tertiary);
403
padding: 3px 8px;
404
-
border-radius: 10px;
405
color: var(--text-secondary);
406
}
407
408
.annotations {
409
display: flex;
410
flex-direction: column;
411
-
gap: 10px;
412
-
padding: 12px 16px;
413
}
414
415
.annotation-item {
416
-
border: 1px solid var(--border);
417
-
border-radius: var(--radius-md);
418
-
padding: 12px;
419
-
background: var(--bg-card);
420
-
transition: border-color 0.15s;
421
}
422
423
.annotation-item:hover {
424
-
border-color: var(--border-hover);
425
}
426
427
.annotation-item-header {
···
432
}
433
434
.annotation-item-avatar {
435
-
width: 28px;
436
-
height: 28px;
437
border-radius: 50%;
438
-
background: linear-gradient(135deg, var(--accent), #c084fc);
439
-
color: white;
440
display: flex;
441
align-items: center;
442
justify-content: center;
443
-
font-size: 11px;
444
font-weight: 600;
445
}
446
···
462
.annotation-type-badge {
463
font-size: 10px;
464
padding: 3px 8px;
465
-
border-radius: var(--radius-sm);
466
font-weight: 500;
467
}
468
469
.annotation-type-badge.highlight {
470
-
background: rgba(251, 191, 36, 0.2);
471
-
color: #fbbf24;
472
}
473
474
.annotation-item-quote {
475
-
padding: 10px 12px;
476
-
border-left: 3px solid #fbbf24;
477
-
margin-bottom: 10px;
478
-
font-size: 13px;
479
color: var(--text-secondary);
480
font-style: italic;
481
-
background: rgba(251, 191, 36, 0.1);
482
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
483
}
484
···
489
}
490
491
.bookmark-item {
492
-
border: 1px solid var(--border);
493
-
border-radius: var(--radius-md);
494
-
padding: 12px;
495
-
background: var(--bg-card);
496
text-decoration: none;
497
color: inherit;
498
display: block;
499
-
transition: border-color 0.15s;
500
}
501
502
.bookmark-item:hover {
503
-
border-color: var(--accent);
504
}
505
506
.bookmark-title {
507
-
font-size: 14px;
508
font-weight: 500;
509
margin-bottom: 4px;
510
white-space: nowrap;
···
534
.empty-icon {
535
margin-bottom: 12px;
536
color: var(--text-tertiary);
537
-
opacity: 0.5;
538
}
539
540
.empty-text {
···
568
569
.btn-primary:hover {
570
background: var(--accent-hover);
571
-
transform: translateY(-1px);
572
}
573
574
.btn-secondary {
···
586
.btn-icon {
587
background: none;
588
border: none;
589
-
color: var(--text-secondary);
590
cursor: pointer;
591
padding: 6px;
592
border-radius: var(--radius-sm);
···
599
600
.popup-link {
601
font-size: 12px;
602
-
color: var(--text-secondary);
603
text-decoration: none;
604
}
605
606
.popup-link:hover {
607
-
color: var(--accent);
608
-
text-decoration: underline;
609
}
610
611
.popup-footer {
612
padding: 12px 16px;
613
border-top: 1px solid var(--border);
614
-
background: var(--bg-secondary);
615
}
616
617
.settings-view {
···
653
}
654
655
::-webkit-scrollbar {
656
-
width: 6px;
657
}
658
659
::-webkit-scrollbar-track {
660
-
background: var(--bg-secondary);
661
}
662
663
::-webkit-scrollbar-thumb {
664
-
background: var(--border);
665
-
border-radius: 3px;
666
}
667
668
::-webkit-scrollbar-thumb:hover {
669
-
background: var(--border-hover);
670
}
671
672
.collection-selector {
···
695
align-items: center;
696
gap: 12px;
697
padding: 12px;
698
-
background: var(--bg-card);
699
border: 1px solid var(--border);
700
border-radius: var(--radius-md);
701
color: var(--text-primary);
···
711
}
712
713
.collection-select-btn:disabled {
714
-
opacity: 0.7;
715
cursor: not-allowed;
716
}
717
···
725
.toggle-switch {
726
position: relative;
727
display: inline-block;
728
-
width: 44px;
729
-
height: 24px;
730
flex-shrink: 0;
731
}
732
···
743
left: 0;
744
right: 0;
745
bottom: 0;
746
-
background-color: var(--border);
747
transition: 0.2s;
748
-
border-radius: 24px;
749
}
750
751
.toggle-slider:before {
752
position: absolute;
753
content: "";
754
-
height: 18px;
755
-
width: 18px;
756
left: 3px;
757
bottom: 3px;
758
-
background-color: var(--text-secondary);
759
transition: 0.2s;
760
border-radius: 50%;
761
}
···
765
}
766
767
.toggle-switch input:checked + .toggle-slider:before {
768
-
transform: translateX(20px);
769
background-color: white;
770
}
771
772
.settings-input {
773
width: 100%;
774
padding: 10px 12px;
775
-
background: var(--bg-tertiary);
776
border: 1px solid var(--border);
777
border-radius: var(--radius-md);
778
color: var(--text-primary);
···
783
outline: none;
784
border-color: var(--accent);
785
}
0
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;
···
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);
···
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
}
···
1
+
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap");
2
+
3
:root {
4
+
--bg-primary: #0a0a0d;
5
+
--bg-secondary: #121216;
6
+
--bg-tertiary: #1a1a1f;
7
+
--bg-card: #0f0f13;
8
+
--bg-elevated: #18181d;
9
+
--bg-hover: #1e1e24;
10
11
+
--text-primary: #eaeaee;
12
+
--text-secondary: #b7b6c5;
13
+
--text-tertiary: #6e6d7a;
0
0
14
15
+
--border: rgba(183, 182, 197, 0.12);
16
+
--border-hover: rgba(183, 182, 197, 0.2);
17
+
18
+
--accent: #957a86;
19
+
--accent-hover: #a98d98;
20
+
--accent-subtle: rgba(149, 122, 134, 0.15);
21
+
--accent-text: #c4a8b2;
22
+
23
+
--success: #7fb069;
24
+
--error: #d97766;
25
+
--warning: #e8a54b;
26
27
+
--radius-sm: 6px;
28
+
--radius-md: 8px;
29
+
--radius-lg: 12px;
30
--radius-full: 9999px;
31
+
32
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
33
+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
34
}
35
36
@media (prefers-color-scheme: light) {
37
:root {
38
+
--bg-primary: #f8f8fa;
39
+
--bg-secondary: #ffffff;
40
+
--bg-tertiary: #f0f0f4;
41
--bg-card: #ffffff;
42
+
--bg-elevated: #ffffff;
43
+
--bg-hover: #eeeef2;
44
45
+
--text-primary: #18171c;
46
+
--text-secondary: #5c495a;
47
+
--text-tertiary: #8a8494;
0
0
48
49
+
--border: rgba(92, 73, 90, 0.12);
50
+
--border-hover: rgba(92, 73, 90, 0.22);
0
0
51
52
+
--accent: #7a5f6d;
53
+
--accent-hover: #664e5b;
54
+
--accent-subtle: rgba(149, 122, 134, 0.12);
55
+
--accent-text: #5c495a;
56
+
57
+
--shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06);
58
+
--shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08);
59
}
60
}
61
62
body.light {
63
+
--bg-primary: #f8f8fa;
64
+
--bg-secondary: #ffffff;
65
+
--bg-tertiary: #f0f0f4;
66
--bg-card: #ffffff;
67
+
--bg-elevated: #ffffff;
68
+
--bg-hover: #eeeef2;
69
70
+
--text-primary: #18171c;
71
+
--text-secondary: #5c495a;
72
+
--text-tertiary: #8a8494;
0
0
73
74
+
--border: rgba(92, 73, 90, 0.12);
75
+
--border-hover: rgba(92, 73, 90, 0.22);
76
+
77
+
--accent: #7a5f6d;
78
+
--accent-hover: #664e5b;
79
+
--accent-subtle: rgba(149, 122, 134, 0.12);
80
+
--accent-text: #5c495a;
81
82
+
--shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06);
83
+
--shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08);
0
84
}
85
86
body.dark {
87
+
--bg-primary: #0a0a0d;
88
+
--bg-secondary: #121216;
89
+
--bg-tertiary: #1a1a1f;
90
+
--bg-card: #0f0f13;
91
+
--bg-elevated: #18181d;
92
+
--bg-hover: #1e1e24;
93
+
94
+
--text-primary: #eaeaee;
95
+
--text-secondary: #b7b6c5;
96
+
--text-tertiary: #6e6d7a;
97
+
98
+
--border: rgba(183, 182, 197, 0.12);
99
+
--border-hover: rgba(183, 182, 197, 0.2);
100
101
+
--accent: #957a86;
102
+
--accent-hover: #a98d98;
103
+
--accent-subtle: rgba(149, 122, 134, 0.15);
104
+
--accent-text: #c4a8b2;
0
105
106
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
107
+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
0
0
0
0
0
108
}
109
110
* {
···
116
body {
117
width: 380px;
118
height: 520px;
119
+
font-family:
120
+
"IBM Plex Sans",
121
+
-apple-system,
122
+
BlinkMacSystemFont,
123
+
sans-serif;
124
color: var(--text-primary);
125
background-color: var(--bg-primary);
126
overflow: hidden;
127
+
-webkit-font-smoothing: antialiased;
128
}
129
130
.popup {
···
139
display: flex;
140
justify-content: space-between;
141
align-items: center;
142
+
background: var(--bg-primary);
0
143
}
144
145
.popup-brand {
···
154
155
.popup-title {
156
font-weight: 600;
157
+
font-size: 15px;
158
color: var(--text-primary);
159
+
letter-spacing: -0.02em;
160
}
161
162
.user-info {
···
169
font-size: 12px;
170
color: var(--text-secondary);
171
background: var(--bg-tertiary);
172
+
padding: 4px 10px;
173
+
border-radius: var(--radius-full);
174
}
175
176
.tabs {
177
display: flex;
178
border-bottom: 1px solid var(--border);
179
+
background: var(--bg-primary);
180
+
padding: 4px 8px;
181
gap: 4px;
182
}
183
···
188
border: none;
189
font-size: 12px;
190
font-weight: 500;
191
+
color: var(--text-tertiary);
192
cursor: pointer;
193
border-radius: var(--radius-sm);
194
transition: all 0.15s;
195
}
196
197
.tab-btn:hover {
198
+
color: var(--text-secondary);
199
background: var(--bg-hover);
200
}
201
202
.tab-btn.active {
203
color: var(--text-primary);
204
+
background: var(--bg-tertiary);
0
205
}
206
207
.tab-content {
···
229
align-items: center;
230
justify-content: center;
231
height: 100%;
232
+
color: var(--text-tertiary);
233
gap: 12px;
234
}
235
236
.spinner {
237
+
width: 20px;
238
+
height: 20px;
239
+
border: 2px solid var(--border);
240
border-top-color: var(--accent);
241
border-radius: 50%;
242
animation: spin 1s linear infinite;
···
260
}
261
262
.login-at-logo {
263
+
font-size: 3.5rem;
264
+
font-weight: 700;
265
color: var(--accent);
266
line-height: 1;
267
}
268
269
.login-title {
270
+
font-size: 1rem;
271
font-weight: 600;
272
color: var(--text-primary);
273
}
274
275
.login-text {
276
+
font-size: 13px;
277
color: var(--text-secondary);
278
line-height: 1.5;
279
}
···
281
.quick-actions {
282
padding: 12px 16px;
283
border-bottom: 1px solid var(--border);
284
+
background: var(--bg-primary);
285
}
286
287
.create-form {
288
padding: 16px;
289
border-bottom: 1px solid var(--border);
290
+
background: var(--bg-primary);
291
}
292
293
.form-header {
···
298
}
299
300
.form-title {
301
+
font-size: 12px;
302
font-weight: 600;
303
color: var(--text-primary);
304
+
letter-spacing: -0.01em;
305
}
306
307
.current-url {
···
322
font-size: 13px;
323
resize: none;
324
margin-bottom: 10px;
325
+
background: var(--bg-elevated);
326
color: var(--text-primary);
327
+
transition: border-color 0.15s;
0
0
328
}
329
330
.annotation-input::placeholder {
···
334
.annotation-input:focus {
335
outline: none;
336
border-color: var(--accent);
0
337
}
338
339
.form-actions {
···
345
margin-bottom: 12px;
346
padding: 10px 12px;
347
background: var(--accent-subtle);
348
+
border-left: 2px solid var(--accent);
349
border-radius: var(--radius-sm);
350
}
351
···
358
font-weight: 600;
359
text-transform: uppercase;
360
letter-spacing: 0.5px;
361
+
color: var(--accent-text);
362
}
363
364
.quote-preview-clear {
···
378
.quote-preview-text {
379
font-size: 12px;
380
font-style: italic;
381
+
color: var(--text-secondary);
382
line-height: 1.4;
383
max-height: 60px;
384
overflow: hidden;
···
393
justify-content: space-between;
394
align-items: center;
395
padding: 14px 16px;
396
+
background: var(--bg-primary);
397
}
398
399
.section-title {
···
408
font-size: 11px;
409
background: var(--bg-tertiary);
410
padding: 3px 8px;
411
+
border-radius: var(--radius-full);
412
color: var(--text-secondary);
413
}
414
415
.annotations {
416
display: flex;
417
flex-direction: column;
418
+
gap: 1px;
419
+
background: var(--border);
420
}
421
422
.annotation-item {
423
+
padding: 14px 16px;
424
+
background: var(--bg-primary);
425
+
transition: background 0.15s;
0
0
426
}
427
428
.annotation-item:hover {
429
+
background: var(--bg-hover);
430
}
431
432
.annotation-item-header {
···
437
}
438
439
.annotation-item-avatar {
440
+
width: 26px;
441
+
height: 26px;
442
border-radius: 50%;
443
+
background: var(--accent);
444
+
color: var(--bg-primary);
445
display: flex;
446
align-items: center;
447
justify-content: center;
448
+
font-size: 10px;
449
font-weight: 600;
450
}
451
···
467
.annotation-type-badge {
468
font-size: 10px;
469
padding: 3px 8px;
470
+
border-radius: var(--radius-full);
471
font-weight: 500;
472
}
473
474
.annotation-type-badge.highlight {
475
+
background: var(--accent-subtle);
476
+
color: var(--accent-text);
477
}
478
479
.annotation-item-quote {
480
+
padding: 8px 12px;
481
+
border-left: 2px solid var(--accent);
482
+
margin-bottom: 8px;
483
+
font-size: 12px;
484
color: var(--text-secondary);
485
font-style: italic;
486
+
background: var(--accent-subtle);
487
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
488
}
489
···
494
}
495
496
.bookmark-item {
497
+
padding: 14px 16px;
498
+
background: var(--bg-primary);
0
0
499
text-decoration: none;
500
color: inherit;
501
display: block;
502
+
transition: background 0.15s;
503
}
504
505
.bookmark-item:hover {
506
+
background: var(--bg-hover);
507
}
508
509
.bookmark-title {
510
+
font-size: 13px;
511
font-weight: 500;
512
margin-bottom: 4px;
513
white-space: nowrap;
···
537
.empty-icon {
538
margin-bottom: 12px;
539
color: var(--text-tertiary);
540
+
opacity: 0.4;
541
}
542
543
.empty-text {
···
571
572
.btn-primary:hover {
573
background: var(--accent-hover);
0
574
}
575
576
.btn-secondary {
···
588
.btn-icon {
589
background: none;
590
border: none;
591
+
color: var(--text-tertiary);
592
cursor: pointer;
593
padding: 6px;
594
border-radius: var(--radius-sm);
···
601
602
.popup-link {
603
font-size: 12px;
604
+
color: var(--text-tertiary);
605
text-decoration: none;
606
}
607
608
.popup-link:hover {
609
+
color: var(--accent-text);
0
610
}
611
612
.popup-footer {
613
padding: 12px 16px;
614
border-top: 1px solid var(--border);
615
+
background: var(--bg-primary);
616
}
617
618
.settings-view {
···
654
}
655
656
::-webkit-scrollbar {
657
+
width: 8px;
658
}
659
660
::-webkit-scrollbar-track {
661
+
background: transparent;
662
}
663
664
::-webkit-scrollbar-thumb {
665
+
background: var(--bg-hover);
666
+
border-radius: var(--radius-full);
667
}
668
669
::-webkit-scrollbar-thumb:hover {
670
+
background: var(--text-tertiary);
671
}
672
673
.collection-selector {
···
696
align-items: center;
697
gap: 12px;
698
padding: 12px;
699
+
background: var(--bg-primary);
700
border: 1px solid var(--border);
701
border-radius: var(--radius-md);
702
color: var(--text-primary);
···
712
}
713
714
.collection-select-btn:disabled {
715
+
opacity: 0.6;
716
cursor: not-allowed;
717
}
718
···
726
.toggle-switch {
727
position: relative;
728
display: inline-block;
729
+
width: 40px;
730
+
height: 22px;
731
flex-shrink: 0;
732
}
733
···
744
left: 0;
745
right: 0;
746
bottom: 0;
747
+
background-color: var(--bg-tertiary);
748
transition: 0.2s;
749
+
border-radius: 22px;
750
}
751
752
.toggle-slider:before {
753
position: absolute;
754
content: "";
755
+
height: 16px;
756
+
width: 16px;
757
left: 3px;
758
bottom: 3px;
759
+
background-color: var(--text-tertiary);
760
transition: 0.2s;
761
border-radius: 50%;
762
}
···
766
}
767
768
.toggle-switch input:checked + .toggle-slider:before {
769
+
transform: translateX(18px);
770
background-color: white;
771
}
772
773
.settings-input {
774
width: 100%;
775
padding: 10px 12px;
776
+
background: var(--bg-elevated);
777
border: 1px solid var(--border);
778
border-radius: var(--radius-md);
779
color: var(--text-primary);
···
784
outline: none;
785
border-color: var(--accent);
786
}
787
+
788
.theme-toggle-group {
789
display: flex;
790
background: var(--bg-tertiary);
791
+
padding: 3px;
792
border-radius: var(--radius-md);
793
gap: 2px;
794
margin-top: 8px;
···
799
padding: 6px;
800
border: none;
801
background: transparent;
802
+
color: var(--text-tertiary);
803
font-size: 12px;
804
font-weight: 500;
805
border-radius: var(--radius-sm);
···
808
}
809
810
.theme-btn:hover {
811
+
color: var(--text-secondary);
0
812
}
813
814
.theme-btn.active {
815
+
background: var(--bg-primary);
816
color: var(--text-primary);
0
817
}
+164
-327
extension/sidepanel/sidepanel.css
···
0
0
1
:root {
2
-
--bg-primary: #09090b;
3
-
--bg-secondary: #0f0f12;
4
-
--bg-tertiary: #18181b;
5
-
--bg-card: #09090b;
6
-
--bg-hover: #18181b;
7
-
--bg-elevated: #18181b;
8
9
-
--text-primary: #e4e4e7;
10
-
--text-secondary: #a1a1aa;
11
-
--text-tertiary: #71717a;
12
13
-
--accent: #6366f1;
14
-
--accent-hover: #4f46e5;
15
-
--accent-subtle: rgba(99, 102, 241, 0.1);
16
-
--accent-text: #818cf8;
17
18
-
--border: #27272a;
19
-
--border-hover: #3f3f46;
0
0
20
21
-
--success: #10b981;
22
-
--error: #ef4444;
23
-
--warning: #f59e0b;
24
25
-
--radius-sm: 4px;
26
-
--radius-md: 6px;
27
-
--radius-lg: 8px;
28
--radius-full: 9999px;
29
30
-
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
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;
0
0
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;
0
0
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;
0
0
105
106
-
--success: #10b981;
107
-
--error: #ef4444;
108
-
--warning: #f59e0b;
109
}
110
111
* {
···
116
117
body {
118
font-family:
119
-
"Inter",
120
-apple-system,
121
BlinkMacSystemFont,
122
-
"Segoe UI",
123
sans-serif;
124
background: var(--bg-primary);
125
color: var(--text-primary);
···
143
background: var(--bg-primary);
144
}
145
146
-
.user-handle {
147
-
font-size: 12px;
148
-
color: var(--text-secondary);
149
-
background: var(--bg-tertiary);
150
-
padding: 4px 8px;
151
-
border-radius: var(--radius-sm);
152
-
}
153
-
154
-
.current-page-info {
155
-
display: flex;
156
-
align-items: center;
157
-
gap: 8px;
158
-
padding: 10px 16px;
159
-
background: var(--bg-primary);
160
-
border-bottom: 1px solid var(--border);
161
-
}
162
-
163
-
.tabs {
164
-
display: flex;
165
-
border-bottom: 1px solid var(--border);
166
-
background: var(--bg-primary);
167
-
padding: 4px;
168
-
gap: 4px;
169
-
margin: 0;
170
-
}
171
-
172
-
.tab-btn {
173
-
flex: 1;
174
-
padding: 10px 8px;
175
-
background: transparent;
176
-
border: none;
177
-
font-size: 12px;
178
-
font-weight: 500;
179
-
color: var(--text-secondary);
180
-
cursor: pointer;
181
-
border-radius: var(--radius-sm);
182
-
transition: all 0.15s;
183
-
}
184
-
185
-
.tab-btn:hover {
186
-
color: var(--text-primary);
187
-
background: var(--bg-hover);
188
-
}
189
-
190
-
.tab-btn.active {
191
-
color: var(--text-primary);
192
-
background: var(--bg-tertiary);
193
-
box-shadow: none;
194
-
}
195
-
196
-
.quick-actions {
197
-
display: flex;
198
-
gap: 8px;
199
-
padding: 12px 16px;
200
-
border-bottom: 1px solid var(--border);
201
-
background: var(--bg-primary);
202
-
}
203
-
204
-
.create-form {
205
-
padding: 16px;
206
-
border-bottom: 1px solid var(--border);
207
-
background: var(--bg-primary);
208
-
}
209
-
210
-
.section-header {
211
-
display: flex;
212
-
justify-content: space-between;
213
-
align-items: center;
214
-
padding: 14px 16px;
215
-
background: var(--bg-primary);
216
-
border-bottom: 1px solid var(--border);
217
-
}
218
-
219
-
.annotation-item {
220
-
border: 1px solid var(--border);
221
-
border-radius: var(--radius-md);
222
-
padding: 12px;
223
-
background: var(--bg-primary);
224
-
transition: border-color 0.15s;
225
-
}
226
-
227
-
.annotation-item:hover {
228
-
border-color: var(--border-hover);
229
-
background: var(--bg-hover);
230
-
}
231
-
232
-
.sidebar-footer {
233
-
display: flex;
234
-
align-items: center;
235
-
justify-content: space-between;
236
-
padding: 12px 16px;
237
-
border-top: 1px solid var(--border);
238
-
background: var(--bg-primary);
239
-
}
240
-
241
-
::-webkit-scrollbar {
242
-
width: 10px;
243
-
height: 10px;
244
-
}
245
-
246
-
::-webkit-scrollbar-track {
247
-
background: transparent;
248
-
}
249
-
250
-
::-webkit-scrollbar-thumb {
251
-
background: var(--border);
252
-
border-radius: 5px;
253
-
border: 2px solid var(--bg-primary);
254
-
}
255
-
256
-
::-webkit-scrollbar-thumb:hover {
257
-
background: var(--border-hover);
258
-
}
259
-
260
-
* {
261
-
margin: 0;
262
-
padding: 0;
263
-
box-sizing: border-box;
264
-
}
265
-
266
-
body {
267
-
font-family:
268
-
"Inter",
269
-
-apple-system,
270
-
BlinkMacSystemFont,
271
-
"Segoe UI",
272
-
sans-serif;
273
-
background: var(--bg-primary);
274
-
color: var(--text-primary);
275
-
min-height: 100vh;
276
-
-webkit-font-smoothing: antialiased;
277
-
}
278
-
279
-
.sidebar {
280
-
display: flex;
281
-
flex-direction: column;
282
-
height: 100vh;
283
-
background: var(--bg-primary);
284
-
}
285
-
286
-
.sidebar-header {
287
-
display: flex;
288
-
align-items: center;
289
-
justify-content: space-between;
290
-
padding: 14px 16px;
291
-
border-bottom: 1px solid var(--border);
292
-
background: var(--bg-secondary);
293
-
}
294
-
295
.sidebar-brand {
296
display: flex;
297
align-items: center;
···
304
305
.sidebar-title {
306
font-weight: 600;
307
-
font-size: 16px;
308
color: var(--text-primary);
0
309
}
310
311
.user-info {
···
318
font-size: 12px;
319
color: var(--text-secondary);
320
background: var(--bg-tertiary);
321
-
padding: 4px 8px;
322
-
border-radius: var(--radius-sm);
323
}
324
325
.current-page-info {
···
327
align-items: center;
328
gap: 8px;
329
padding: 10px 16px;
330
-
background: var(--bg-tertiary);
331
border-bottom: 1px solid var(--border);
332
}
333
334
.page-url {
335
font-size: 12px;
336
-
color: var(--text-secondary);
337
white-space: nowrap;
338
overflow: hidden;
339
text-overflow: ellipsis;
···
352
align-items: center;
353
justify-content: center;
354
height: 100%;
355
-
color: var(--text-secondary);
356
gap: 12px;
357
}
358
359
.spinner {
360
-
width: 24px;
361
-
height: 24px;
362
-
border: 3px solid var(--border);
363
border-top-color: var(--accent);
364
border-radius: 50%;
365
animation: spin 1s linear infinite;
···
383
}
384
385
.login-at-logo {
386
-
font-size: 4rem;
387
-
font-weight: 800;
388
color: var(--accent);
389
line-height: 1;
390
}
391
392
.login-title {
393
-
font-size: 1.1rem;
394
font-weight: 600;
395
color: var(--text-primary);
396
}
397
398
.login-text {
399
-
font-size: 14px;
400
color: var(--text-secondary);
401
line-height: 1.5;
402
}
···
404
.tabs {
405
display: flex;
406
border-bottom: 1px solid var(--border);
407
-
background: var(--bg-tertiary);
408
-
padding: 4px;
409
gap: 4px;
410
margin: 0;
411
}
···
417
border: none;
418
font-size: 12px;
419
font-weight: 500;
420
-
color: var(--text-secondary);
421
cursor: pointer;
422
border-radius: var(--radius-sm);
423
transition: all 0.15s;
424
}
425
426
.tab-btn:hover {
427
-
color: var(--text-primary);
428
background: var(--bg-hover);
429
}
430
431
.tab-btn.active {
432
color: var(--text-primary);
433
-
background: var(--bg-card);
434
-
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
435
}
436
437
.tab-content {
···
450
gap: 8px;
451
padding: 12px 16px;
452
border-bottom: 1px solid var(--border);
453
-
background: var(--bg-secondary);
454
}
455
456
.btn {
···
479
480
.btn-primary:hover {
481
background: var(--accent-hover);
482
-
transform: translateY(-1px);
483
}
484
485
.btn-primary:disabled {
486
opacity: 0.5;
487
cursor: not-allowed;
488
-
transform: none;
489
}
490
491
.btn-secondary {
···
507
.btn-icon {
508
background: none;
509
border: none;
510
-
color: var(--text-secondary);
511
cursor: pointer;
512
padding: 6px;
513
border-radius: var(--radius-sm);
···
521
.create-form {
522
padding: 16px;
523
border-bottom: 1px solid var(--border);
524
-
background: var(--bg-secondary);
525
}
526
527
.form-header {
···
532
}
533
534
.form-title {
535
-
font-size: 13px;
536
font-weight: 600;
537
color: var(--text-primary);
0
538
}
539
540
.annotation-input {
···
546
font-size: 13px;
547
resize: none;
548
margin-bottom: 10px;
549
-
background: var(--bg-tertiary);
550
color: var(--text-primary);
551
-
transition:
552
-
border-color 0.15s,
553
-
box-shadow 0.15s;
554
}
555
556
.annotation-input::placeholder {
···
560
.annotation-input:focus {
561
outline: none;
562
border-color: var(--accent);
563
-
box-shadow: 0 0 0 3px var(--accent-subtle);
564
}
565
566
.form-actions {
···
572
margin-bottom: 12px;
573
padding: 12px;
574
background: var(--accent-subtle);
575
-
border: 1px solid var(--accent);
576
-
border-radius: var(--radius-md);
577
}
578
579
.quote-preview-header {
···
581
justify-content: space-between;
582
align-items: center;
583
margin-bottom: 8px;
584
-
font-size: 11px;
585
font-weight: 600;
586
text-transform: uppercase;
587
letter-spacing: 0.5px;
588
-
color: var(--accent);
589
}
590
591
.quote-preview-clear {
···
603
}
604
605
.quote-preview-text {
606
-
font-size: 13px;
607
font-style: italic;
608
-
color: var(--text-primary);
609
line-height: 1.5;
610
}
611
···
618
justify-content: space-between;
619
align-items: center;
620
padding: 14px 16px;
621
-
background: var(--bg-secondary);
0
622
}
623
624
.section-title {
···
633
font-size: 11px;
634
background: var(--bg-tertiary);
635
padding: 3px 8px;
636
-
border-radius: 10px;
637
color: var(--text-secondary);
638
}
639
640
.annotations-list {
641
display: flex;
642
flex-direction: column;
643
-
gap: 10px;
644
-
padding: 12px 16px;
645
}
646
647
.annotation-item {
648
-
border: 1px solid var(--border);
649
-
border-radius: var(--radius-md);
650
-
padding: 12px;
651
-
background: var(--bg-card);
652
-
transition: border-color 0.15s;
653
}
654
655
.annotation-item:hover {
656
-
border-color: var(--border-hover);
657
}
658
659
.annotation-item-header {
···
664
}
665
666
.annotation-item-avatar {
667
-
width: 28px;
668
-
height: 28px;
669
border-radius: 50%;
670
-
background: linear-gradient(135deg, var(--accent), #c084fc);
671
-
color: white;
672
display: flex;
673
align-items: center;
674
justify-content: center;
675
-
font-size: 11px;
676
font-weight: 600;
677
}
678
···
694
.annotation-type-badge {
695
font-size: 10px;
696
padding: 3px 8px;
697
-
border-radius: var(--radius-sm);
698
font-weight: 500;
699
}
700
701
.annotation-type-badge.highlight {
702
-
background: rgba(251, 191, 36, 0.2);
703
-
color: #fbbf24;
704
}
705
706
.annotation-item-quote {
707
-
padding: 10px 12px;
708
-
border-left: 3px solid #fbbf24;
709
-
margin-bottom: 10px;
710
-
font-size: 13px;
711
color: var(--text-secondary);
712
font-style: italic;
713
-
background: rgba(251, 191, 36, 0.1);
714
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
715
}
716
···
723
.bookmarks-list {
724
display: flex;
725
flex-direction: column;
726
-
gap: 10px;
727
-
padding: 12px 16px;
728
}
729
730
.bookmark-item {
731
-
border: 1px solid var(--border);
732
-
border-radius: var(--radius-md);
733
-
padding: 12px;
734
-
background: var(--bg-card);
735
text-decoration: none;
736
color: inherit;
737
display: block;
738
-
transition: border-color 0.15s;
739
}
740
741
.bookmark-item:hover {
742
-
border-color: var(--accent);
743
}
744
745
.bookmark-title {
746
-
font-size: 14px;
747
font-weight: 500;
748
margin-bottom: 4px;
749
white-space: nowrap;
···
773
.empty-icon {
774
margin-bottom: 12px;
775
color: var(--text-tertiary);
776
-
opacity: 0.5;
777
}
778
779
.empty-text {
···
793
justify-content: space-between;
794
padding: 12px 16px;
795
border-top: 1px solid var(--border);
796
-
background: var(--bg-secondary);
797
}
798
799
.sidebar-link {
800
font-size: 12px;
801
-
color: var(--text-secondary);
802
text-decoration: none;
803
}
804
805
.sidebar-link:hover {
806
-
color: var(--accent);
807
-
text-decoration: underline;
808
}
809
810
.settings-view {
···
852
border-radius: var(--radius-md);
853
font-family: inherit;
854
font-size: 13px;
855
-
background: var(--bg-tertiary);
856
color: var(--text-primary);
857
-
transition:
858
-
border-color 0.15s,
859
-
box-shadow 0.15s;
860
}
861
862
.settings-input:focus {
863
outline: none;
864
border-color: var(--accent);
865
-
box-shadow: 0 0 0 3px var(--accent-subtle);
866
}
867
868
.setting-help {
···
877
gap: 4px;
878
padding: 6px 10px;
879
font-size: 11px;
880
-
color: var(--accent);
881
background: var(--accent-subtle);
882
border: none;
883
border-radius: var(--radius-sm);
···
887
}
888
889
.scroll-to-btn:hover {
890
-
background: rgba(168, 85, 247, 0.25);
891
}
892
893
::-webkit-scrollbar {
894
-
width: 6px;
895
}
896
897
::-webkit-scrollbar-track {
898
-
background: var(--bg-secondary);
899
}
900
901
::-webkit-scrollbar-thumb {
902
-
background: var(--border);
903
-
border-radius: 3px;
904
}
905
906
::-webkit-scrollbar-thumb:hover {
907
-
background: var(--border-hover);
908
}
909
910
.collection-selector {
···
933
align-items: center;
934
gap: 12px;
935
padding: 12px;
936
-
background: var(--bg-card);
937
border: 1px solid var(--border);
938
border-radius: var(--radius-md);
939
color: var(--text-primary);
···
949
}
950
951
.collection-select-btn:disabled {
952
-
opacity: 0.7;
953
cursor: not-allowed;
954
}
955
···
963
.toggle-switch {
964
position: relative;
965
display: inline-block;
966
-
width: 44px;
967
-
height: 24px;
968
flex-shrink: 0;
969
}
970
···
981
left: 0;
982
right: 0;
983
bottom: 0;
984
-
background-color: var(--border);
985
transition: 0.2s;
986
-
border-radius: 24px;
987
}
988
989
.toggle-slider:before {
990
position: absolute;
991
content: "";
992
-
height: 18px;
993
-
width: 18px;
994
left: 3px;
995
bottom: 3px;
996
-
background-color: var(--text-secondary);
997
transition: 0.2s;
998
border-radius: 50%;
999
}
···
1003
}
1004
1005
.toggle-switch input:checked + .toggle-slider:before {
1006
-
transform: translateX(20px);
1007
background-color: white;
1008
}
0
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;
···
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);
···
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
}
···
1
+
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap");
2
+
3
:root {
4
+
--bg-primary: #0a0a0d;
5
+
--bg-secondary: #121216;
6
+
--bg-tertiary: #1a1a1f;
7
+
--bg-card: #0f0f13;
8
+
--bg-elevated: #18181d;
9
+
--bg-hover: #1e1e24;
10
11
+
--text-primary: #eaeaee;
12
+
--text-secondary: #b7b6c5;
13
+
--text-tertiary: #6e6d7a;
14
15
+
--border: rgba(183, 182, 197, 0.12);
16
+
--border-hover: rgba(183, 182, 197, 0.2);
0
0
17
18
+
--accent: #957a86;
19
+
--accent-hover: #a98d98;
20
+
--accent-subtle: rgba(149, 122, 134, 0.15);
21
+
--accent-text: #c4a8b2;
22
23
+
--success: #7fb069;
24
+
--error: #d97766;
25
+
--warning: #e8a54b;
26
27
+
--radius-sm: 6px;
28
+
--radius-md: 8px;
29
+
--radius-lg: 12px;
30
--radius-full: 9999px;
31
32
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
33
+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
34
}
35
36
@media (prefers-color-scheme: light) {
37
:root {
38
+
--bg-primary: #f8f8fa;
39
+
--bg-secondary: #ffffff;
40
+
--bg-tertiary: #f0f0f4;
41
--bg-card: #ffffff;
42
+
--bg-elevated: #ffffff;
43
+
--bg-hover: #eeeef2;
44
45
+
--text-primary: #18171c;
46
+
--text-secondary: #5c495a;
47
+
--text-tertiary: #8a8494;
48
49
+
--border: rgba(92, 73, 90, 0.12);
50
+
--border-hover: rgba(92, 73, 90, 0.22);
0
0
51
52
+
--accent: #7a5f6d;
53
+
--accent-hover: #664e5b;
54
+
--accent-subtle: rgba(149, 122, 134, 0.12);
55
+
--accent-text: #5c495a;
56
57
+
--shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06);
58
+
--shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08);
0
59
}
60
}
61
62
body.light {
63
+
--bg-primary: #f8f8fa;
64
+
--bg-secondary: #ffffff;
65
+
--bg-tertiary: #f0f0f4;
66
--bg-card: #ffffff;
67
+
--bg-elevated: #ffffff;
68
+
--bg-hover: #eeeef2;
69
70
+
--text-primary: #18171c;
71
+
--text-secondary: #5c495a;
72
+
--text-tertiary: #8a8494;
73
74
+
--border: rgba(92, 73, 90, 0.12);
75
+
--border-hover: rgba(92, 73, 90, 0.22);
0
0
76
77
+
--accent: #7a5f6d;
78
+
--accent-hover: #664e5b;
79
+
--accent-subtle: rgba(149, 122, 134, 0.12);
80
+
--accent-text: #5c495a;
81
82
+
--shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06);
83
+
--shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08);
0
84
}
85
86
body.dark {
87
+
--bg-primary: #0a0a0d;
88
+
--bg-secondary: #121216;
89
+
--bg-tertiary: #1a1a1f;
90
+
--bg-card: #0f0f13;
91
+
--bg-elevated: #18181d;
92
+
--bg-hover: #1e1e24;
93
94
+
--text-primary: #eaeaee;
95
+
--text-secondary: #b7b6c5;
96
+
--text-tertiary: #6e6d7a;
97
98
+
--border: rgba(183, 182, 197, 0.12);
99
+
--border-hover: rgba(183, 182, 197, 0.2);
0
0
100
101
+
--accent: #957a86;
102
+
--accent-hover: #a98d98;
103
+
--accent-subtle: rgba(149, 122, 134, 0.15);
104
+
--accent-text: #c4a8b2;
105
106
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
107
+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
0
108
}
109
110
* {
···
115
116
body {
117
font-family:
118
+
"IBM Plex Sans",
119
-apple-system,
120
BlinkMacSystemFont,
0
121
sans-serif;
122
background: var(--bg-primary);
123
color: var(--text-primary);
···
141
background: var(--bg-primary);
142
}
143
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
144
.sidebar-brand {
145
display: flex;
146
align-items: center;
···
153
154
.sidebar-title {
155
font-weight: 600;
156
+
font-size: 15px;
157
color: var(--text-primary);
158
+
letter-spacing: -0.02em;
159
}
160
161
.user-info {
···
168
font-size: 12px;
169
color: var(--text-secondary);
170
background: var(--bg-tertiary);
171
+
padding: 4px 10px;
172
+
border-radius: var(--radius-full);
173
}
174
175
.current-page-info {
···
177
align-items: center;
178
gap: 8px;
179
padding: 10px 16px;
180
+
background: var(--bg-primary);
181
border-bottom: 1px solid var(--border);
182
}
183
184
.page-url {
185
font-size: 12px;
186
+
color: var(--text-tertiary);
187
white-space: nowrap;
188
overflow: hidden;
189
text-overflow: ellipsis;
···
202
align-items: center;
203
justify-content: center;
204
height: 100%;
205
+
color: var(--text-tertiary);
206
gap: 12px;
207
}
208
209
.spinner {
210
+
width: 20px;
211
+
height: 20px;
212
+
border: 2px solid var(--border);
213
border-top-color: var(--accent);
214
border-radius: 50%;
215
animation: spin 1s linear infinite;
···
233
}
234
235
.login-at-logo {
236
+
font-size: 3.5rem;
237
+
font-weight: 700;
238
color: var(--accent);
239
line-height: 1;
240
}
241
242
.login-title {
243
+
font-size: 1rem;
244
font-weight: 600;
245
color: var(--text-primary);
246
}
247
248
.login-text {
249
+
font-size: 13px;
250
color: var(--text-secondary);
251
line-height: 1.5;
252
}
···
254
.tabs {
255
display: flex;
256
border-bottom: 1px solid var(--border);
257
+
background: var(--bg-primary);
258
+
padding: 4px 8px;
259
gap: 4px;
260
margin: 0;
261
}
···
267
border: none;
268
font-size: 12px;
269
font-weight: 500;
270
+
color: var(--text-tertiary);
271
cursor: pointer;
272
border-radius: var(--radius-sm);
273
transition: all 0.15s;
274
}
275
276
.tab-btn:hover {
277
+
color: var(--text-secondary);
278
background: var(--bg-hover);
279
}
280
281
.tab-btn.active {
282
color: var(--text-primary);
283
+
background: var(--bg-tertiary);
0
284
}
285
286
.tab-content {
···
299
gap: 8px;
300
padding: 12px 16px;
301
border-bottom: 1px solid var(--border);
302
+
background: var(--bg-primary);
303
}
304
305
.btn {
···
328
329
.btn-primary:hover {
330
background: var(--accent-hover);
0
331
}
332
333
.btn-primary:disabled {
334
opacity: 0.5;
335
cursor: not-allowed;
0
336
}
337
338
.btn-secondary {
···
354
.btn-icon {
355
background: none;
356
border: none;
357
+
color: var(--text-tertiary);
358
cursor: pointer;
359
padding: 6px;
360
border-radius: var(--radius-sm);
···
368
.create-form {
369
padding: 16px;
370
border-bottom: 1px solid var(--border);
371
+
background: var(--bg-primary);
372
}
373
374
.form-header {
···
379
}
380
381
.form-title {
382
+
font-size: 12px;
383
font-weight: 600;
384
color: var(--text-primary);
385
+
letter-spacing: -0.01em;
386
}
387
388
.annotation-input {
···
394
font-size: 13px;
395
resize: none;
396
margin-bottom: 10px;
397
+
background: var(--bg-elevated);
398
color: var(--text-primary);
399
+
transition: border-color 0.15s;
0
0
400
}
401
402
.annotation-input::placeholder {
···
406
.annotation-input:focus {
407
outline: none;
408
border-color: var(--accent);
0
409
}
410
411
.form-actions {
···
417
margin-bottom: 12px;
418
padding: 12px;
419
background: var(--accent-subtle);
420
+
border-left: 2px solid var(--accent);
421
+
border-radius: var(--radius-sm);
422
}
423
424
.quote-preview-header {
···
426
justify-content: space-between;
427
align-items: center;
428
margin-bottom: 8px;
429
+
font-size: 10px;
430
font-weight: 600;
431
text-transform: uppercase;
432
letter-spacing: 0.5px;
433
+
color: var(--accent-text);
434
}
435
436
.quote-preview-clear {
···
448
}
449
450
.quote-preview-text {
451
+
font-size: 12px;
452
font-style: italic;
453
+
color: var(--text-secondary);
454
line-height: 1.5;
455
}
456
···
463
justify-content: space-between;
464
align-items: center;
465
padding: 14px 16px;
466
+
background: var(--bg-primary);
467
+
border-bottom: 1px solid var(--border);
468
}
469
470
.section-title {
···
479
font-size: 11px;
480
background: var(--bg-tertiary);
481
padding: 3px 8px;
482
+
border-radius: var(--radius-full);
483
color: var(--text-secondary);
484
}
485
486
.annotations-list {
487
display: flex;
488
flex-direction: column;
489
+
gap: 1px;
490
+
background: var(--border);
491
}
492
493
.annotation-item {
494
+
padding: 14px 16px;
495
+
background: var(--bg-primary);
496
+
transition: background 0.15s;
0
0
497
}
498
499
.annotation-item:hover {
500
+
background: var(--bg-hover);
501
}
502
503
.annotation-item-header {
···
508
}
509
510
.annotation-item-avatar {
511
+
width: 26px;
512
+
height: 26px;
513
border-radius: 50%;
514
+
background: var(--accent);
515
+
color: var(--bg-primary);
516
display: flex;
517
align-items: center;
518
justify-content: center;
519
+
font-size: 10px;
520
font-weight: 600;
521
}
522
···
538
.annotation-type-badge {
539
font-size: 10px;
540
padding: 3px 8px;
541
+
border-radius: var(--radius-full);
542
font-weight: 500;
543
}
544
545
.annotation-type-badge.highlight {
546
+
background: var(--accent-subtle);
547
+
color: var(--accent-text);
548
}
549
550
.annotation-item-quote {
551
+
padding: 8px 12px;
552
+
border-left: 2px solid var(--accent);
553
+
margin-bottom: 8px;
554
+
font-size: 12px;
555
color: var(--text-secondary);
556
font-style: italic;
557
+
background: var(--accent-subtle);
558
border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
559
}
560
···
567
.bookmarks-list {
568
display: flex;
569
flex-direction: column;
570
+
gap: 1px;
571
+
background: var(--border);
572
}
573
574
.bookmark-item {
575
+
padding: 14px 16px;
576
+
background: var(--bg-primary);
0
0
577
text-decoration: none;
578
color: inherit;
579
display: block;
580
+
transition: background 0.15s;
581
}
582
583
.bookmark-item:hover {
584
+
background: var(--bg-hover);
585
}
586
587
.bookmark-title {
588
+
font-size: 13px;
589
font-weight: 500;
590
margin-bottom: 4px;
591
white-space: nowrap;
···
615
.empty-icon {
616
margin-bottom: 12px;
617
color: var(--text-tertiary);
618
+
opacity: 0.4;
619
}
620
621
.empty-text {
···
635
justify-content: space-between;
636
padding: 12px 16px;
637
border-top: 1px solid var(--border);
638
+
background: var(--bg-primary);
639
}
640
641
.sidebar-link {
642
font-size: 12px;
643
+
color: var(--text-tertiary);
644
text-decoration: none;
645
}
646
647
.sidebar-link:hover {
648
+
color: var(--accent-text);
0
649
}
650
651
.settings-view {
···
693
border-radius: var(--radius-md);
694
font-family: inherit;
695
font-size: 13px;
696
+
background: var(--bg-elevated);
697
color: var(--text-primary);
698
+
transition: border-color 0.15s;
0
0
699
}
700
701
.settings-input:focus {
702
outline: none;
703
border-color: var(--accent);
0
704
}
705
706
.setting-help {
···
715
gap: 4px;
716
padding: 6px 10px;
717
font-size: 11px;
718
+
color: var(--accent-text);
719
background: var(--accent-subtle);
720
border: none;
721
border-radius: var(--radius-sm);
···
725
}
726
727
.scroll-to-btn:hover {
728
+
background: rgba(149, 122, 134, 0.25);
729
}
730
731
::-webkit-scrollbar {
732
+
width: 8px;
733
}
734
735
::-webkit-scrollbar-track {
736
+
background: transparent;
737
}
738
739
::-webkit-scrollbar-thumb {
740
+
background: var(--bg-hover);
741
+
border-radius: var(--radius-full);
742
}
743
744
::-webkit-scrollbar-thumb:hover {
745
+
background: var(--text-tertiary);
746
}
747
748
.collection-selector {
···
771
align-items: center;
772
gap: 12px;
773
padding: 12px;
774
+
background: var(--bg-primary);
775
border: 1px solid var(--border);
776
border-radius: var(--radius-md);
777
color: var(--text-primary);
···
787
}
788
789
.collection-select-btn:disabled {
790
+
opacity: 0.6;
791
cursor: not-allowed;
792
}
793
···
801
.toggle-switch {
802
position: relative;
803
display: inline-block;
804
+
width: 40px;
805
+
height: 22px;
806
flex-shrink: 0;
807
}
808
···
819
left: 0;
820
right: 0;
821
bottom: 0;
822
+
background-color: var(--bg-tertiary);
823
transition: 0.2s;
824
+
border-radius: 22px;
825
}
826
827
.toggle-slider:before {
828
position: absolute;
829
content: "";
830
+
height: 16px;
831
+
width: 16px;
832
left: 3px;
833
bottom: 3px;
834
+
background-color: var(--text-tertiary);
835
transition: 0.2s;
836
border-radius: 50%;
837
}
···
841
}
842
843
.toggle-switch input:checked + .toggle-slider:before {
844
+
transform: translateX(18px);
845
background-color: white;
846
}
847
+
848
.theme-toggle-group {
849
display: flex;
850
background: var(--bg-tertiary);
851
+
padding: 3px;
852
border-radius: var(--radius-md);
853
gap: 2px;
854
margin-top: 8px;
···
859
padding: 6px;
860
border: none;
861
background: transparent;
862
+
color: var(--text-tertiary);
863
font-size: 12px;
864
font-weight: 500;
865
border-radius: var(--radius-sm);
···
868
}
869
870
.theme-btn:hover {
871
+
color: var(--text-secondary);
0
872
}
873
874
.theme-btn.active {
875
+
background: var(--bg-primary);
876
color: var(--text-primary);
0
877
}
+19
-21
web/index.html
···
1
<!doctype html>
2
<html lang="en">
3
-
<head>
4
-
<meta charset="UTF-8" />
5
-
<link rel="icon" href="/favicon.ico" />
6
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
-
<meta
8
-
name="description"
9
-
content="Margin - Write in the margins of the web. Comment on any URL with AT Protocol."
10
-
/>
11
-
<title>Margin - Write in the margins of the web</title>
12
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
13
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14
-
<link
15
-
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
16
-
rel="stylesheet"
17
-
/>
18
-
</head>
19
20
-
<body>
21
-
<div id="root"></div>
22
-
<script type="module" src="/src/main.jsx"></script>
23
-
</body>
24
-
</html>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
<!doctype html>
2
<html lang="en">
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
3
4
+
<head>
5
+
<meta charset="UTF-8" />
6
+
<link rel="icon" href="/favicon.ico" />
7
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+
<meta name="description" content="Margin - Write in the margins of the web. Comment on any URL with AT Protocol." />
9
+
<title>Margin - Write in the margins of the web</title>
10
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
11
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
+
<link
13
+
href="https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400..700;1,400..700&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap"
14
+
rel="stylesheet" />
15
+
</head>
16
+
17
+
<body>
18
+
<div id="root"></div>
19
+
<script type="module" src="/src/main.jsx"></script>
20
+
</body>
21
+
22
+
</html>
+40
-44
web/src/App.jsx
···
1
import { Routes, Route } from "react-router-dom";
2
import { useEffect } from "react";
3
import { AuthProvider, useAuth } from "./context/AuthContext";
4
-
import Sidebar from "./components/Sidebar";
5
-
import RightSidebar from "./components/RightSidebar";
6
import MobileNav from "./components/MobileNav";
7
import Feed from "./pages/Feed";
8
import Url from "./pages/Url";
···
31
}, [user]);
32
33
return (
34
-
<div className="layout">
35
<ScrollToTop />
36
-
<Sidebar />
37
-
<div className="main-layout">
38
-
<main className="main-content-wrapper">
39
-
<Routes>
40
-
<Route path="/" element={<Feed />} />
41
-
<Route path="/url" element={<Url />} />
42
-
<Route path="/new" element={<New />} />
43
-
<Route path="/bookmarks" element={<Bookmarks />} />
44
-
<Route path="/highlights" element={<Highlights />} />
45
-
<Route path="/notifications" element={<Notifications />} />
46
-
<Route path="/profile" element={<Profile />} />
47
-
<Route path="/profile/:handle" element={<Profile />} />
48
-
<Route path="/login" element={<Login />} />
49
-
<Route path="/at/:did/:rkey" element={<AnnotationDetail />} />
50
-
<Route path="/annotation/:uri" element={<AnnotationDetail />} />
51
-
<Route path="/collections" element={<Collections />} />
52
-
<Route path="/collections/:rkey" element={<CollectionDetail />} />
53
-
<Route
54
-
path="/:handle/collection/:rkey"
55
-
element={<CollectionDetail />}
56
-
/>
57
-
<Route
58
-
path="/:handle/annotation/:rkey"
59
-
element={<AnnotationDetail />}
60
-
/>
61
-
<Route
62
-
path="/:handle/highlight/:rkey"
63
-
element={<AnnotationDetail />}
64
-
/>
65
-
<Route
66
-
path="/:handle/bookmark/:rkey"
67
-
element={<AnnotationDetail />}
68
-
/>
69
-
<Route path="/:handle/url/*" element={<UserUrl />} />
70
-
<Route path="/collection/*" element={<CollectionDetail />} />
71
-
<Route path="/privacy" element={<Privacy />} />
72
-
<Route path="/terms" element={<Terms />} />
73
-
</Routes>
74
-
</main>
75
-
</div>
76
-
<RightSidebar />
77
<MobileNav />
78
</div>
79
);
···
1
import { Routes, Route } from "react-router-dom";
2
import { useEffect } from "react";
3
import { AuthProvider, useAuth } from "./context/AuthContext";
4
+
import TopNav from "./components/TopNav";
0
5
import MobileNav from "./components/MobileNav";
6
import Feed from "./pages/Feed";
7
import Url from "./pages/Url";
···
30
}, [user]);
31
32
return (
33
+
<div className="app">
34
<ScrollToTop />
35
+
<TopNav />
36
+
<main className="main-content">
37
+
<Routes>
38
+
<Route path="/" element={<Feed />} />
39
+
<Route path="/url" element={<Url />} />
40
+
<Route path="/new" element={<New />} />
41
+
<Route path="/bookmarks" element={<Bookmarks />} />
42
+
<Route path="/highlights" element={<Highlights />} />
43
+
<Route path="/notifications" element={<Notifications />} />
44
+
<Route path="/profile" element={<Profile />} />
45
+
<Route path="/profile/:handle" element={<Profile />} />
46
+
<Route path="/login" element={<Login />} />
47
+
<Route path="/at/:did/:rkey" element={<AnnotationDetail />} />
48
+
<Route path="/annotation/:uri" element={<AnnotationDetail />} />
49
+
<Route path="/collections" element={<Collections />} />
50
+
<Route path="/collections/:rkey" element={<CollectionDetail />} />
51
+
<Route
52
+
path="/:handle/collection/:rkey"
53
+
element={<CollectionDetail />}
54
+
/>
55
+
<Route
56
+
path="/:handle/annotation/:rkey"
57
+
element={<AnnotationDetail />}
58
+
/>
59
+
<Route
60
+
path="/:handle/highlight/:rkey"
61
+
element={<AnnotationDetail />}
62
+
/>
63
+
<Route
64
+
path="/:handle/bookmark/:rkey"
65
+
element={<AnnotationDetail />}
66
+
/>
67
+
<Route path="/:handle/url/*" element={<UserUrl />} />
68
+
<Route path="/collection/*" element={<CollectionDetail />} />
69
+
<Route path="/privacy" element={<Privacy />} />
70
+
<Route path="/terms" element={<Terms />} />
71
+
</Routes>
72
+
</main>
0
0
0
73
<MobileNav />
74
</div>
75
);
+135
-239
web/src/components/AnnotationCard.jsx
···
34
if (!selector || selector.type !== "TextQuoteSelector" || !selector.exact) {
35
return baseUrl;
36
}
37
-
38
let fragment = ":~:text=";
39
if (selector.prefix) {
40
fragment += encodeURIComponent(selector.prefix) + "-,";
···
43
if (selector.suffix) {
44
fragment += ",-" + encodeURIComponent(selector.suffix);
45
}
46
-
47
return baseUrl + "#" + fragment;
48
}
49
50
-
const truncateUrl = (url, maxLength = 60) => {
51
if (!url) return "";
52
try {
53
const parsed = new URL(url);
···
60
}
61
};
62
0
0
0
0
0
0
0
0
0
63
export default function AnnotationCard({
64
annotation,
65
onDelete,
···
75
const [editText, setEditText] = useState(data.text || "");
76
const [editTags, setEditTags] = useState(data.tags?.join(", ") || "");
77
const [saving, setSaving] = useState(false);
78
-
79
const [showHistory, setShowHistory] = useState(false);
80
const [editHistory, setEditHistory] = useState([]);
81
const [loadingHistory, setLoadingHistory] = useState(false);
82
-
83
const [replies, setReplies] = useState([]);
84
const [replyCount, setReplyCount] = useState(data.replyCount || 0);
85
const [showReplies, setShowReplies] = useState(false);
86
const [replyingTo, setReplyingTo] = useState(null);
87
const [replyText, setReplyText] = useState("");
88
const [posting, setPosting] = useState(false);
0
89
90
const isOwner = user?.did && data.author?.did === user.did;
91
-
92
-
const [hasEditHistory, setHasEditHistory] = useState(false);
0
0
93
94
useEffect(() => {
95
if (data.uri && !data.color && !data.description) {
96
getEditHistory(data.uri)
97
.then((history) => {
98
-
if (history && history.length > 0) {
99
-
setHasEditHistory(true);
100
-
}
101
})
102
.catch(() => {});
103
}
···
122
123
const handlePostReply = async (parentReply) => {
124
if (!replyText.trim()) return;
125
-
126
try {
127
setPosting(true);
128
const parentUri = parentReply
···
175
}
176
};
177
178
-
const highlightedText =
179
-
data.selector?.type === "TextQuoteSelector" ? data.selector.exact : null;
180
-
const fragmentUrl = buildTextFragmentUrl(data.url, data.selector);
181
-
182
const handleLike = async () => {
183
if (!user) {
184
login();
···
195
const cid = annotation.cid || data.cid || "";
196
if (data.uri && cid) await likeAnnotation(data.uri, cid);
197
}
198
-
} catch (err) {
199
setIsLiked(!isLiked);
200
setLikeCount((prev) => (isLiked ? prev + 1 : prev - 1));
201
-
console.error("Failed to toggle like:", err);
202
}
203
};
204
···
218
}
219
};
220
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
221
return (
222
<article className="card annotation-card">
223
<header className="annotation-header">
···
225
<UserMeta author={data.author} createdAt={data.createdAt} />
226
</div>
227
<div className="annotation-header-right">
228
-
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
229
-
{data.uri && data.uri.includes("network.cosmik") && (
230
-
<div
231
-
style={{
232
-
display: "flex",
233
-
alignItems: "center",
234
-
gap: "4px",
235
-
fontSize: "0.75rem",
236
-
color: "var(--text-tertiary)",
237
-
marginRight: "8px",
238
-
}}
239
-
title="Added using Semble"
240
-
>
241
-
<span>via Semble</span>
242
-
<img
243
-
src="/semble-logo.svg"
244
-
alt="Semble"
245
-
style={{ width: "16px", height: "16px" }}
246
-
/>
247
-
</div>
248
-
)}
249
-
{hasEditHistory && !data.color && !data.description && (
250
<button
251
className="annotation-action action-icon-only"
252
-
onClick={fetchHistory}
253
-
title="View Edit History"
0
254
>
255
-
<Clock size={16} />
256
</button>
257
-
)}
258
-
259
-
{isOwner && !(data.uri && data.uri.includes("network.cosmik")) && (
260
-
<>
261
-
{!data.color && !data.description && (
262
-
<button
263
-
className="annotation-action action-icon-only"
264
-
onClick={() => setIsEditing(!isEditing)}
265
-
title="Edit"
266
-
>
267
-
<Edit2 size={16} />
268
-
</button>
269
-
)}
270
-
<button
271
-
className="annotation-action action-icon-only"
272
-
onClick={handleDelete}
273
-
disabled={deleting}
274
-
title="Delete"
275
-
>
276
-
<Trash2 size={16} />
277
-
</button>
278
-
</>
279
-
)}
280
-
</div>
281
</div>
282
</header>
283
···
286
<div className="history-header">
287
<h4 className="history-title">Edit History</h4>
288
<button
289
-
className="history-close-btn"
290
onClick={() => setShowHistory(false)}
291
-
title="Close History"
292
>
293
<X size={14} />
294
</button>
···
321
>
322
{truncateUrl(data.url)}
323
{data.title && (
324
-
<span className="annotation-source-title"> • {data.title}</span>
325
)}
326
</a>
327
···
331
target="_blank"
332
rel="noopener noreferrer"
333
className="annotation-highlight"
334
-
style={{
335
-
borderLeftColor: data.color || "var(--accent)",
336
-
}}
337
>
338
-
<mark>"{highlightedText}"</mark>
339
</a>
340
)}
341
342
{isEditing ? (
343
-
<div className="mt-3">
344
<textarea
345
value={editText}
346
onChange={(e) => setEditText(e.target.value)}
347
className="reply-input"
348
rows={3}
349
-
style={{ marginBottom: "8px" }}
350
/>
351
<input
352
type="text"
···
354
placeholder="Tags (comma separated)..."
355
value={editTags}
356
onChange={(e) => setEditTags(e.target.value)}
357
-
style={{ marginBottom: "8px" }}
358
/>
359
-
<div className="action-buttons-end">
360
<button
361
onClick={() => setIsEditing(false)}
362
className="btn btn-ghost"
···
366
<button
367
onClick={handleSaveEdit}
368
disabled={saving}
369
-
className="btn btn-primary btn-sm"
370
>
371
{saving ? (
372
"Saving..."
···
403
className={`annotation-action ${isLiked ? "liked" : ""}`}
404
onClick={handleLike}
405
>
406
-
<Heart filled={isLiked} size={16} />
407
{likeCount > 0 && <span>{likeCount}</span>}
408
</button>
0
409
<button
410
className={`annotation-action ${showReplies ? "active" : ""}`}
411
-
onClick={async () => {
412
-
if (!showReplies && replies.length === 0) {
413
-
try {
414
-
const res = await getReplies(data.uri);
415
-
if (res.items) setReplies(res.items);
416
-
} catch (err) {
417
-
console.error("Failed to load replies:", err);
418
-
}
419
-
}
420
-
setShowReplies(!showReplies);
421
-
}}
422
>
423
<MessageSquare size={16} />
424
-
<span>{replyCount > 0 ? `${replyCount}` : "Reply"}</span>
425
</button>
0
426
<ShareMenu
427
uri={data.uri}
428
text={data.title || data.url}
···
430
type="Annotation"
431
url={data.url}
432
/>
433
-
<button
434
-
className="annotation-action"
435
-
onClick={() => {
436
-
if (!user) {
437
-
login();
438
-
return;
439
-
}
440
-
if (onAddToCollection) onAddToCollection();
441
-
}}
442
-
>
443
<Folder size={16} />
444
<span>Collect</span>
445
</button>
···
471
472
<div className="reply-form">
473
{replyingTo && (
474
-
<div
475
-
style={{
476
-
display: "flex",
477
-
alignItems: "center",
478
-
gap: "8px",
479
-
marginBottom: "8px",
480
-
fontSize: "0.85rem",
481
-
color: "var(--text-secondary)",
482
-
}}
483
-
>
484
<span>
485
Replying to @
486
{(replyingTo.creator || replyingTo.author)?.handle ||
···
488
</span>
489
<button
490
onClick={() => setReplyingTo(null)}
491
-
style={{
492
-
background: "none",
493
-
border: "none",
494
-
color: "var(--text-tertiary)",
495
-
cursor: "pointer",
496
-
padding: "2px 6px",
497
-
}}
498
>
499
×
500
</button>
···
509
}
510
value={replyText}
511
onChange={(e) => setReplyText(e.target.value)}
512
-
onFocus={(e) => {
513
-
if (!user) {
514
-
e.preventDefault();
515
-
alert("Please sign in to like annotations");
516
-
}
517
-
}}
518
rows={2}
519
/>
520
-
<div className="action-buttons-end">
521
<button
522
-
className="btn btn-primary btn-sm"
523
disabled={posting || !replyText.trim()}
524
onClick={() => {
525
if (!user) {
···
551
data.selector?.type === "TextQuoteSelector" ? data.selector.exact : null;
552
const fragmentUrl = buildTextFragmentUrl(data.url, data.selector);
553
const isOwner = user?.did && data.author?.did === user.did;
0
0
554
const [isEditing, setIsEditing] = useState(false);
555
const [editColor, setEditColor] = useState(data.color || "#f59e0b");
556
const [editTags, setEditTags] = useState(data.tags?.join(", ") || "");
···
561
.split(",")
562
.map((t) => t.trim())
563
.filter(Boolean);
564
-
565
await updateHighlight(data.uri, editColor, tagList);
566
setIsEditing(false);
567
-
if (typeof onUpdate === "function")
568
onUpdate({ ...highlight, color: editColor, tags: tagList });
0
569
} catch (err) {
570
alert("Failed to update: " + err.message);
571
}
572
};
573
0
0
0
0
0
0
0
0
574
return (
575
<article className="card annotation-card">
576
<header className="annotation-header">
577
<div className="annotation-header-left">
578
<UserMeta author={data.author} createdAt={data.createdAt} />
579
</div>
580
-
581
<div className="annotation-header-right">
582
-
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
583
-
{data.uri && data.uri.includes("network.cosmik") && (
584
-
<div
585
-
style={{
586
-
display: "flex",
587
-
alignItems: "center",
588
-
gap: "4px",
589
-
fontSize: "0.75rem",
590
-
color: "var(--text-tertiary)",
591
-
marginRight: "8px",
0
0
0
0
0
0
0
0
0
0
592
}}
593
-
title="Added using Semble"
594
>
595
-
<span>via Semble</span>
596
-
<img
597
-
src="/semble-logo.svg"
598
-
alt="Semble"
599
-
style={{ width: "16px", height: "16px" }}
600
-
/>
601
-
</div>
602
-
)}
603
-
{isOwner && (
604
-
<>
605
-
<button
606
-
className="annotation-action action-icon-only"
607
-
onClick={() => setIsEditing(!isEditing)}
608
-
title="Edit Color"
609
-
>
610
-
<Edit2 size={16} />
611
-
</button>
612
-
<button
613
-
className="annotation-action action-icon-only"
614
-
onClick={(e) => {
615
-
e.preventDefault();
616
-
onDelete && onDelete(highlight.id || highlight.uri);
617
-
}}
618
-
>
619
-
<TrashIcon size={16} />
620
-
</button>
621
-
</>
622
-
)}
623
-
</div>
624
</div>
625
</header>
626
···
644
borderLeftColor: isEditing ? editColor : data.color || "#f59e0b",
645
}}
646
>
647
-
<mark>"{highlightedText}"</mark>
648
</a>
649
)}
650
651
{isEditing && (
652
-
<div
653
-
className="mt-3"
654
-
style={{
655
-
display: "flex",
656
-
gap: "8px",
657
-
alignItems: "center",
658
-
padding: "8px",
659
-
background: "var(--bg-secondary)",
660
-
borderRadius: "var(--radius-md)",
661
-
border: "1px solid var(--border)",
662
-
}}
663
-
>
664
-
<div
665
-
className="color-picker-compact"
666
-
style={{
667
-
position: "relative",
668
-
width: "28px",
669
-
height: "28px",
670
-
flexShrink: 0,
671
-
}}
672
-
>
673
<div
674
-
style={{
675
-
backgroundColor: editColor,
676
-
width: "100%",
677
-
height: "100%",
678
-
borderRadius: "50%",
679
-
border: "2px solid var(--bg-card)",
680
-
boxShadow: "0 0 0 1px var(--border)",
681
-
}}
682
/>
683
<input
684
type="color"
685
value={editColor}
686
onChange={(e) => setEditColor(e.target.value)}
687
-
style={{
688
-
position: "absolute",
689
-
top: 0,
690
-
left: 0,
691
-
width: "100%",
692
-
height: "100%",
693
-
opacity: 0,
694
-
cursor: "pointer",
695
-
}}
696
-
title="Change Color"
697
/>
698
</div>
699
-
700
<input
701
type="text"
702
className="reply-input"
703
-
placeholder="e.g. tag1, tag2"
704
value={editTags}
705
onChange={(e) => setEditTags(e.target.value)}
706
-
style={{
707
-
margin: 0,
708
-
flex: 1,
709
-
fontSize: "0.9rem",
710
-
padding: "6px 10px",
711
-
height: "32px",
712
-
border: "none",
713
-
background: "transparent",
714
-
}}
715
/>
716
-
717
<button
718
onClick={handleSaveEdit}
719
-
className="btn btn-primary btn-sm"
720
-
style={{ padding: "0 10px", height: "32px", minWidth: "auto" }}
721
-
title="Save"
722
>
723
<Save size={16} />
724
</button>
···
744
<div className="annotation-actions-left">
745
<span
746
className="annotation-action"
747
-
style={{
748
-
color: data.color || "#f59e0b",
749
-
background: "none",
750
-
paddingLeft: 0,
751
-
}}
752
>
753
<HighlightIcon size={14} /> Highlight
754
</span>
0
755
<ShareMenu
756
uri={data.uri}
757
text={data.title || data.description}
758
handle={data.author?.handle}
759
type="Highlight"
760
/>
761
-
<button
762
-
className="annotation-action"
763
-
onClick={() => {
764
-
if (!user) {
765
-
login();
766
-
return;
767
-
}
768
-
if (onAddToCollection) onAddToCollection();
769
-
}}
770
-
>
771
<Folder size={16} />
772
<span>Collect</span>
773
</button>
···
34
if (!selector || selector.type !== "TextQuoteSelector" || !selector.exact) {
35
return baseUrl;
36
}
0
37
let fragment = ":~:text=";
38
if (selector.prefix) {
39
fragment += encodeURIComponent(selector.prefix) + "-,";
···
42
if (selector.suffix) {
43
fragment += ",-" + encodeURIComponent(selector.suffix);
44
}
0
45
return baseUrl + "#" + fragment;
46
}
47
48
+
const truncateUrl = (url, maxLength = 50) => {
49
if (!url) return "";
50
try {
51
const parsed = new URL(url);
···
58
}
59
};
60
61
+
function SembleBadge() {
62
+
return (
63
+
<div className="semble-badge" title="Added using Semble">
64
+
<span>via Semble</span>
65
+
<img src="/semble-logo.svg" alt="Semble" />
66
+
</div>
67
+
);
68
+
}
69
+
70
export default function AnnotationCard({
71
annotation,
72
onDelete,
···
82
const [editText, setEditText] = useState(data.text || "");
83
const [editTags, setEditTags] = useState(data.tags?.join(", ") || "");
84
const [saving, setSaving] = useState(false);
0
85
const [showHistory, setShowHistory] = useState(false);
86
const [editHistory, setEditHistory] = useState([]);
87
const [loadingHistory, setLoadingHistory] = useState(false);
0
88
const [replies, setReplies] = useState([]);
89
const [replyCount, setReplyCount] = useState(data.replyCount || 0);
90
const [showReplies, setShowReplies] = useState(false);
91
const [replyingTo, setReplyingTo] = useState(null);
92
const [replyText, setReplyText] = useState("");
93
const [posting, setPosting] = useState(false);
94
+
const [hasEditHistory, setHasEditHistory] = useState(false);
95
96
const isOwner = user?.did && data.author?.did === user.did;
97
+
const isSemble = data.uri?.includes("network.cosmik");
98
+
const highlightedText =
99
+
data.selector?.type === "TextQuoteSelector" ? data.selector.exact : null;
100
+
const fragmentUrl = buildTextFragmentUrl(data.url, data.selector);
101
102
useEffect(() => {
103
if (data.uri && !data.color && !data.description) {
104
getEditHistory(data.uri)
105
.then((history) => {
106
+
if (history?.length > 0) setHasEditHistory(true);
0
0
107
})
108
.catch(() => {});
109
}
···
128
129
const handlePostReply = async (parentReply) => {
130
if (!replyText.trim()) return;
0
131
try {
132
setPosting(true);
133
const parentUri = parentReply
···
180
}
181
};
182
0
0
0
0
183
const handleLike = async () => {
184
if (!user) {
185
login();
···
196
const cid = annotation.cid || data.cid || "";
197
if (data.uri && cid) await likeAnnotation(data.uri, cid);
198
}
199
+
} catch {
200
setIsLiked(!isLiked);
201
setLikeCount((prev) => (isLiked ? prev + 1 : prev - 1));
0
202
}
203
};
204
···
218
}
219
};
220
221
+
const loadReplies = async () => {
222
+
if (!showReplies && replies.length === 0) {
223
+
try {
224
+
const res = await getReplies(data.uri);
225
+
if (res.items) setReplies(res.items);
226
+
} catch (err) {
227
+
console.error("Failed to load replies:", err);
228
+
}
229
+
}
230
+
setShowReplies(!showReplies);
231
+
};
232
+
233
+
const handleCollect = () => {
234
+
if (!user) {
235
+
login();
236
+
return;
237
+
}
238
+
if (onAddToCollection) onAddToCollection();
239
+
};
240
+
241
return (
242
<article className="card annotation-card">
243
<header className="annotation-header">
···
245
<UserMeta author={data.author} createdAt={data.createdAt} />
246
</div>
247
<div className="annotation-header-right">
248
+
{isSemble && <SembleBadge />}
249
+
{hasEditHistory && !data.color && !data.description && (
250
+
<button
251
+
className="annotation-action action-icon-only"
252
+
onClick={fetchHistory}
253
+
title="View Edit History"
254
+
>
255
+
<Clock size={16} />
256
+
</button>
257
+
)}
258
+
{isOwner && !isSemble && (
259
+
<>
260
+
{!data.color && !data.description && (
261
+
<button
262
+
className="annotation-action action-icon-only"
263
+
onClick={() => setIsEditing(!isEditing)}
264
+
title="Edit"
265
+
>
266
+
<Edit2 size={16} />
267
+
</button>
268
+
)}
0
269
<button
270
className="annotation-action action-icon-only"
271
+
onClick={handleDelete}
272
+
disabled={deleting}
273
+
title="Delete"
274
>
275
+
<Trash2 size={16} />
276
</button>
277
+
</>
278
+
)}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
279
</div>
280
</header>
281
···
284
<div className="history-header">
285
<h4 className="history-title">Edit History</h4>
286
<button
287
+
className="annotation-action action-icon-only"
288
onClick={() => setShowHistory(false)}
0
289
>
290
<X size={14} />
291
</button>
···
318
>
319
{truncateUrl(data.url)}
320
{data.title && (
321
+
<span className="annotation-source-title"> · {data.title}</span>
322
)}
323
</a>
324
···
328
target="_blank"
329
rel="noopener noreferrer"
330
className="annotation-highlight"
331
+
style={{ borderLeftColor: data.color || "var(--accent)" }}
0
0
332
>
333
+
<mark>“{highlightedText}”</mark>
334
</a>
335
)}
336
337
{isEditing ? (
338
+
<div className="edit-form">
339
<textarea
340
value={editText}
341
onChange={(e) => setEditText(e.target.value)}
342
className="reply-input"
343
rows={3}
344
+
placeholder="Your annotation..."
345
/>
346
<input
347
type="text"
···
349
placeholder="Tags (comma separated)..."
350
value={editTags}
351
onChange={(e) => setEditTags(e.target.value)}
352
+
style={{ marginTop: "8px" }}
353
/>
354
+
<div className="action-buttons-end" style={{ marginTop: "8px" }}>
355
<button
356
onClick={() => setIsEditing(false)}
357
className="btn btn-ghost"
···
361
<button
362
onClick={handleSaveEdit}
363
disabled={saving}
364
+
className="btn btn-primary"
365
>
366
{saving ? (
367
"Saving..."
···
398
className={`annotation-action ${isLiked ? "liked" : ""}`}
399
onClick={handleLike}
400
>
401
+
<Heart size={16} fill={isLiked ? "currentColor" : "none"} />
402
{likeCount > 0 && <span>{likeCount}</span>}
403
</button>
404
+
405
<button
406
className={`annotation-action ${showReplies ? "active" : ""}`}
407
+
onClick={loadReplies}
0
0
0
0
0
0
0
0
0
0
408
>
409
<MessageSquare size={16} />
410
+
<span>{replyCount > 0 ? replyCount : "Reply"}</span>
411
</button>
412
+
413
<ShareMenu
414
uri={data.uri}
415
text={data.title || data.url}
···
417
type="Annotation"
418
url={data.url}
419
/>
420
+
421
+
<button className="annotation-action" onClick={handleCollect}>
0
0
0
0
0
0
0
0
422
<Folder size={16} />
423
<span>Collect</span>
424
</button>
···
450
451
<div className="reply-form">
452
{replyingTo && (
453
+
<div className="replying-to-banner">
0
0
0
0
0
0
0
0
0
454
<span>
455
Replying to @
456
{(replyingTo.creator || replyingTo.author)?.handle ||
···
458
</span>
459
<button
460
onClick={() => setReplyingTo(null)}
461
+
className="cancel-reply"
0
0
0
0
0
0
462
>
463
×
464
</button>
···
473
}
474
value={replyText}
475
onChange={(e) => setReplyText(e.target.value)}
0
0
0
0
0
0
476
rows={2}
477
/>
478
+
<div className="reply-form-actions">
479
<button
480
+
className="btn btn-primary"
481
disabled={posting || !replyText.trim()}
482
onClick={() => {
483
if (!user) {
···
509
data.selector?.type === "TextQuoteSelector" ? data.selector.exact : null;
510
const fragmentUrl = buildTextFragmentUrl(data.url, data.selector);
511
const isOwner = user?.did && data.author?.did === user.did;
512
+
const isSemble = data.uri?.includes("network.cosmik");
513
+
514
const [isEditing, setIsEditing] = useState(false);
515
const [editColor, setEditColor] = useState(data.color || "#f59e0b");
516
const [editTags, setEditTags] = useState(data.tags?.join(", ") || "");
···
521
.split(",")
522
.map((t) => t.trim())
523
.filter(Boolean);
0
524
await updateHighlight(data.uri, editColor, tagList);
525
setIsEditing(false);
526
+
if (typeof onUpdate === "function") {
527
onUpdate({ ...highlight, color: editColor, tags: tagList });
528
+
}
529
} catch (err) {
530
alert("Failed to update: " + err.message);
531
}
532
};
533
534
+
const handleCollect = () => {
535
+
if (!user) {
536
+
login();
537
+
return;
538
+
}
539
+
if (onAddToCollection) onAddToCollection();
540
+
};
541
+
542
return (
543
<article className="card annotation-card">
544
<header className="annotation-header">
545
<div className="annotation-header-left">
546
<UserMeta author={data.author} createdAt={data.createdAt} />
547
</div>
0
548
<div className="annotation-header-right">
549
+
{isSemble && (
550
+
<div className="semble-badge" title="Added using Semble">
551
+
<span>via Semble</span>
552
+
<img src="/semble-logo.svg" alt="Semble" />
553
+
</div>
554
+
)}
555
+
{isOwner && (
556
+
<>
557
+
<button
558
+
className="annotation-action action-icon-only"
559
+
onClick={() => setIsEditing(!isEditing)}
560
+
title="Edit Color"
561
+
>
562
+
<Edit2 size={16} />
563
+
</button>
564
+
<button
565
+
className="annotation-action action-icon-only"
566
+
onClick={(e) => {
567
+
e.preventDefault();
568
+
onDelete && onDelete(highlight.id || highlight.uri);
569
}}
570
+
title="Delete"
571
>
572
+
<TrashIcon size={16} />
573
+
</button>
574
+
</>
575
+
)}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
576
</div>
577
</header>
578
···
596
borderLeftColor: isEditing ? editColor : data.color || "#f59e0b",
597
}}
598
>
599
+
<mark>“{highlightedText}”</mark>
600
</a>
601
)}
602
603
{isEditing && (
604
+
<div className="color-edit-form">
605
+
<div className="color-picker-wrapper">
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
606
<div
607
+
className="color-preview"
608
+
style={{ backgroundColor: editColor }}
0
0
0
0
0
0
609
/>
610
<input
611
type="color"
612
value={editColor}
613
onChange={(e) => setEditColor(e.target.value)}
614
+
className="color-input"
0
0
0
0
0
0
0
0
0
615
/>
616
</div>
0
617
<input
618
type="text"
619
className="reply-input"
620
+
placeholder="Tags (comma separated)"
621
value={editTags}
622
onChange={(e) => setEditTags(e.target.value)}
623
+
style={{ flex: 1, margin: 0 }}
0
0
0
0
0
0
0
0
624
/>
0
625
<button
626
onClick={handleSaveEdit}
627
+
className="btn btn-primary"
628
+
style={{ padding: "0 12px", height: "32px" }}
0
629
>
630
<Save size={16} />
631
</button>
···
651
<div className="annotation-actions-left">
652
<span
653
className="annotation-action"
654
+
style={{ color: data.color || "#f59e0b", cursor: "default" }}
0
0
0
0
655
>
656
<HighlightIcon size={14} /> Highlight
657
</span>
658
+
659
<ShareMenu
660
uri={data.uri}
661
text={data.title || data.description}
662
handle={data.author?.handle}
663
type="Highlight"
664
/>
665
+
666
+
<button className="annotation-action" onClick={handleCollect}>
0
0
0
0
0
0
0
0
667
<Folder size={16} />
668
<span>Collect</span>
669
</button>
+36
-56
web/src/components/BookmarkCard.jsx
···
8
getLikeCount,
9
deleteBookmark,
10
} from "../api/client";
11
-
import { HeartIcon, TrashIcon, BookmarkIcon } from "./Icons";
12
-
import { Folder } from "lucide-react";
13
import ShareMenu from "./ShareMenu";
14
import UserMeta from "./UserMeta";
15
···
28
const [deleting, setDeleting] = useState(false);
29
30
const isOwner = user?.did && data.author?.did === user.did;
0
0
0
0
0
0
0
0
31
32
useEffect(() => {
33
let mounted = true;
···
75
onDelete(data.uri);
76
return;
77
}
78
-
79
if (!confirm("Delete this bookmark?")) return;
80
try {
81
setDeleting(true);
···
90
}
91
};
92
93
-
let domain = "";
94
-
try {
95
-
if (data.url) domain = new URL(data.url).hostname.replace("www.", "");
96
-
} catch {
97
-
/* ignore */
98
-
}
0
99
100
return (
101
<article className="card annotation-card bookmark-card">
···
103
<div className="annotation-header-left">
104
<UserMeta author={data.author} createdAt={data.createdAt} />
105
</div>
106
-
107
<div className="annotation-header-right">
108
-
<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
109
-
{data.uri && data.uri.includes("network.cosmik") && (
110
-
<div
111
-
style={{
112
-
display: "flex",
113
-
alignItems: "center",
114
-
gap: "4px",
115
-
fontSize: "0.75rem",
116
-
color: "var(--text-tertiary)",
117
-
marginRight: "8px",
118
-
}}
119
-
title="Added using Semble"
120
-
>
121
-
<span>via Semble</span>
122
-
<img
123
-
src="/semble-logo.svg"
124
-
alt="Semble"
125
-
style={{ width: "16px", height: "16px" }}
126
-
/>
127
-
</div>
128
-
)}
129
-
<div style={{ display: "flex", gap: "4px" }}>
130
-
{((isOwner &&
131
-
!(data.uri && data.uri.includes("network.cosmik"))) ||
132
-
onDelete) && (
133
-
<button
134
-
className="annotation-action action-icon-only"
135
-
onClick={handleDelete}
136
-
disabled={deleting}
137
-
title="Delete"
138
-
>
139
-
<TrashIcon size={16} />
140
-
</button>
141
-
)}
142
</div>
143
-
</div>
0
0
0
0
0
0
0
0
0
0
144
</div>
145
</header>
146
···
153
>
154
<div className="bookmark-preview-content">
155
<div className="bookmark-preview-site">
156
-
<BookmarkIcon size={14} />
157
<span>{domain}</span>
158
</div>
159
<h3 className="bookmark-preview-title">{data.title || data.url}</h3>
···
183
<HeartIcon filled={isLiked} size={16} />
184
{likeCount > 0 && <span>{likeCount}</span>}
185
</button>
0
186
<ShareMenu
187
uri={data.uri}
188
text={data.title || data.description}
···
190
type="Bookmark"
191
url={data.url}
192
/>
193
-
<button
194
-
className="annotation-action"
195
-
onClick={() => {
196
-
if (!user) {
197
-
login();
198
-
return;
199
-
}
200
-
if (onAddToCollection) onAddToCollection();
201
-
}}
202
-
>
203
<Folder size={16} />
204
<span>Collect</span>
205
</button>
···
8
getLikeCount,
9
deleteBookmark,
10
} from "../api/client";
11
+
import { HeartIcon, TrashIcon } from "./Icons";
12
+
import { Folder, ExternalLink } from "lucide-react";
13
import ShareMenu from "./ShareMenu";
14
import UserMeta from "./UserMeta";
15
···
28
const [deleting, setDeleting] = useState(false);
29
30
const isOwner = user?.did && data.author?.did === user.did;
31
+
const isSemble = data.uri?.includes("network.cosmik");
32
+
33
+
let domain = "";
34
+
try {
35
+
if (data.url) domain = new URL(data.url).hostname.replace("www.", "");
36
+
} catch {
37
+
/* ignore */
38
+
}
39
40
useEffect(() => {
41
let mounted = true;
···
83
onDelete(data.uri);
84
return;
85
}
0
86
if (!confirm("Delete this bookmark?")) return;
87
try {
88
setDeleting(true);
···
97
}
98
};
99
100
+
const handleCollect = () => {
101
+
if (!user) {
102
+
login();
103
+
return;
104
+
}
105
+
if (onAddToCollection) onAddToCollection();
106
+
};
107
108
return (
109
<article className="card annotation-card bookmark-card">
···
111
<div className="annotation-header-left">
112
<UserMeta author={data.author} createdAt={data.createdAt} />
113
</div>
0
114
<div className="annotation-header-right">
115
+
{isSemble && (
116
+
<div className="semble-badge" title="Added using Semble">
117
+
<span>via Semble</span>
118
+
<img src="/semble-logo.svg" alt="Semble" />
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
119
</div>
120
+
)}
121
+
{((isOwner && !isSemble) || onDelete) && (
122
+
<button
123
+
className="annotation-action action-icon-only"
124
+
onClick={handleDelete}
125
+
disabled={deleting}
126
+
title="Delete"
127
+
>
128
+
<TrashIcon size={16} />
129
+
</button>
130
+
)}
131
</div>
132
</header>
133
···
140
>
141
<div className="bookmark-preview-content">
142
<div className="bookmark-preview-site">
143
+
<ExternalLink size={12} />
144
<span>{domain}</span>
145
</div>
146
<h3 className="bookmark-preview-title">{data.title || data.url}</h3>
···
170
<HeartIcon filled={isLiked} size={16} />
171
{likeCount > 0 && <span>{likeCount}</span>}
172
</button>
173
+
174
<ShareMenu
175
uri={data.uri}
176
text={data.title || data.description}
···
178
type="Bookmark"
179
url={data.url}
180
/>
181
+
182
+
<button className="annotation-action" onClick={handleCollect}>
0
0
0
0
0
0
0
0
183
<Folder size={16} />
184
<span>Collect</span>
185
</button>
+55
-70
web/src/components/CollectionItemCard.jsx
···
5
import CollectionIcon from "./CollectionIcon";
6
import ShareMenu from "./ShareMenu";
7
8
-
export default function CollectionItemCard({ item }) {
9
const author = item.creator;
10
const collection = item.collection;
11
12
if (!author || !collection) return null;
13
14
-
let inner = null;
15
-
if (item.annotation) {
16
-
inner = <AnnotationCard annotation={item.annotation} />;
17
-
} else if (item.highlight) {
18
-
inner = <HighlightCard highlight={item.highlight} />;
19
-
} else if (item.bookmark) {
20
-
inner = <BookmarkCard bookmark={item.bookmark} />;
21
-
}
22
23
-
if (!inner) return null;
24
25
return (
26
-
<div className="collection-feed-item" style={{ marginBottom: "20px" }}>
27
-
<div
28
-
className="feed-context-header"
29
-
style={{
30
-
display: "flex",
31
-
alignItems: "center",
32
-
gap: "8px",
33
-
marginBottom: "8px",
34
-
fontSize: "14px",
35
-
color: "var(--text-secondary)",
36
-
}}
37
-
>
38
-
{author.avatar && (
39
-
<img
40
-
src={author.avatar}
41
-
alt={author.handle}
42
-
style={{
43
-
width: "24px",
44
-
height: "24px",
45
-
borderRadius: "50%",
46
-
objectFit: "cover",
47
-
}}
48
-
/>
49
-
)}
50
-
<span>
51
-
<span style={{ fontWeight: 600, color: "var(--text-primary)" }}>
52
-
{author.displayName || author.handle}
53
-
</span>{" "}
54
-
added to{" "}
55
-
<Link
56
-
to={`/${author.handle}/collection/${collection.uri.split("/").pop()}`}
57
-
style={{
58
-
display: "inline-flex",
59
-
alignItems: "center",
60
-
gap: "4px",
61
-
fontWeight: 500,
62
-
color: "var(--primary)",
63
-
textDecoration: "none",
64
-
}}
65
-
>
66
-
<CollectionIcon icon={collection.icon} size={14} />
67
-
{collection.name}
68
-
</Link>
69
-
</span>
70
-
<div style={{ marginLeft: "auto" }}>
71
-
<ShareMenu
72
-
uri={collection.uri}
73
-
handle={author.handle}
74
-
type="Collection"
75
-
text={`Check out this collection by ${author.displayName}: ${collection.name}`}
76
-
/>
77
</div>
78
-
</div>
79
-
<div
80
-
className="feed-context-body"
81
-
style={{
82
-
paddingLeft: "16px",
83
-
borderLeft: "2px solid var(--border-color)",
84
-
}}
85
-
>
86
-
{inner}
87
</div>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
88
</div>
89
);
90
}
···
5
import CollectionIcon from "./CollectionIcon";
6
import ShareMenu from "./ShareMenu";
7
8
+
export default function CollectionItemCard({ item, onAddToCollection }) {
9
const author = item.creator;
10
const collection = item.collection;
11
12
if (!author || !collection) return null;
13
14
+
const innerItem = item.annotation || item.highlight || item.bookmark;
15
+
if (!innerItem) return null;
0
0
0
0
0
0
16
17
+
const innerUri = innerItem.uri || innerItem.id;
18
19
return (
20
+
<div className="collection-feed-item">
21
+
<div className="collection-context-badge">
22
+
<div className="collection-context-inner">
23
+
{author.avatar && (
24
+
<img
25
+
src={author.avatar}
26
+
alt={author.handle}
27
+
className="collection-context-avatar"
28
+
/>
29
+
)}
30
+
<span className="collection-context-text">
31
+
<Link
32
+
to={`/profile/${author.did}`}
33
+
className="collection-context-author"
34
+
>
35
+
{author.displayName || author.handle}
36
+
</Link>{" "}
37
+
added to{" "}
38
+
<Link
39
+
to={`/${author.handle}/collection/${collection.uri.split("/").pop()}`}
40
+
className="collection-context-link"
41
+
>
42
+
<CollectionIcon icon={collection.icon} size={14} />
43
+
{collection.name}
44
+
</Link>
45
+
</span>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
46
</div>
47
+
<ShareMenu
48
+
uri={collection.uri}
49
+
handle={author.handle}
50
+
type="Collection"
51
+
text={`Check out this collection: ${collection.name}`}
52
+
/>
0
0
0
53
</div>
54
+
55
+
{item.annotation && (
56
+
<AnnotationCard
57
+
annotation={item.annotation}
58
+
onAddToCollection={() => onAddToCollection?.(innerUri)}
59
+
/>
60
+
)}
61
+
{item.highlight && (
62
+
<HighlightCard
63
+
highlight={item.highlight}
64
+
onAddToCollection={() => onAddToCollection?.(innerUri)}
65
+
/>
66
+
)}
67
+
{item.bookmark && (
68
+
<BookmarkCard
69
+
bookmark={item.bookmark}
70
+
onAddToCollection={() => onAddToCollection?.(innerUri)}
71
+
/>
72
+
)}
73
</div>
74
);
75
}
+47
-1
web/src/components/CollectionModal.jsx
···
41
Moon,
42
Flame,
43
Leaf,
0
44
} from "lucide-react";
45
-
import { createCollection, updateCollection } from "../api/client";
0
0
0
0
46
47
const EMOJI_OPTIONS = [
48
"📁",
···
125
onClose,
126
onSuccess,
127
collectionToEdit,
0
128
}) {
129
const [name, setName] = useState("");
130
const [description, setDescription] = useState("");
···
132
const [customEmoji, setCustomEmoji] = useState("");
133
const [activeTab, setActiveTab] = useState("emoji");
134
const [loading, setLoading] = useState(false);
0
135
const [error, setError] = useState(null);
136
137
useEffect(() => {
···
211
}
212
};
213
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
214
return (
215
<div className="modal-overlay" onClick={onClose}>
216
<div
···
327
</div>
328
329
<div className="modal-actions">
0
0
0
0
0
0
0
0
0
0
0
0
330
<button type="button" onClick={onClose} className="btn btn-ghost">
331
Cancel
332
</button>
···
41
Moon,
42
Flame,
43
Leaf,
44
+
Trash2,
45
} from "lucide-react";
46
+
import {
47
+
createCollection,
48
+
updateCollection,
49
+
deleteCollection,
50
+
} from "../api/client";
51
52
const EMOJI_OPTIONS = [
53
"📁",
···
130
onClose,
131
onSuccess,
132
collectionToEdit,
133
+
onDelete,
134
}) {
135
const [name, setName] = useState("");
136
const [description, setDescription] = useState("");
···
138
const [customEmoji, setCustomEmoji] = useState("");
139
const [activeTab, setActiveTab] = useState("emoji");
140
const [loading, setLoading] = useState(false);
141
+
const [deleting, setDeleting] = useState(false);
142
const [error, setError] = useState(null);
143
144
useEffect(() => {
···
218
}
219
};
220
221
+
const handleDelete = async () => {
222
+
if (
223
+
!confirm(
224
+
"Delete this collection and all its items? This cannot be undone.",
225
+
)
226
+
) {
227
+
return;
228
+
}
229
+
setDeleting(true);
230
+
setError(null);
231
+
232
+
try {
233
+
await deleteCollection(collectionToEdit.uri);
234
+
if (onDelete) {
235
+
onDelete();
236
+
} else {
237
+
onSuccess();
238
+
}
239
+
onClose();
240
+
} catch (err) {
241
+
console.error(err);
242
+
setError(err.message || "Failed to delete collection");
243
+
} finally {
244
+
setDeleting(false);
245
+
}
246
+
};
247
+
248
return (
249
<div className="modal-overlay" onClick={onClose}>
250
<div
···
361
</div>
362
363
<div className="modal-actions">
364
+
{collectionToEdit && (
365
+
<button
366
+
type="button"
367
+
onClick={handleDelete}
368
+
disabled={deleting}
369
+
className="btn btn-danger"
370
+
>
371
+
<Trash2 size={16} />
372
+
{deleting ? "Deleting..." : "Delete"}
373
+
</button>
374
+
)}
375
+
<div style={{ flex: 1 }} />
376
<button type="button" onClick={onClose} className="btn btn-ghost">
377
Cancel
378
</button>
+52
web/src/components/IOSInstallBanner.jsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { useState } from "react";
2
+
import { X } from "lucide-react";
3
+
import { SiApple } from "react-icons/si";
4
+
5
+
function shouldShowBanner() {
6
+
if (typeof window === "undefined") return false;
7
+
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);
8
+
if (!isIOS) return false;
9
+
10
+
const dismissedAt = localStorage.getItem("ios-shortcut-dismissed");
11
+
const daysSinceDismissed = dismissedAt
12
+
? (Date.now() - parseInt(dismissedAt, 10)) / (1000 * 60 * 60 * 24)
13
+
: Infinity;
14
+
return daysSinceDismissed > 7;
15
+
}
16
+
17
+
export default function IOSInstallBanner() {
18
+
const [show, setShow] = useState(shouldShowBanner);
19
+
20
+
const handleDismiss = () => {
21
+
setShow(false);
22
+
localStorage.setItem("ios-shortcut-dismissed", Date.now().toString());
23
+
};
24
+
25
+
if (!show) return null;
26
+
27
+
return (
28
+
<div className="ios-shortcut-banner">
29
+
<button
30
+
className="ios-shortcut-banner-close"
31
+
onClick={handleDismiss}
32
+
aria-label="Dismiss"
33
+
>
34
+
<X size={14} />
35
+
</button>
36
+
<div className="ios-shortcut-banner-content">
37
+
<div className="ios-shortcut-banner-text">
38
+
<p>Save pages directly from Safari</p>
39
+
</div>
40
+
<a
41
+
href="https://www.icloud.com/shortcuts/21c87edf29b046db892c9e57dac6d1fd"
42
+
target="_blank"
43
+
rel="noopener noreferrer"
44
+
className="ios-shortcut-banner-btn"
45
+
>
46
+
<SiApple size={14} />
47
+
Get iOS Shortcut
48
+
</a>
49
+
</div>
50
+
</div>
51
+
);
52
+
}
+68
-39
web/src/components/MobileNav.jsx
···
1
import { Link, useLocation } from "react-router-dom";
2
import { useAuth } from "../context/AuthContext";
3
-
import { Home, Search, Folder, User, PenSquare } from "lucide-react";
4
5
export default function MobileNav() {
6
const { user, isAuthenticated } = useAuth();
···
12
};
13
14
return (
15
-
<nav className="mobile-nav">
16
-
<div className="mobile-nav-inner">
17
-
<Link
18
-
to="/"
19
-
className={`mobile-nav-item ${isActive("/") ? "active" : ""}`}
20
-
>
21
-
<Home />
22
-
<span>Home</span>
23
-
</Link>
24
25
-
<Link
26
-
to="/url"
27
-
className={`mobile-nav-item ${isActive("/url") ? "active" : ""}`}
28
-
>
29
-
<Search />
30
-
<span>Browse</span>
31
-
</Link>
32
33
-
{isAuthenticated ? (
34
-
<Link to="/new" className="mobile-nav-item mobile-nav-new">
35
-
<PenSquare />
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
36
</Link>
37
-
) : (
38
-
<Link to="/login" className="mobile-nav-item mobile-nav-new">
39
-
<User />
0
0
0
0
0
0
0
40
</Link>
41
-
)}
42
43
-
<Link
44
-
to="/collections"
45
-
className={`mobile-nav-item ${isActive("/collections") ? "active" : ""}`}
46
-
>
47
-
<Folder />
48
-
<span>Library</span>
49
-
</Link>
50
51
-
<Link
52
-
to={isAuthenticated && user?.did ? `/profile/${user.did}` : "/login"}
53
-
className={`mobile-nav-item ${isActive("/profile") ? "active" : ""}`}
54
-
>
55
-
<User />
56
-
<span>Profile</span>
57
-
</Link>
58
-
</div>
59
</nav>
60
);
61
}
···
1
import { Link, useLocation } from "react-router-dom";
2
import { useAuth } from "../context/AuthContext";
3
+
import { Home, Search, Folder, User, PenSquare, Bookmark } from "lucide-react";
4
5
export default function MobileNav() {
6
const { user, isAuthenticated } = useAuth();
···
12
};
13
14
return (
15
+
<nav className="mobile-bottom-nav">
16
+
<Link
17
+
to="/"
18
+
className={`mobile-bottom-nav-item ${isActive("/") ? "active" : ""}`}
19
+
>
20
+
<Home size={22} />
21
+
<span>Home</span>
22
+
</Link>
0
23
24
+
<Link
25
+
to="/url"
26
+
className={`mobile-bottom-nav-item ${isActive("/url") ? "active" : ""}`}
27
+
>
28
+
<Search size={22} />
29
+
<span>Browse</span>
30
+
</Link>
31
32
+
{isAuthenticated ? (
33
+
<>
34
+
<Link
35
+
to="/new"
36
+
className="mobile-bottom-nav-item mobile-bottom-nav-new"
37
+
>
38
+
<div className="mobile-nav-new-btn">
39
+
<PenSquare size={20} />
40
+
</div>
41
+
</Link>
42
+
43
+
<Link
44
+
to="/bookmarks"
45
+
className={`mobile-bottom-nav-item ${isActive("/bookmarks") || isActive("/collections") ? "active" : ""}`}
46
+
>
47
+
<Bookmark size={22} />
48
+
<span>Library</span>
49
+
</Link>
50
+
51
+
<Link
52
+
to={user?.did ? `/profile/${user.did}` : "/profile"}
53
+
className={`mobile-bottom-nav-item ${isActive("/profile") ? "active" : ""}`}
54
+
>
55
+
{user?.avatar ? (
56
+
<img src={user.avatar} alt="" className="mobile-nav-avatar" />
57
+
) : (
58
+
<User size={22} />
59
+
)}
60
+
<span>You</span>
61
</Link>
62
+
</>
63
+
) : (
64
+
<>
65
+
<Link
66
+
to="/login"
67
+
className="mobile-bottom-nav-item mobile-bottom-nav-new"
68
+
>
69
+
<div className="mobile-nav-new-btn">
70
+
<User size={20} />
71
+
</div>
72
</Link>
0
73
74
+
<Link
75
+
to="/collections"
76
+
className={`mobile-bottom-nav-item ${isActive("/collections") ? "active" : ""}`}
77
+
>
78
+
<Folder size={22} />
79
+
<span>Library</span>
80
+
</Link>
81
82
+
<Link to="/login" className={`mobile-bottom-nav-item`}>
83
+
<User size={22} />
84
+
<span>Sign In</span>
85
+
</Link>
86
+
</>
87
+
)}
0
0
88
</nav>
89
);
90
}
-226
web/src/components/RightSidebar.jsx
···
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,
7
-
SiGithub,
8
-
SiBluesky,
9
-
SiApple,
10
-
SiKofi,
11
-
SiDiscord,
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 =
19
-
typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent);
20
-
const isEdge =
21
-
typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent);
22
-
const isMobileSafari =
23
-
typeof navigator !== "undefined" &&
24
-
/iPhone|iPad|iPod/.test(navigator.userAgent) &&
25
-
/Safari/.test(navigator.userAgent) &&
26
-
!/CriOS|FxiOS|OPiOS|EdgiOS/.test(navigator.userAgent);
27
-
28
-
function getExtensionInfo() {
29
-
if (isMobileSafari) {
30
-
return {
31
-
url: "https://margin.at/soon",
32
-
icon: SiApple,
33
-
name: "iOS",
34
-
label: "Coming Soon",
35
-
};
36
-
}
37
-
if (isFirefox) {
38
-
return {
39
-
url: "https://addons.mozilla.org/en-US/firefox/addon/margin/",
40
-
icon: SiFirefox,
41
-
name: "Firefox",
42
-
label: "Install for Firefox",
43
-
};
44
-
}
45
-
if (isEdge) {
46
-
return {
47
-
url: "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn",
48
-
icon: FaEdge,
49
-
name: "Edge",
50
-
label: "Install for Edge",
51
-
};
52
-
}
53
-
return {
54
-
url: "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/",
55
-
icon: SiGooglechrome,
56
-
name: "Chrome",
57
-
label: "Install for Chrome",
58
-
};
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;
66
-
const [trendingTags, setTrendingTags] = useState([]);
67
-
const [loading, setLoading] = useState(true);
68
-
69
-
useEffect(() => {
70
-
getTrendingTags()
71
-
.then((tags) => setTrendingTags(tags))
72
-
.catch((err) => console.error("Failed to fetch trending tags:", err))
73
-
.finally(() => setLoading(false));
74
-
}, []);
75
-
76
-
return (
77
-
<aside className="right-sidebar">
78
-
<div className="right-section">
79
-
<h3 className="right-section-title">
80
-
{isMobileSafari ? "Save from Safari" : "Get the Extension"}
81
-
</h3>
82
-
<p className="right-section-desc">
83
-
{isMobileSafari
84
-
? "Bookmark pages using Safari's share sheet"
85
-
: "Annotate, highlight, and bookmark any webpage"}
86
-
</p>
87
-
<a
88
-
href={ext.url}
89
-
target="_blank"
90
-
rel="noopener noreferrer"
91
-
className="right-extension-btn"
92
-
>
93
-
<ExtIcon size={18} />
94
-
{ext.label}
95
-
<ExternalLink size={14} />
96
-
</a>
97
-
</div>
98
-
99
-
{isAuthenticated ? (
100
-
<div className="right-section">
101
-
<h3 className="right-section-title">Trending Tags</h3>
102
-
<div className="right-links">
103
-
{loading ? (
104
-
<span className="right-section-desc">Loading...</span>
105
-
) : trendingTags.length > 0 ? (
106
-
trendingTags.map(({ tag, count }) => (
107
-
<Link
108
-
key={tag}
109
-
to={`/?tag=${encodeURIComponent(tag)}`}
110
-
className="right-link"
111
-
>
112
-
<span>#{tag}</span>
113
-
<span style={{ fontSize: "0.75rem", opacity: 0.6 }}>
114
-
{count}
115
-
</span>
116
-
</Link>
117
-
))
118
-
) : (
119
-
<span className="right-section-desc">No trending tags yet</span>
120
-
)}
121
-
</div>
122
-
</div>
123
-
) : (
124
-
<div className="right-section">
125
-
<h3 className="right-section-title">Explore</h3>
126
-
<nav className="right-links">
127
-
<Link to="/url" className="right-link">
128
-
Browse by URL
129
-
</Link>
130
-
</nav>
131
-
</div>
132
-
)}
133
-
134
-
<div className="right-section">
135
-
<h3 className="right-section-title">Resources</h3>
136
-
<nav className="right-links">
137
-
<a
138
-
href="https://github.com/margin-at/margin"
139
-
target="_blank"
140
-
rel="noopener noreferrer"
141
-
className="right-link"
142
-
>
143
-
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
144
-
<SiGithub size={16} />
145
-
GitHub
146
-
</div>
147
-
<ExternalLink size={12} />
148
-
</a>
149
-
<a
150
-
href="https://tangled.org/margin.at/margin"
151
-
target="_blank"
152
-
rel="noopener noreferrer"
153
-
className="right-link"
154
-
>
155
-
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
156
-
<div className="tangled-icon" />
157
-
Tangled
158
-
</div>
159
-
<ExternalLink size={12} />
160
-
</a>
161
-
<a
162
-
href="https://bsky.app/profile/margin.at"
163
-
target="_blank"
164
-
rel="noopener noreferrer"
165
-
className="right-link"
166
-
>
167
-
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
168
-
<SiBluesky size={16} />
169
-
Bluesky
170
-
</div>
171
-
<ExternalLink size={12} />
172
-
</a>
173
-
<a
174
-
href="https://discord.gg/ZQbkGqwzBH"
175
-
target="_blank"
176
-
rel="noopener noreferrer"
177
-
className="right-link"
178
-
>
179
-
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
180
-
<SiDiscord size={16} />
181
-
Discord
182
-
</div>
183
-
<ExternalLink size={12} />
184
-
</a>
185
-
<a
186
-
href="https://ko-fi.com/scan"
187
-
target="_blank"
188
-
rel="noopener noreferrer"
189
-
className="right-link"
190
-
>
191
-
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
192
-
<SiKofi size={16} />
193
-
Donate
194
-
</div>
195
-
<ExternalLink size={12} />
196
-
</a>
197
-
</nav>
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
-
);
226
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-189
web/src/components/Sidebar.jsx
···
1
-
import { useState, useRef, useEffect } from "react";
2
-
import { Link, useLocation } from "react-router-dom";
3
-
import { useAuth } from "../context/AuthContext";
4
-
import {
5
-
Home,
6
-
Search,
7
-
Folder,
8
-
Bell,
9
-
PenSquare,
10
-
User,
11
-
LogOut,
12
-
MoreHorizontal,
13
-
Highlighter,
14
-
Bookmark,
15
-
} from "lucide-react";
16
-
import { getUnreadNotificationCount } from "../api/client";
17
-
import logo from "../assets/logo.svg";
18
-
19
-
export default function Sidebar() {
20
-
const { user, isAuthenticated, logout, loading } = useAuth();
21
-
const location = useLocation();
22
-
const [menuOpen, setMenuOpen] = useState(false);
23
-
const [unreadCount, setUnreadCount] = useState(0);
24
-
const menuRef = useRef(null);
25
-
26
-
const isActive = (path) => {
27
-
if (path === "/") return location.pathname === "/";
28
-
return location.pathname.startsWith(path);
29
-
};
30
-
31
-
useEffect(() => {
32
-
if (isAuthenticated) {
33
-
getUnreadNotificationCount()
34
-
.then((data) => setUnreadCount(data.count || 0))
35
-
.catch(() => {});
36
-
const interval = setInterval(() => {
37
-
getUnreadNotificationCount()
38
-
.then((data) => setUnreadCount(data.count || 0))
39
-
.catch(() => {});
40
-
}, 60000);
41
-
return () => clearInterval(interval);
42
-
}
43
-
}, [isAuthenticated]);
44
-
45
-
useEffect(() => {
46
-
const handleClickOutside = (e) => {
47
-
if (menuRef.current && !menuRef.current.contains(e.target)) {
48
-
setMenuOpen(false);
49
-
}
50
-
};
51
-
document.addEventListener("mousedown", handleClickOutside);
52
-
return () => document.removeEventListener("mousedown", handleClickOutside);
53
-
}, []);
54
-
55
-
const getInitials = () => {
56
-
if (user?.displayName) {
57
-
return user.displayName.substring(0, 2).toUpperCase();
58
-
}
59
-
if (user?.handle) {
60
-
return user.handle.substring(0, 2).toUpperCase();
61
-
}
62
-
return "U";
63
-
};
64
-
65
-
return (
66
-
<aside className="sidebar">
67
-
<Link to="/" className="sidebar-header">
68
-
<img src={logo} alt="Margin" className="sidebar-logo" />
69
-
<span className="sidebar-brand">Margin</span>
70
-
</Link>
71
-
72
-
<nav className="sidebar-nav">
73
-
<Link
74
-
to="/"
75
-
className={`sidebar-link ${isActive("/") ? "active" : ""}`}
76
-
>
77
-
<Home size={20} />
78
-
<span>Home</span>
79
-
</Link>
80
-
<Link
81
-
to="/url"
82
-
className={`sidebar-link ${isActive("/url") ? "active" : ""}`}
83
-
>
84
-
<Search size={20} />
85
-
<span>Browse</span>
86
-
</Link>
87
-
88
-
{isAuthenticated && (
89
-
<>
90
-
<div className="sidebar-section-title">Library</div>
91
-
<Link
92
-
to="/highlights"
93
-
className={`sidebar-link ${isActive("/highlights") ? "active" : ""}`}
94
-
>
95
-
<Highlighter size={20} />
96
-
<span>Highlights</span>
97
-
</Link>
98
-
<Link
99
-
to="/bookmarks"
100
-
className={`sidebar-link ${isActive("/bookmarks") ? "active" : ""}`}
101
-
>
102
-
<Bookmark size={20} />
103
-
<span>Bookmarks</span>
104
-
</Link>
105
-
<Link
106
-
to="/collections"
107
-
className={`sidebar-link ${isActive("/collections") ? "active" : ""}`}
108
-
>
109
-
<Folder size={20} />
110
-
<span>Collections</span>
111
-
</Link>
112
-
<Link
113
-
to="/notifications"
114
-
className={`sidebar-link ${isActive("/notifications") ? "active" : ""}`}
115
-
onClick={() => setUnreadCount(0)}
116
-
>
117
-
<Bell size={20} />
118
-
<span>Notifications</span>
119
-
{unreadCount > 0 && (
120
-
<span className="notification-badge">{unreadCount}</span>
121
-
)}
122
-
</Link>
123
-
</>
124
-
)}
125
-
</nav>
126
-
127
-
{isAuthenticated && (
128
-
<Link to="/new" className="sidebar-new-btn">
129
-
<PenSquare size={18} />
130
-
<span>New</span>
131
-
</Link>
132
-
)}
133
-
134
-
<div className="sidebar-footer" ref={menuRef}>
135
-
{!loading &&
136
-
(isAuthenticated ? (
137
-
<>
138
-
<div
139
-
className="sidebar-user"
140
-
onClick={() => setMenuOpen(!menuOpen)}
141
-
>
142
-
<div className="sidebar-avatar">
143
-
{user?.avatar ? (
144
-
<img src={user.avatar} alt={user.displayName} />
145
-
) : (
146
-
<span>{getInitials()}</span>
147
-
)}
148
-
</div>
149
-
<div className="sidebar-user-info">
150
-
<div className="sidebar-user-name">
151
-
{user?.displayName || user?.handle}
152
-
</div>
153
-
<div className="sidebar-user-handle">@{user?.handle}</div>
154
-
</div>
155
-
<MoreHorizontal size={18} className="sidebar-user-menu" />
156
-
</div>
157
-
158
-
{menuOpen && (
159
-
<div className="sidebar-dropdown">
160
-
<Link
161
-
to={`/profile/${user?.did}`}
162
-
className="sidebar-dropdown-item"
163
-
onClick={() => setMenuOpen(false)}
164
-
>
165
-
<User size={16} />
166
-
View Profile
167
-
</Link>
168
-
<button
169
-
onClick={() => {
170
-
logout();
171
-
setMenuOpen(false);
172
-
}}
173
-
className="sidebar-dropdown-item danger"
174
-
>
175
-
<LogOut size={16} />
176
-
Sign Out
177
-
</button>
178
-
</div>
179
-
)}
180
-
</>
181
-
) : (
182
-
<Link to="/login" className="sidebar-new-btn" style={{ margin: 0 }}>
183
-
Sign In
184
-
</Link>
185
-
))}
186
-
</div>
187
-
</aside>
188
-
);
189
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
+408
web/src/components/TopNav.jsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { useState, useRef, useEffect } from "react";
2
+
import { Link, useLocation } from "react-router-dom";
3
+
import { useAuth } from "../context/AuthContext";
4
+
import { useTheme } from "../context/ThemeContext";
5
+
import {
6
+
Home,
7
+
Search,
8
+
Folder,
9
+
Bell,
10
+
PenSquare,
11
+
User,
12
+
LogOut,
13
+
ChevronDown,
14
+
Highlighter,
15
+
Bookmark,
16
+
Sun,
17
+
Moon,
18
+
Monitor,
19
+
ExternalLink,
20
+
Menu,
21
+
X,
22
+
} from "lucide-react";
23
+
import {
24
+
SiFirefox,
25
+
SiGooglechrome,
26
+
SiGithub,
27
+
SiBluesky,
28
+
SiDiscord,
29
+
} from "react-icons/si";
30
+
import { FaEdge } from "react-icons/fa";
31
+
import tangledLogo from "../assets/tangled.svg";
32
+
import { getUnreadNotificationCount } from "../api/client";
33
+
import logo from "../assets/logo.svg";
34
+
35
+
const isFirefox =
36
+
typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent);
37
+
const isEdge =
38
+
typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent);
39
+
40
+
function getExtensionInfo() {
41
+
if (isFirefox) {
42
+
return {
43
+
url: "https://addons.mozilla.org/en-US/firefox/addon/margin/",
44
+
icon: SiFirefox,
45
+
label: "Firefox",
46
+
};
47
+
}
48
+
if (isEdge) {
49
+
return {
50
+
url: "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn",
51
+
icon: FaEdge,
52
+
label: "Edge",
53
+
};
54
+
}
55
+
return {
56
+
url: "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/",
57
+
icon: SiGooglechrome,
58
+
label: "Chrome",
59
+
};
60
+
}
61
+
62
+
export default function TopNav() {
63
+
const { user, isAuthenticated, logout, loading } = useAuth();
64
+
const { theme, setTheme } = useTheme();
65
+
const location = useLocation();
66
+
const [userMenuOpen, setUserMenuOpen] = useState(false);
67
+
const [moreMenuOpen, setMoreMenuOpen] = useState(false);
68
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
69
+
const [unreadCount, setUnreadCount] = useState(0);
70
+
const userMenuRef = useRef(null);
71
+
const moreMenuRef = useRef(null);
72
+
73
+
const isActive = (path) => {
74
+
if (path === "/") return location.pathname === "/";
75
+
return location.pathname.startsWith(path);
76
+
};
77
+
78
+
const ext = getExtensionInfo();
79
+
const ExtIcon = ext.icon;
80
+
81
+
useEffect(() => {
82
+
if (isAuthenticated) {
83
+
getUnreadNotificationCount()
84
+
.then((data) => setUnreadCount(data.count || 0))
85
+
.catch(() => {});
86
+
const interval = setInterval(() => {
87
+
getUnreadNotificationCount()
88
+
.then((data) => setUnreadCount(data.count || 0))
89
+
.catch(() => {});
90
+
}, 60000);
91
+
return () => clearInterval(interval);
92
+
}
93
+
}, [isAuthenticated]);
94
+
95
+
useEffect(() => {
96
+
const handleClickOutside = (e) => {
97
+
if (userMenuRef.current && !userMenuRef.current.contains(e.target)) {
98
+
setUserMenuOpen(false);
99
+
}
100
+
if (moreMenuRef.current && !moreMenuRef.current.contains(e.target)) {
101
+
setMoreMenuOpen(false);
102
+
}
103
+
};
104
+
document.addEventListener("mousedown", handleClickOutside);
105
+
return () => document.removeEventListener("mousedown", handleClickOutside);
106
+
}, []);
107
+
108
+
const closeMobileMenu = () => setMobileMenuOpen(false);
109
+
110
+
const getInitials = () => {
111
+
if (user?.displayName)
112
+
return user.displayName.substring(0, 2).toUpperCase();
113
+
if (user?.handle) return user.handle.substring(0, 2).toUpperCase();
114
+
return "U";
115
+
};
116
+
117
+
const cycleTheme = () => {
118
+
const next =
119
+
theme === "system" ? "light" : theme === "light" ? "dark" : "system";
120
+
setTheme(next);
121
+
};
122
+
123
+
return (
124
+
<header className="top-nav">
125
+
<div className="top-nav-inner">
126
+
<Link to="/" className="top-nav-logo">
127
+
<img src={logo} alt="Margin" />
128
+
<span>Margin</span>
129
+
</Link>
130
+
131
+
<nav className="top-nav-links">
132
+
<Link
133
+
to="/"
134
+
className={`top-nav-link ${isActive("/") ? "active" : ""}`}
135
+
>
136
+
Home
137
+
</Link>
138
+
<Link
139
+
to="/url"
140
+
className={`top-nav-link ${isActive("/url") ? "active" : ""}`}
141
+
>
142
+
Browse
143
+
</Link>
144
+
{isAuthenticated && (
145
+
<>
146
+
<Link
147
+
to="/highlights"
148
+
className={`top-nav-link ${isActive("/highlights") ? "active" : ""}`}
149
+
>
150
+
Highlights
151
+
</Link>
152
+
<Link
153
+
to="/bookmarks"
154
+
className={`top-nav-link ${isActive("/bookmarks") ? "active" : ""}`}
155
+
>
156
+
Bookmarks
157
+
</Link>
158
+
<Link
159
+
to="/collections"
160
+
className={`top-nav-link ${isActive("/collections") ? "active" : ""}`}
161
+
>
162
+
Collections
163
+
</Link>
164
+
</>
165
+
)}
166
+
</nav>
167
+
168
+
<div className="top-nav-actions">
169
+
<a
170
+
href={ext.url}
171
+
target="_blank"
172
+
rel="noopener noreferrer"
173
+
className="top-nav-link extension-link"
174
+
title={`Get ${ext.label} Extension`}
175
+
>
176
+
<ExtIcon size={16} />
177
+
<span>Get Extension</span>
178
+
</a>
179
+
180
+
<div className="top-nav-dropdown" ref={moreMenuRef}>
181
+
<button
182
+
className="top-nav-icon-btn"
183
+
onClick={() => setMoreMenuOpen(!moreMenuOpen)}
184
+
title="More"
185
+
>
186
+
<ChevronDown size={18} />
187
+
</button>
188
+
{moreMenuOpen && (
189
+
<div className="dropdown-menu dropdown-right">
190
+
<a
191
+
href="https://github.com/margin-at/margin"
192
+
target="_blank"
193
+
rel="noopener noreferrer"
194
+
className="dropdown-item"
195
+
>
196
+
<SiGithub size={16} />
197
+
GitHub
198
+
<ExternalLink size={12} className="dropdown-external" />
199
+
</a>
200
+
<a
201
+
href="https://tangled.sh/@margin.at/margin"
202
+
target="_blank"
203
+
rel="noopener noreferrer"
204
+
className="dropdown-item"
205
+
>
206
+
<span className="tangled-icon-wrapper">
207
+
<img src={tangledLogo} alt="" />
208
+
</span>
209
+
Tangled
210
+
<ExternalLink size={12} className="dropdown-external" />
211
+
</a>
212
+
<a
213
+
href="https://bsky.app/profile/margin.at"
214
+
target="_blank"
215
+
rel="noopener noreferrer"
216
+
className="dropdown-item"
217
+
>
218
+
<SiBluesky size={16} />
219
+
Bluesky
220
+
<ExternalLink size={12} className="dropdown-external" />
221
+
</a>
222
+
<a
223
+
href="https://discord.gg/ZQbkGqwzBH"
224
+
target="_blank"
225
+
rel="noopener noreferrer"
226
+
className="dropdown-item"
227
+
>
228
+
<SiDiscord size={16} />
229
+
Discord
230
+
<ExternalLink size={12} className="dropdown-external" />
231
+
</a>
232
+
<div className="dropdown-divider" />
233
+
<button className="dropdown-item" onClick={cycleTheme}>
234
+
{theme === "system" && <Monitor size={16} />}
235
+
{theme === "dark" && <Moon size={16} />}
236
+
{theme === "light" && <Sun size={16} />}
237
+
Theme: {theme}
238
+
</button>
239
+
<div className="dropdown-divider" />
240
+
<Link
241
+
to="/privacy"
242
+
className="dropdown-item"
243
+
onClick={() => setMoreMenuOpen(false)}
244
+
>
245
+
Privacy
246
+
</Link>
247
+
<Link
248
+
to="/terms"
249
+
className="dropdown-item"
250
+
onClick={() => setMoreMenuOpen(false)}
251
+
>
252
+
Terms
253
+
</Link>
254
+
</div>
255
+
)}
256
+
</div>
257
+
258
+
{isAuthenticated && (
259
+
<>
260
+
<Link
261
+
to="/notifications"
262
+
className="top-nav-icon-btn"
263
+
onClick={() => setUnreadCount(0)}
264
+
title="Notifications"
265
+
>
266
+
<Bell size={18} />
267
+
{unreadCount > 0 && <span className="notif-dot" />}
268
+
</Link>
269
+
270
+
<Link to="/new" className="top-nav-new-btn">
271
+
<PenSquare size={16} />
272
+
<span>New</span>
273
+
</Link>
274
+
</>
275
+
)}
276
+
277
+
{!loading &&
278
+
(isAuthenticated ? (
279
+
<div className="top-nav-dropdown" ref={userMenuRef}>
280
+
<button
281
+
className="top-nav-avatar"
282
+
onClick={() => setUserMenuOpen(!userMenuOpen)}
283
+
>
284
+
{user?.avatar ? (
285
+
<img src={user.avatar} alt={user.displayName} />
286
+
) : (
287
+
<span>{getInitials()}</span>
288
+
)}
289
+
</button>
290
+
{userMenuOpen && (
291
+
<div className="dropdown-menu dropdown-right">
292
+
<div className="dropdown-user-info">
293
+
<span className="dropdown-user-name">
294
+
{user?.displayName || user?.handle}
295
+
</span>
296
+
<span className="dropdown-user-handle">
297
+
@{user?.handle}
298
+
</span>
299
+
</div>
300
+
<div className="dropdown-divider" />
301
+
<Link
302
+
to={`/profile/${user?.did}`}
303
+
className="dropdown-item"
304
+
onClick={() => setUserMenuOpen(false)}
305
+
>
306
+
<User size={16} />
307
+
View Profile
308
+
</Link>
309
+
<button
310
+
onClick={() => {
311
+
logout();
312
+
setUserMenuOpen(false);
313
+
}}
314
+
className="dropdown-item danger"
315
+
>
316
+
<LogOut size={16} />
317
+
Sign Out
318
+
</button>
319
+
</div>
320
+
)}
321
+
</div>
322
+
) : (
323
+
<Link to="/login" className="top-nav-new-btn">
324
+
Sign In
325
+
</Link>
326
+
))}
327
+
328
+
<button
329
+
className="top-nav-mobile-toggle"
330
+
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
331
+
>
332
+
{mobileMenuOpen ? <X size={22} /> : <Menu size={22} />}
333
+
</button>
334
+
</div>
335
+
</div>
336
+
337
+
{mobileMenuOpen && (
338
+
<div className="mobile-menu">
339
+
<Link
340
+
to="/"
341
+
className={`mobile-menu-link ${isActive("/") ? "active" : ""}`}
342
+
onClick={closeMobileMenu}
343
+
>
344
+
<Home size={20} /> Home
345
+
</Link>
346
+
<Link
347
+
to="/url"
348
+
className={`mobile-menu-link ${isActive("/url") ? "active" : ""}`}
349
+
onClick={closeMobileMenu}
350
+
>
351
+
<Search size={20} /> Browse
352
+
</Link>
353
+
{isAuthenticated && (
354
+
<>
355
+
<Link
356
+
to="/highlights"
357
+
className={`mobile-menu-link ${isActive("/highlights") ? "active" : ""}`}
358
+
onClick={closeMobileMenu}
359
+
>
360
+
<Highlighter size={20} /> Highlights
361
+
</Link>
362
+
<Link
363
+
to="/bookmarks"
364
+
className={`mobile-menu-link ${isActive("/bookmarks") ? "active" : ""}`}
365
+
onClick={closeMobileMenu}
366
+
>
367
+
<Bookmark size={20} /> Bookmarks
368
+
</Link>
369
+
<Link
370
+
to="/collections"
371
+
className={`mobile-menu-link ${isActive("/collections") ? "active" : ""}`}
372
+
onClick={closeMobileMenu}
373
+
>
374
+
<Folder size={20} /> Collections
375
+
</Link>
376
+
<Link
377
+
to="/notifications"
378
+
className={`mobile-menu-link ${isActive("/notifications") ? "active" : ""}`}
379
+
onClick={closeMobileMenu}
380
+
>
381
+
<Bell size={20} /> Notifications
382
+
{unreadCount > 0 && (
383
+
<span className="notification-badge">{unreadCount}</span>
384
+
)}
385
+
</Link>
386
+
<Link
387
+
to="/new"
388
+
className={`mobile-menu-link ${isActive("/new") ? "active" : ""}`}
389
+
onClick={closeMobileMenu}
390
+
>
391
+
<PenSquare size={20} /> New
392
+
</Link>
393
+
</>
394
+
)}
395
+
<div className="mobile-menu-divider" />
396
+
<a
397
+
href={ext.url}
398
+
target="_blank"
399
+
rel="noopener noreferrer"
400
+
className="mobile-menu-link"
401
+
>
402
+
<ExtIcon size={20} /> Get Extension
403
+
</a>
404
+
</div>
405
+
)}
406
+
</header>
407
+
);
408
+
}
+126
-96
web/src/css/annotations.css
···
1
.annotation-detail-page {
2
-
max-width: 680px;
3
margin: 0 auto;
4
-
padding: 24px 16px;
5
min-height: 100vh;
6
}
7
8
.annotation-detail-header {
9
-
margin-bottom: 24px;
10
}
11
12
.back-link {
···
14
align-items: center;
15
color: var(--text-tertiary);
16
text-decoration: none;
17
-
font-size: 0.9rem;
18
font-weight: 500;
19
transition: color 0.15s;
20
}
···
24
}
25
26
.replies-section {
27
-
margin-top: 32px;
28
border-top: 1px solid var(--border);
29
-
padding-top: 24px;
30
}
31
32
.replies-title {
33
display: flex;
34
align-items: center;
35
-
gap: 8px;
36
-
font-size: 1.1rem;
37
font-weight: 600;
38
color: var(--text-primary);
39
-
margin-bottom: 20px;
40
}
41
42
.annotation-card {
43
display: flex;
44
flex-direction: column;
45
-
gap: 12px;
46
-
padding: 20px 0;
47
-
border-bottom: 1px solid var(--border);
48
-
transition: background 0.15s ease;
0
0
0
0
0
49
}
50
51
-
.annotation-card:last-child {
52
-
border-bottom: none;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
53
}
54
55
.annotation-header {
56
display: flex;
57
justify-content: space-between;
58
align-items: flex-start;
59
-
gap: 12px;
60
}
61
62
.annotation-header-left {
63
display: flex;
64
align-items: center;
65
-
gap: 10px;
66
flex: 1;
67
min-width: 0;
68
}
69
70
.annotation-avatar {
71
-
width: 36px;
72
-
height: 36px;
73
-
min-width: 36px;
74
-
border-radius: 50%;
75
background: var(--bg-tertiary);
76
display: flex;
77
align-items: center;
78
justify-content: center;
79
font-weight: 600;
80
-
font-size: 0.85rem;
81
color: var(--text-secondary);
82
overflow: hidden;
83
}
···
92
display: flex;
93
flex-direction: column;
94
justify-content: center;
95
-
line-height: 1.3;
0
0
96
}
97
98
.annotation-avatar-link {
99
text-decoration: none;
100
-
border-radius: 50%;
101
}
102
103
.annotation-author-row {
104
display: flex;
105
align-items: baseline;
106
-
gap: 6px;
107
flex-wrap: wrap;
108
}
109
110
.annotation-author {
111
font-weight: 600;
112
color: var(--text-primary);
113
-
font-size: 0.9rem;
114
}
115
116
.annotation-handle {
117
-
font-size: 0.85rem;
118
color: var(--text-tertiary);
119
text-decoration: none;
120
}
···
131
.annotation-content {
132
display: flex;
133
flex-direction: column;
134
-
gap: 10px;
135
-
padding-left: 46px;
0
0
136
}
137
138
.annotation-source {
139
display: inline-flex;
140
align-items: center;
141
gap: 6px;
142
-
font-size: 0.75rem;
143
-
color: var(--text-tertiary);
144
text-decoration: none;
145
transition: color 0.15s ease;
146
max-width: 100%;
147
overflow: hidden;
148
-
text-overflow: ellipsis;
149
-
white-space: nowrap;
150
}
151
152
.annotation-source:hover {
153
-
color: var(--text-secondary);
154
text-decoration: underline;
155
}
156
157
.annotation-source-title {
158
-
color: var(--text-tertiary);
159
-
opacity: 0.7;
0
0
0
160
}
161
162
.annotation-highlight {
163
display: block;
164
position: relative;
165
-
padding-left: 12px;
166
-
margin: 4px 0;
167
text-decoration: none;
168
-
border-left: 2px solid var(--border);
0
0
169
transition: all 0.15s ease;
0
0
170
}
171
172
.annotation-highlight:hover {
173
-
border-left-color: var(--text-secondary);
174
}
175
176
.annotation-highlight mark {
177
background: transparent;
178
color: var(--text-primary);
179
font-style: italic;
180
-
font-size: 1rem;
181
-
line-height: 1.6;
182
font-weight: 400;
183
-
font-family: var(--font-serif, var(--font-sans));
184
-
display: inline;
185
-
overflow-wrap: anywhere;
186
-
word-break: break-all;
187
-
padding-right: 4px;
188
}
189
190
.annotation-text {
191
-
font-size: 0.95rem;
192
-
line-height: 1.6;
193
color: var(--text-primary);
194
white-space: pre-wrap;
195
}
···
198
display: flex;
199
flex-wrap: wrap;
200
gap: 6px;
201
-
margin-top: 4px;
202
}
203
204
.annotation-tag {
205
-
font-size: 0.8rem;
206
color: var(--accent);
207
text-decoration: none;
208
font-weight: 500;
209
-
opacity: 0.9;
210
transition: opacity 0.15s;
211
}
212
213
.annotation-tag:hover {
214
-
opacity: 1;
215
text-decoration: underline;
216
}
217
218
.annotation-actions {
219
display: flex;
220
align-items: center;
221
-
justify-content: space-between;
0
0
222
margin-top: 4px;
223
-
padding-left: 46px;
224
}
225
226
.annotation-actions-left {
227
display: flex;
228
align-items: center;
229
-
gap: 16px;
230
}
231
232
.annotation-action {
233
display: flex;
234
align-items: center;
235
-
gap: 6px;
236
color: var(--text-tertiary);
237
font-size: 0.8rem;
238
font-weight: 500;
239
-
padding: 6px;
240
-
margin-left: -6px;
241
-
border-radius: var(--radius-sm);
242
transition: all 0.15s ease;
243
background: transparent;
244
cursor: pointer;
···
251
}
252
253
.annotation-action.liked {
254
-
color: #ef4444;
255
}
256
257
.annotation-action.liked svg {
258
-
fill: #ef4444;
259
}
260
261
.annotation-action.active {
···
263
}
264
265
.action-icon-only {
266
-
padding: 6px;
267
}
268
269
.annotation-header-right {
···
276
}
277
278
.inline-replies {
279
-
margin-top: 12px;
280
-
padding-left: 46px;
0
281
}
282
283
.annotation-text,
···
288
max-width: 100%;
289
}
290
291
-
.annotation-highlight mark {
292
-
overflow-wrap: break-word;
293
-
word-break: break-word;
294
-
display: inline;
295
-
}
296
-
297
.annotation-header-left,
298
.annotation-meta,
299
.reply-meta {
···
306
max-width: 100%;
307
}
308
309
-
.annotation-source {
310
-
max-width: 100%;
311
-
}
312
-
313
@media (max-width: 768px) {
314
.annotation-content,
315
.annotation-actions,
···
320
.annotation-header-right {
321
opacity: 1;
322
}
0
0
0
0
0
0
0
0
0
0
323
}
324
325
.replies-list-threaded {
326
-
margin-top: 16px;
327
display: flex;
328
flex-direction: column;
329
}
···
331
.reply-card-threaded {
332
position: relative;
333
padding-left: 0;
0
334
transition: background 0.15s;
335
}
336
337
.reply-header {
338
display: flex;
339
align-items: center;
340
-
gap: 10px;
341
-
margin-bottom: 6px;
342
}
343
344
.reply-avatar {
345
width: 28px;
346
height: 28px;
347
-
border-radius: 50%;
348
background: var(--bg-tertiary);
349
overflow: hidden;
350
flex-shrink: 0;
···
368
.reply-meta {
369
display: flex;
370
align-items: baseline;
371
-
gap: 6px;
372
flex: 1;
373
min-width: 0;
374
}
375
376
.reply-author {
377
font-weight: 600;
378
-
font-size: 0.85rem;
379
color: var(--text-primary);
380
white-space: nowrap;
381
overflow: hidden;
···
392
}
393
394
.reply-time {
395
-
font-size: 0.75rem;
396
color: var(--text-tertiary);
397
white-space: nowrap;
398
}
···
407
line-height: 1.5;
408
color: var(--text-primary);
409
margin: 0;
410
-
padding-left: 38px;
411
}
412
413
.reply-actions {
···
428
padding: 4px;
429
color: var(--text-tertiary);
430
cursor: pointer;
431
-
border-radius: 4px;
432
display: flex;
433
align-items: center;
434
justify-content: center;
···
440
}
441
442
.reply-action-delete:hover {
443
-
color: #ef4444;
444
-
background: rgba(239, 68, 68, 0.1);
445
}
446
447
.reply-form {
448
border: 1px solid var(--border);
449
border-radius: var(--radius-md);
450
-
padding: 16px;
451
background: var(--bg-secondary);
452
-
margin-bottom: 24px;
453
}
454
455
.replying-to-banner {
···
457
justify-content: space-between;
458
align-items: center;
459
background: var(--bg-tertiary);
460
-
padding: 8px 12px;
461
border-radius: var(--radius-sm);
462
-
margin-bottom: 12px;
463
-
font-size: 0.85rem;
464
color: var(--text-secondary);
465
}
466
···
469
border: none;
470
color: var(--text-tertiary);
471
cursor: pointer;
472
-
font-size: 1.2rem;
473
padding: 0 4px;
474
line-height: 1;
475
}
···
483
background: var(--bg-primary);
484
border: 1px solid var(--border);
485
border-radius: var(--radius-sm);
486
-
padding: 12px;
487
color: var(--text-primary);
488
font-family: inherit;
489
-
font-size: 0.95rem;
490
resize: vertical;
491
-
min-height: 80px;
492
transition: border-color 0.15s;
493
display: block;
494
box-sizing: border-box;
···
502
.reply-form-actions {
503
display: flex;
504
justify-content: flex-end;
505
-
margin-top: 12px;
506
}
507
508
.rich-text-link {
···
1
.annotation-detail-page {
2
+
max-width: 640px;
3
margin: 0 auto;
0
4
min-height: 100vh;
5
}
6
7
.annotation-detail-header {
8
+
margin-bottom: var(--spacing-md);
9
}
10
11
.back-link {
···
13
align-items: center;
14
color: var(--text-tertiary);
15
text-decoration: none;
16
+
font-size: 0.8rem;
17
font-weight: 500;
18
transition: color 0.15s;
19
}
···
23
}
24
25
.replies-section {
26
+
margin-top: var(--spacing-lg);
27
border-top: 1px solid var(--border);
28
+
padding-top: var(--spacing-md);
29
}
30
31
.replies-title {
32
display: flex;
33
align-items: center;
34
+
gap: 6px;
35
+
font-size: 0.9rem;
36
font-weight: 600;
37
color: var(--text-primary);
38
+
margin-bottom: var(--spacing-md);
39
}
40
41
.annotation-card {
42
display: flex;
43
flex-direction: column;
44
+
gap: 8px;
45
+
padding: 16px 20px;
46
+
transition: all 0.15s ease;
47
+
width: 100%;
48
+
box-sizing: border-box;
49
+
overflow: visible;
50
+
background: var(--bg-primary);
51
+
border: none;
52
+
position: relative;
53
}
54
55
+
.feed > .annotation-card,
56
+
.feed > .card {
57
+
border-radius: 0;
58
+
}
59
+
60
+
.feed > .annotation-card:first-child,
61
+
.feed > .card:first-child {
62
+
border-top-left-radius: var(--radius-lg) !important;
63
+
border-top-right-radius: var(--radius-lg) !important;
64
+
}
65
+
66
+
.feed > .annotation-card:last-child,
67
+
.feed > .card:last-child {
68
+
border-bottom-left-radius: var(--radius-lg) !important;
69
+
border-bottom-right-radius: var(--radius-lg) !important;
70
+
}
71
+
72
+
.feed > .annotation-card:only-child,
73
+
.feed > .card:only-child {
74
+
border-radius: var(--radius-lg) !important;
75
}
76
77
.annotation-header {
78
display: flex;
79
justify-content: space-between;
80
align-items: flex-start;
81
+
gap: var(--spacing-sm);
82
}
83
84
.annotation-header-left {
85
display: flex;
86
align-items: center;
87
+
gap: 8px;
88
flex: 1;
89
min-width: 0;
90
}
91
92
.annotation-avatar {
93
+
width: 32px;
94
+
height: 32px;
95
+
min-width: 32px;
96
+
border-radius: var(--radius-full);
97
background: var(--bg-tertiary);
98
display: flex;
99
align-items: center;
100
justify-content: center;
101
font-weight: 600;
102
+
font-size: 0.75rem;
103
color: var(--text-secondary);
104
overflow: hidden;
105
}
···
114
display: flex;
115
flex-direction: column;
116
justify-content: center;
117
+
line-height: 1.4;
118
+
min-width: 0;
119
+
flex: 1;
120
}
121
122
.annotation-avatar-link {
123
text-decoration: none;
124
+
border-radius: var(--radius-full);
125
}
126
127
.annotation-author-row {
128
display: flex;
129
align-items: baseline;
130
+
gap: 8px;
131
flex-wrap: wrap;
132
}
133
134
.annotation-author {
135
font-weight: 600;
136
color: var(--text-primary);
137
+
font-size: 0.875rem;
138
}
139
140
.annotation-handle {
141
+
font-size: 0.8rem;
142
color: var(--text-tertiary);
143
text-decoration: none;
144
}
···
155
.annotation-content {
156
display: flex;
157
flex-direction: column;
158
+
gap: 8px;
159
+
padding-left: 0;
160
+
max-width: 100%;
161
+
overflow: hidden;
162
}
163
164
.annotation-source {
165
display: inline-flex;
166
align-items: center;
167
gap: 6px;
168
+
font-size: 0.8rem;
169
+
color: var(--accent);
170
text-decoration: none;
171
transition: color 0.15s ease;
172
max-width: 100%;
173
overflow: hidden;
0
0
174
}
175
176
.annotation-source:hover {
0
177
text-decoration: underline;
178
}
179
180
.annotation-source-title {
181
+
color: var(--text-primary);
182
+
font-weight: 500;
183
+
overflow: hidden;
184
+
text-overflow: ellipsis;
185
+
white-space: nowrap;
186
}
187
188
.annotation-highlight {
189
display: block;
190
position: relative;
191
+
padding: 10px 14px;
192
+
margin: 0;
193
text-decoration: none;
194
+
background: var(--bg-tertiary);
195
+
border-left: 3px solid var(--accent);
196
+
border-radius: 0 var(--radius-md) var(--radius-md) 0;
197
transition: all 0.15s ease;
198
+
max-width: 100%;
199
+
overflow: hidden;
200
}
201
202
.annotation-highlight:hover {
203
+
background: var(--bg-hover);
204
}
205
206
.annotation-highlight mark {
207
background: transparent;
208
color: var(--text-primary);
209
font-style: italic;
210
+
font-size: 0.875rem;
211
+
line-height: 1.5;
212
font-weight: 400;
213
+
display: block;
214
+
overflow-wrap: break-word;
215
+
word-break: break-word;
0
0
216
}
217
218
.annotation-text {
219
+
font-size: 1rem;
220
+
line-height: 1.7;
221
color: var(--text-primary);
222
white-space: pre-wrap;
223
}
···
226
display: flex;
227
flex-wrap: wrap;
228
gap: 6px;
229
+
margin-top: 2px;
230
}
231
232
.annotation-tag {
233
+
font-size: 0.75rem;
234
color: var(--accent);
235
text-decoration: none;
236
font-weight: 500;
0
237
transition: opacity 0.15s;
238
}
239
240
.annotation-tag:hover {
241
+
opacity: 0.8;
242
text-decoration: underline;
243
}
244
245
.annotation-actions {
246
display: flex;
247
align-items: center;
248
+
justify-content: flex-start;
249
+
gap: 4px;
250
+
padding-left: 0;
251
margin-top: 4px;
252
+
position: relative;
253
}
254
255
.annotation-actions-left {
256
display: flex;
257
align-items: center;
258
+
gap: 8px;
259
}
260
261
.annotation-action {
262
display: flex;
263
align-items: center;
264
+
gap: 5px;
265
color: var(--text-tertiary);
266
font-size: 0.8rem;
267
font-weight: 500;
268
+
padding: 6px 10px;
269
+
border-radius: var(--radius-md);
0
270
transition: all 0.15s ease;
271
background: transparent;
272
cursor: pointer;
···
279
}
280
281
.annotation-action.liked {
282
+
color: var(--error);
283
}
284
285
.annotation-action.liked svg {
286
+
fill: var(--error);
287
}
288
289
.annotation-action.active {
···
291
}
292
293
.action-icon-only {
294
+
padding: 4px;
295
}
296
297
.annotation-header-right {
···
304
}
305
306
.inline-replies {
307
+
margin-top: var(--spacing-sm);
308
+
padding-left: 0;
309
+
position: relative;
310
}
311
312
.annotation-text,
···
317
max-width: 100%;
318
}
319
0
0
0
0
0
0
320
.annotation-header-left,
321
.annotation-meta,
322
.reply-meta {
···
329
max-width: 100%;
330
}
331
0
0
0
0
332
@media (max-width: 768px) {
333
.annotation-content,
334
.annotation-actions,
···
339
.annotation-header-right {
340
opacity: 1;
341
}
342
+
343
+
.annotation-card {
344
+
padding: 16px;
345
+
}
346
+
347
+
.annotation-avatar {
348
+
width: 36px;
349
+
height: 36px;
350
+
min-width: 36px;
351
+
}
352
}
353
354
.replies-list-threaded {
355
+
margin-top: var(--spacing-md);
356
display: flex;
357
flex-direction: column;
358
}
···
360
.reply-card-threaded {
361
position: relative;
362
padding-left: 0;
363
+
padding: var(--spacing-sm) 0;
364
transition: background 0.15s;
365
}
366
367
.reply-header {
368
display: flex;
369
align-items: center;
370
+
gap: 8px;
371
+
margin-bottom: 4px;
372
}
373
374
.reply-avatar {
375
width: 28px;
376
height: 28px;
377
+
border-radius: var(--radius-full);
378
background: var(--bg-tertiary);
379
overflow: hidden;
380
flex-shrink: 0;
···
398
.reply-meta {
399
display: flex;
400
align-items: baseline;
401
+
gap: 8px;
402
flex: 1;
403
min-width: 0;
404
}
405
406
.reply-author {
407
font-weight: 600;
408
+
font-size: 0.875rem;
409
color: var(--text-primary);
410
white-space: nowrap;
411
overflow: hidden;
···
422
}
423
424
.reply-time {
425
+
font-size: 0.8rem;
426
color: var(--text-tertiary);
427
white-space: nowrap;
428
}
···
437
line-height: 1.5;
438
color: var(--text-primary);
439
margin: 0;
440
+
padding-left: 36px;
441
}
442
443
.reply-actions {
···
458
padding: 4px;
459
color: var(--text-tertiary);
460
cursor: pointer;
461
+
border-radius: var(--radius-sm);
462
display: flex;
463
align-items: center;
464
justify-content: center;
···
470
}
471
472
.reply-action-delete:hover {
473
+
color: var(--error);
474
+
background: rgba(255, 69, 58, 0.1);
475
}
476
477
.reply-form {
478
border: 1px solid var(--border);
479
border-radius: var(--radius-md);
480
+
padding: var(--spacing-md);
481
background: var(--bg-secondary);
482
+
margin-bottom: var(--spacing-md);
483
}
484
485
.replying-to-banner {
···
487
justify-content: space-between;
488
align-items: center;
489
background: var(--bg-tertiary);
490
+
padding: 6px 10px;
491
border-radius: var(--radius-sm);
492
+
margin-bottom: var(--spacing-sm);
493
+
font-size: 0.8rem;
494
color: var(--text-secondary);
495
}
496
···
499
border: none;
500
color: var(--text-tertiary);
501
cursor: pointer;
502
+
font-size: 1rem;
503
padding: 0 4px;
504
line-height: 1;
505
}
···
513
background: var(--bg-primary);
514
border: 1px solid var(--border);
515
border-radius: var(--radius-sm);
516
+
padding: 10px 12px;
517
color: var(--text-primary);
518
font-family: inherit;
519
+
font-size: 0.875rem;
520
resize: vertical;
521
+
min-height: 60px;
522
transition: border-color 0.15s;
523
display: block;
524
box-sizing: border-box;
···
532
.reply-form-actions {
533
display: flex;
534
justify-content: flex-end;
535
+
margin-top: var(--spacing-sm);
536
}
537
538
.rich-text-link {
+136
-80
web/src/css/base.css
···
0
0
1
:root {
2
-
--bg-primary: #09090b;
3
-
--bg-secondary: #0f0f12;
4
-
--bg-tertiary: #18181b;
5
-
--bg-card: #09090b;
6
-
--bg-elevated: #18181b;
7
-
--text-primary: #e4e4e7;
8
-
--text-secondary: #a1a1aa;
9
-
--text-tertiary: #71717a;
10
-
--border: #27272a;
11
-
--border-hover: #3f3f46;
12
-
--accent: #6366f1;
13
-
--accent-hover: #4f46e5;
14
-
--accent-subtle: rgba(99, 102, 241, 0.1);
15
-
--accent-text: #818cf8;
16
-
--success: #10b981;
17
-
--error: #ef4444;
18
-
--warning: #f59e0b;
19
-
--info: #3b82f6;
20
-
--radius-sm: 4px;
21
-
--radius-md: 6px;
22
-
--radius-lg: 8px;
0
0
0
0
0
0
0
0
0
0
23
--radius-full: 9999px;
24
-
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
25
-
--shadow-md:
26
-
0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
27
-
--shadow-lg:
28
-
0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
29
-
--font-sans:
30
-
"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
31
-
--font-mono:
32
-
"JetBrains Mono", source-code-pro, Menlo, Monaco, Consolas, monospace;
33
-
--nav-bg: rgba(9, 9, 11, 0.9);
0
0
0
0
0
0
0
0
0
0
34
}
35
36
[data-theme="light"] {
37
-
--bg-primary: #ffffff;
38
-
--bg-secondary: #f4f4f5;
39
-
--bg-tertiary: #e4e4e7;
40
--bg-card: #ffffff;
41
-
--bg-elevated: #f4f4f5;
42
-
--text-primary: #18181b;
43
-
--text-secondary: #52525b;
44
-
--text-tertiary: #71717a;
45
-
--border: #e4e4e7;
46
-
--border-hover: #d4d4d8;
47
-
--accent: #4f46e5;
48
-
--accent-hover: #4338ca;
49
-
--accent-subtle: rgba(79, 70, 229, 0.1);
50
-
--accent-text: #4f46e5;
51
-
--success: #059669;
52
-
--error: #dc2626;
53
-
--warning: #d97706;
54
-
--info: #2563eb;
55
-
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
56
-
--shadow-md:
57
-
0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
58
-
--nav-bg: rgba(255, 255, 255, 0.9);
0
0
0
0
0
0
59
}
60
61
* {
···
74
font-family: var(--font-sans);
75
background: var(--bg-primary);
76
color: var(--text-primary);
77
-
line-height: 1.5;
78
min-height: 100vh;
79
-webkit-font-smoothing: antialiased;
80
-moz-osx-font-smoothing: grayscale;
81
overflow-x: hidden;
82
max-width: 100vw;
83
-
}
84
-
85
-
a {
86
-
color: inherit;
87
-
text-decoration: none;
88
-
transition: color 0.15s ease;
89
}
90
91
h1,
···
94
h4,
95
h5,
96
h6 {
0
97
font-weight: 600;
98
-
line-height: 1.25;
99
-
letter-spacing: -0.025em;
100
color: var(--text-primary);
0
0
0
0
0
0
0
0
0
0
0
101
}
102
103
p {
104
color: var(--text-secondary);
0
0
0
0
0
0
0
105
}
106
107
button {
···
124
color: var(--accent-text);
125
}
126
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
127
.text-sm {
128
-
font-size: 0.875rem;
129
}
130
131
.text-xs {
132
-
font-size: 0.75rem;
133
}
134
135
.font-medium {
···
140
font-weight: 600;
141
}
142
0
0
0
0
143
.text-muted {
144
color: var(--text-secondary);
145
}
···
148
color: var(--text-tertiary);
149
}
150
151
-
::-webkit-scrollbar {
152
-
width: 10px;
153
-
height: 10px;
154
-
}
155
-
156
-
::-webkit-scrollbar-track {
157
-
background: transparent;
158
-
}
159
-
160
-
::-webkit-scrollbar-thumb {
161
-
background: var(--border);
162
-
border-radius: 5px;
163
-
border: 2px solid var(--bg-primary);
164
-
}
165
-
166
-
::-webkit-scrollbar-thumb:hover {
167
-
background: var(--border-hover);
168
}
···
1
+
@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap");
2
+
3
:root {
4
+
--bg-primary: #0a0a0d;
5
+
--bg-secondary: #121216;
6
+
--bg-tertiary: #1a1a1f;
7
+
--bg-card: #0f0f13;
8
+
--bg-elevated: #18181d;
9
+
--bg-hover: #1e1e24;
10
+
11
+
--glass-border: rgba(234, 234, 238, 0.08);
12
+
--glass-bg: rgba(10, 10, 13, 0.92);
13
+
14
+
--text-primary: #eaeaee;
15
+
--text-secondary: #b7b6c5;
16
+
--text-tertiary: #6e6d7a;
17
+
18
+
--border: rgba(183, 182, 197, 0.12);
19
+
--border-hover: rgba(183, 182, 197, 0.2);
20
+
--accent: #957a86;
21
+
--accent-hover: #a98d98;
22
+
--accent-subtle: rgba(149, 122, 134, 0.15);
23
+
--accent-text: #c4a8b2;
24
+
25
+
--success: #7fb069;
26
+
--error: #d97766;
27
+
--warning: #e8a54b;
28
+
--info: #6eb5ff;
29
+
30
+
--radius-xs: 4px;
31
+
--radius-sm: 6px;
32
+
--radius-md: 8px;
33
+
--radius-lg: 12px;
34
+
--radius-xl: 16px;
35
--radius-full: 9999px;
36
+
37
+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
38
+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
39
+
--shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
40
+
--shadow-glow: 0 0 20px rgba(149, 122, 134, 0.2);
41
+
42
+
--font-sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, sans-serif;
43
+
--font-display: "IBM Plex Sans", sans-serif;
44
+
--font-mono: "IBM Plex Mono", monospace;
45
+
46
+
--nav-bg: rgba(10, 10, 13, 0.95);
47
+
48
+
--sidebar-width: 200px;
49
+
--right-sidebar-width: 260px;
50
+
--content-max-width: 600px;
51
+
--spacing-xs: 4px;
52
+
--spacing-sm: 8px;
53
+
--spacing-md: 12px;
54
+
--spacing-lg: 20px;
55
+
--spacing-xl: 28px;
56
}
57
58
[data-theme="light"] {
59
+
--bg-primary: #f8f8fa;
60
+
--bg-secondary: #ffffff;
61
+
--bg-tertiary: #f0f0f4;
62
--bg-card: #ffffff;
63
+
--bg-elevated: #ffffff;
64
+
--bg-hover: #eeeef2;
65
+
66
+
--glass-border: rgba(92, 73, 90, 0.1);
67
+
--glass-bg: rgba(248, 248, 250, 0.95);
68
+
69
+
--text-primary: #18171c;
70
+
--text-secondary: #5c495a;
71
+
--text-tertiary: #8a8494;
72
+
73
+
--border: rgba(92, 73, 90, 0.12);
74
+
--border-hover: rgba(92, 73, 90, 0.22);
75
+
76
+
--accent: #7a5f6d;
77
+
--accent-hover: #664e5b;
78
+
--accent-subtle: rgba(149, 122, 134, 0.12);
79
+
--accent-text: #5c495a;
80
+
81
+
--shadow-sm: 0 1px 3px rgba(92, 73, 90, 0.06);
82
+
--shadow-md: 0 4px 12px rgba(92, 73, 90, 0.08);
83
+
--shadow-lg: 0 8px 24px rgba(92, 73, 90, 0.1);
84
+
--shadow-glow: 0 0 20px rgba(149, 122, 134, 0.1);
85
+
86
+
--nav-bg: rgba(255, 255, 255, 0.95);
87
}
88
89
* {
···
102
font-family: var(--font-sans);
103
background: var(--bg-primary);
104
color: var(--text-primary);
105
+
line-height: 1.55;
106
min-height: 100vh;
107
-webkit-font-smoothing: antialiased;
108
-moz-osx-font-smoothing: grayscale;
109
overflow-x: hidden;
110
max-width: 100vw;
111
+
font-size: 0.9375rem;
0
0
0
0
0
112
}
113
114
h1,
···
117
h4,
118
h5,
119
h6 {
120
+
font-family: var(--font-display);
121
font-weight: 600;
122
+
letter-spacing: -0.02em;
0
123
color: var(--text-primary);
124
+
line-height: 1.3;
125
+
}
126
+
127
+
h1 {
128
+
font-size: 1.5rem;
129
+
}
130
+
h2 {
131
+
font-size: 1.25rem;
132
+
}
133
+
h3 {
134
+
font-size: 1.1rem;
135
}
136
137
p {
138
color: var(--text-secondary);
139
+
line-height: 1.6;
140
+
}
141
+
142
+
a {
143
+
color: inherit;
144
+
text-decoration: none;
145
+
transition: color 0.2s ease;
146
}
147
148
button {
···
165
color: var(--accent-text);
166
}
167
168
+
::-webkit-scrollbar {
169
+
width: 10px;
170
+
height: 10px;
171
+
}
172
+
173
+
::-webkit-scrollbar-track {
174
+
background: var(--bg-secondary);
175
+
}
176
+
177
+
::-webkit-scrollbar-thumb {
178
+
background: var(--bg-hover);
179
+
border-radius: var(--radius-full);
180
+
border: 2px solid var(--bg-secondary);
181
+
}
182
+
183
+
::-webkit-scrollbar-thumb:hover {
184
+
background: var(--text-tertiary);
185
+
}
186
+
187
+
:focus-visible {
188
+
outline: 2px solid var(--accent);
189
+
outline-offset: 3px;
190
+
}
191
+
192
.text-sm {
193
+
font-size: 0.9rem;
194
}
195
196
.text-xs {
197
+
font-size: 0.8rem;
198
}
199
200
.font-medium {
···
205
font-weight: 600;
206
}
207
208
+
.font-mono {
209
+
font-family: var(--font-mono);
210
+
}
211
+
212
.text-muted {
213
color: var(--text-secondary);
214
}
···
217
color: var(--text-tertiary);
218
}
219
220
+
.card {
221
+
background: var(--bg-card);
222
+
border-radius: var(--radius-lg);
223
+
border: 1px solid var(--border);
0
0
0
0
0
0
0
0
0
0
0
0
0
224
}
+39
-25
web/src/css/buttons.css
···
2
display: inline-flex;
3
align-items: center;
4
justify-content: center;
5
-
gap: 8px;
6
-
padding: 10px 20px;
7
-
font-size: 0.9rem;
8
font-weight: 500;
9
border-radius: var(--radius-md);
10
transition: all 0.15s ease;
11
-
white-space: pre;
0
0
12
}
13
14
.btn-primary {
···
18
19
.btn-primary:hover {
20
background: var(--accent-hover);
21
-
transform: translateY(-1px);
22
-
box-shadow: var(--shadow-md);
23
}
24
25
.btn-secondary {
···
36
.btn-ghost {
37
color: var(--text-secondary);
38
padding: 8px 12px;
0
39
}
40
41
.btn-ghost:hover {
···
49
display: flex;
50
align-items: center;
51
justify-content: center;
52
-
gap: 10px;
53
-
transition:
54
-
background 0.2s,
55
-
transform 0.2s;
56
}
57
58
.btn-bluesky:hover {
59
background: #0070dd;
60
-
transform: translateY(-1px);
61
}
62
63
.btn-sm {
64
padding: 6px 12px;
65
-
font-size: 0.85rem;
66
}
67
68
.btn-text {
69
background: none;
70
border: none;
71
color: var(--text-secondary);
72
-
font-size: 0.9rem;
73
-
padding: 8px 12px;
74
cursor: pointer;
75
transition: color 0.15s;
0
76
}
77
78
.btn-text:hover {
79
color: var(--text-primary);
0
80
}
81
82
.btn-block {
83
width: 100%;
84
text-align: left;
85
-
padding: 8px 12px;
86
color: var(--text-secondary);
87
background: var(--bg-tertiary);
88
border-radius: var(--radius-md);
89
margin-top: 8px;
90
-
font-size: 0.9rem;
91
cursor: pointer;
92
-
transition: all 0.2s;
0
93
}
94
95
.btn-block:hover {
96
-
background: var(--border);
97
color: var(--text-primary);
0
98
}
99
100
.btn-icon-danger {
101
padding: 8px;
102
-
background: var(--error);
103
-
color: white;
104
border: none;
105
border-radius: var(--radius-md);
106
cursor: pointer;
107
-
box-shadow: var(--shadow-md);
108
transition: all 0.15s ease;
109
display: flex;
110
align-items: center;
···
112
}
113
114
.btn-icon-danger:hover {
115
-
background: #dc2626;
116
-
transform: scale(1.05);
0
0
0
0
0
0
0
0
0
0
0
0
117
}
118
119
.action-buttons {
120
display: flex;
121
-
gap: 8px;
122
flex-wrap: wrap;
123
}
124
125
.action-buttons-end {
126
display: flex;
127
justify-content: flex-end;
128
-
gap: 8px;
129
}
···
2
display: inline-flex;
3
align-items: center;
4
justify-content: center;
5
+
gap: 6px;
6
+
padding: 8px 16px;
7
+
font-size: 0.85rem;
8
font-weight: 500;
9
border-radius: var(--radius-md);
10
transition: all 0.15s ease;
11
+
white-space: nowrap;
12
+
border: none;
13
+
cursor: pointer;
14
}
15
16
.btn-primary {
···
20
21
.btn-primary:hover {
22
background: var(--accent-hover);
23
+
box-shadow: var(--shadow-glow);
0
24
}
25
26
.btn-secondary {
···
37
.btn-ghost {
38
color: var(--text-secondary);
39
padding: 8px 12px;
40
+
background: transparent;
41
}
42
43
.btn-ghost:hover {
···
51
display: flex;
52
align-items: center;
53
justify-content: center;
54
+
gap: 8px;
55
+
transition: all 0.15s;
0
0
56
}
57
58
.btn-bluesky:hover {
59
background: #0070dd;
0
60
}
61
62
.btn-sm {
63
padding: 6px 12px;
64
+
font-size: 0.8rem;
65
}
66
67
.btn-text {
68
background: none;
69
border: none;
70
color: var(--text-secondary);
71
+
font-size: 0.85rem;
72
+
padding: 6px 10px;
73
cursor: pointer;
74
transition: color 0.15s;
75
+
border-radius: var(--radius-sm);
76
}
77
78
.btn-text:hover {
79
color: var(--text-primary);
80
+
background: var(--bg-tertiary);
81
}
82
83
.btn-block {
84
width: 100%;
85
text-align: left;
86
+
padding: 10px 14px;
87
color: var(--text-secondary);
88
background: var(--bg-tertiary);
89
border-radius: var(--radius-md);
90
margin-top: 8px;
91
+
font-size: 0.85rem;
92
cursor: pointer;
93
+
transition: all 0.15s;
94
+
border: 1px solid transparent;
95
}
96
97
.btn-block:hover {
98
+
background: var(--bg-hover);
99
color: var(--text-primary);
100
+
border-color: var(--border);
101
}
102
103
.btn-icon-danger {
104
padding: 8px;
105
+
background: rgba(255, 69, 58, 0.1);
106
+
color: var(--error);
107
border: none;
108
border-radius: var(--radius-md);
109
cursor: pointer;
0
110
transition: all 0.15s ease;
111
display: flex;
112
align-items: center;
···
114
}
115
116
.btn-icon-danger:hover {
117
+
background: var(--error);
118
+
color: white;
119
+
}
120
+
121
+
.btn-danger {
122
+
background: rgba(255, 69, 58, 0.1);
123
+
color: var(--error);
124
+
border: 1px solid rgba(255, 69, 58, 0.2);
125
+
}
126
+
127
+
.btn-danger:hover {
128
+
background: var(--error);
129
+
color: white;
130
+
border-color: var(--error);
131
}
132
133
.action-buttons {
134
display: flex;
135
+
gap: var(--spacing-sm);
136
flex-wrap: wrap;
137
}
138
139
.action-buttons-end {
140
display: flex;
141
justify-content: flex-end;
142
+
gap: var(--spacing-sm);
143
}
+270
web/src/css/cards.css
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
.card {
2
+
background: var(--bg-primary);
3
+
border: none;
4
+
border-radius: 0;
5
+
transition: all 0.15s ease;
6
+
position: relative;
7
+
overflow: visible;
8
+
}
9
+
10
+
.semble-badge {
11
+
display: flex;
12
+
align-items: center;
13
+
gap: 4px;
14
+
font-size: 0.75rem;
15
+
color: var(--text-tertiary);
16
+
margin-right: 4px;
17
+
}
18
+
19
+
.semble-badge img {
20
+
width: 14px;
21
+
height: 14px;
22
+
}
23
+
24
+
.bookmark-preview {
25
+
display: block;
26
+
padding: 14px 16px;
27
+
background: linear-gradient(
28
+
135deg,
29
+
var(--bg-tertiary) 0%,
30
+
var(--bg-secondary) 100%
31
+
);
32
+
border: 1px solid var(--border);
33
+
border-left: 3px solid var(--accent);
34
+
border-radius: var(--radius-md);
35
+
text-decoration: none;
36
+
transition: all 0.2s ease;
37
+
position: relative;
38
+
z-index: 1;
39
+
}
40
+
41
+
.bookmark-preview:hover {
42
+
background: var(--bg-hover);
43
+
border-left-color: var(--accent-hover);
44
+
}
45
+
46
+
.bookmark-preview-content {
47
+
display: flex;
48
+
flex-direction: column;
49
+
gap: 4px;
50
+
}
51
+
52
+
.bookmark-preview-site {
53
+
display: flex;
54
+
align-items: center;
55
+
gap: 6px;
56
+
font-size: 0.7rem;
57
+
color: var(--text-tertiary);
58
+
text-transform: uppercase;
59
+
letter-spacing: 0.06em;
60
+
font-weight: 500;
61
+
}
62
+
63
+
.bookmark-preview-site svg {
64
+
color: var(--accent);
65
+
}
66
+
67
+
.bookmark-preview-title {
68
+
font-size: 0.95rem;
69
+
font-weight: 600;
70
+
color: var(--text-primary);
71
+
line-height: 1.35;
72
+
margin: 0;
73
+
display: -webkit-box;
74
+
-webkit-line-clamp: 2;
75
+
-webkit-box-orient: vertical;
76
+
overflow: hidden;
77
+
}
78
+
79
+
.bookmark-preview-desc {
80
+
font-size: 0.8rem;
81
+
color: var(--text-secondary);
82
+
line-height: 1.45;
83
+
margin: 0;
84
+
display: -webkit-box;
85
+
-webkit-line-clamp: 2;
86
+
-webkit-box-orient: vertical;
87
+
overflow: hidden;
88
+
}
89
+
90
+
.bookmark-card .annotation-content {
91
+
padding-left: 0;
92
+
overflow: visible;
93
+
}
94
+
95
+
.bookmark-card {
96
+
overflow: visible !important;
97
+
}
98
+
99
+
.bookmark-card:hover {
100
+
z-index: 100 !important;
101
+
overflow: visible !important;
102
+
}
103
+
104
+
.bookmark-site {
105
+
display: flex;
106
+
align-items: center;
107
+
gap: 6px;
108
+
font-size: 0.8rem;
109
+
color: var(--text-tertiary);
110
+
text-transform: uppercase;
111
+
letter-spacing: 0.02em;
112
+
}
113
+
114
+
.bookmark-title {
115
+
font-size: 1rem;
116
+
font-weight: 600;
117
+
color: var(--text-primary);
118
+
line-height: 1.4;
119
+
margin: 0;
120
+
}
121
+
122
+
.bookmark-desc {
123
+
font-size: 0.875rem;
124
+
color: var(--text-secondary);
125
+
line-height: 1.5;
126
+
margin: 0;
127
+
display: -webkit-box;
128
+
-webkit-line-clamp: 2;
129
+
-webkit-box-orient: vertical;
130
+
overflow: hidden;
131
+
}
132
+
133
+
.edit-form {
134
+
display: flex;
135
+
flex-direction: column;
136
+
gap: 8px;
137
+
}
138
+
139
+
.edit-textarea,
140
+
.edit-input {
141
+
width: 100%;
142
+
padding: 10px 12px;
143
+
background: var(--bg-primary);
144
+
border: 1px solid var(--border);
145
+
border-radius: var(--radius-md);
146
+
color: var(--text-primary);
147
+
font-family: inherit;
148
+
font-size: 0.9rem;
149
+
transition: border-color 0.15s ease;
150
+
}
151
+
152
+
.edit-textarea {
153
+
resize: vertical;
154
+
min-height: 80px;
155
+
}
156
+
157
+
.edit-textarea:focus,
158
+
.edit-input:focus {
159
+
outline: none;
160
+
border-color: var(--accent);
161
+
}
162
+
163
+
.edit-actions {
164
+
display: flex;
165
+
justify-content: flex-end;
166
+
gap: 8px;
167
+
}
168
+
169
+
.color-edit-form {
170
+
display: flex;
171
+
align-items: center;
172
+
gap: 8px;
173
+
padding: 10px 12px;
174
+
background: var(--bg-secondary);
175
+
border: 1px solid var(--border);
176
+
border-radius: var(--radius-md);
177
+
}
178
+
179
+
.color-picker-wrapper {
180
+
position: relative;
181
+
width: 28px;
182
+
height: 28px;
183
+
flex-shrink: 0;
184
+
}
185
+
186
+
.color-preview {
187
+
width: 100%;
188
+
height: 100%;
189
+
border-radius: 50%;
190
+
border: 2px solid var(--bg-card);
191
+
box-shadow: 0 0 0 1px var(--border);
192
+
}
193
+
194
+
.color-input {
195
+
position: absolute;
196
+
top: 0;
197
+
left: 0;
198
+
width: 100%;
199
+
height: 100%;
200
+
opacity: 0;
201
+
cursor: pointer;
202
+
}
203
+
204
+
.color-edit-form .edit-input {
205
+
margin: 0;
206
+
flex: 1;
207
+
padding: 6px 10px;
208
+
height: 32px;
209
+
border: none;
210
+
background: transparent;
211
+
}
212
+
213
+
.btn-icon {
214
+
padding: 0 10px;
215
+
height: 32px;
216
+
min-width: auto;
217
+
}
218
+
219
+
.history-panel {
220
+
padding: 12px;
221
+
background: var(--bg-secondary);
222
+
border: 1px solid var(--border);
223
+
border-radius: var(--radius-md);
224
+
}
225
+
226
+
.history-header {
227
+
display: flex;
228
+
justify-content: space-between;
229
+
align-items: center;
230
+
margin-bottom: 12px;
231
+
}
232
+
233
+
.history-title {
234
+
font-size: 0.9rem;
235
+
font-weight: 600;
236
+
color: var(--text-primary);
237
+
}
238
+
239
+
.history-status {
240
+
font-size: 0.85rem;
241
+
color: var(--text-tertiary);
242
+
font-style: italic;
243
+
}
244
+
245
+
.history-list {
246
+
list-style: none;
247
+
padding: 0;
248
+
margin: 0;
249
+
display: flex;
250
+
flex-direction: column;
251
+
gap: 8px;
252
+
}
253
+
254
+
.history-item {
255
+
padding: 8px 10px;
256
+
background: var(--bg-tertiary);
257
+
border-radius: var(--radius-sm);
258
+
}
259
+
260
+
.history-date {
261
+
font-size: 0.75rem;
262
+
color: var(--text-tertiary);
263
+
margin-bottom: 4px;
264
+
}
265
+
266
+
.history-content {
267
+
font-size: 0.85rem;
268
+
color: var(--text-secondary);
269
+
line-height: 1.5;
270
+
}
+163
-159
web/src/css/collections.css
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1
.collections-list {
2
display: flex;
3
flex-direction: column;
4
-
gap: 2px;
0
0
0
5
background: var(--bg-card);
6
border: 1px solid var(--border);
7
border-radius: var(--radius-lg);
8
-
overflow: hidden;
9
}
10
11
.collection-row {
12
display: flex;
13
align-items: center;
14
-
background: var(--bg-card);
15
transition: background 0.15s ease;
16
}
17
18
-
.collection-row:not(:last-child) {
19
-
border-bottom: 1px solid var(--border);
20
-
}
21
-
22
.collection-row:hover {
23
background: var(--bg-secondary);
24
}
···
27
flex: 1;
28
display: flex;
29
align-items: center;
30
-
gap: 16px;
31
-
padding: 16px 20px;
32
text-decoration: none;
33
min-width: 0;
34
}
35
36
.collection-row-icon {
37
-
width: 44px;
38
-
height: 44px;
39
-
min-width: 44px;
40
display: flex;
41
align-items: center;
42
justify-content: center;
43
-
background: linear-gradient(
44
-
135deg,
45
-
rgba(79, 70, 229, 0.1),
46
-
rgba(168, 85, 247, 0.15)
47
-
);
48
color: var(--accent);
49
border-radius: var(--radius-md);
50
-
transition: all 0.2s ease;
0
51
}
52
53
.collection-row:hover .collection-row-icon {
54
-
background: linear-gradient(
55
-
135deg,
56
-
rgba(79, 70, 229, 0.15),
57
-
rgba(168, 85, 247, 0.2)
58
-
);
59
-
transform: scale(1.05);
60
}
61
62
.collection-row-info {
63
flex: 1;
64
min-width: 0;
0
0
0
65
}
66
67
.collection-row-name {
68
-
font-size: 1rem;
69
font-weight: 600;
70
color: var(--text-primary);
71
-
margin: 0 0 2px 0;
72
white-space: nowrap;
73
overflow: hidden;
74
text-overflow: ellipsis;
75
}
76
77
-
.collection-row:hover .collection-row-name {
78
-
color: var(--accent);
79
-
}
80
-
81
.collection-row-desc {
82
-
font-size: 0.85rem;
83
color: var(--text-secondary);
84
-
margin: 0;
85
white-space: nowrap;
86
overflow: hidden;
87
text-overflow: ellipsis;
···
90
.collection-row-arrow {
91
color: var(--text-tertiary);
92
opacity: 0;
93
-
transition: all 0.2s ease;
94
}
95
96
.collection-row:hover .collection-row-arrow {
97
opacity: 1;
98
-
color: var(--accent);
99
-
transform: translateX(2px);
100
}
101
102
.collection-row-edit {
103
-
padding: 10px;
104
-
margin-right: 12px;
105
color: var(--text-tertiary);
106
-
background: none;
107
-
border: none;
108
border-radius: var(--radius-sm);
109
-
cursor: pointer;
110
opacity: 0;
111
-
transition: all 0.15s ease;
0
112
}
113
114
.collection-row:hover .collection-row-edit {
···
116
}
117
118
.collection-row-edit:hover {
119
-
color: var(--text-primary);
120
background: var(--bg-tertiary);
0
121
}
122
123
.collection-detail-header {
124
display: flex;
125
-
gap: 20px;
126
-
padding: 24px;
127
-
background: var(--bg-card);
0
128
border: 1px solid var(--border);
129
border-radius: var(--radius-lg);
130
-
margin-bottom: 32px;
131
position: relative;
132
}
133
···
138
display: flex;
139
align-items: center;
140
justify-content: center;
141
-
background: linear-gradient(
142
-
135deg,
143
-
rgba(79, 70, 229, 0.1),
144
-
rgba(168, 85, 247, 0.1)
145
-
);
146
color: var(--accent);
147
-
border-radius: var(--radius-md);
0
148
}
149
150
.collection-detail-info {
151
-
flex: 1;
152
-
min-width: 0;
0
153
}
154
155
.collection-detail-visibility {
156
-
display: flex;
157
align-items: center;
158
-
gap: 6px;
159
-
font-size: 0.8rem;
160
font-weight: 600;
0
0
161
color: var(--accent);
162
-
text-transform: capitalize;
163
-
margin-bottom: 8px;
0
0
164
}
165
166
.collection-detail-title {
0
167
font-size: 1.5rem;
168
font-weight: 700;
169
color: var(--text-primary);
170
-
margin-bottom: 8px;
171
-
line-height: 1.3;
172
-
}
173
-
174
-
@media (max-width: 600px) {
175
-
.collection-detail-header {
176
-
flex-direction: column;
177
-
padding: 16px;
178
-
gap: 16px;
179
-
}
180
-
181
-
.collection-detail-actions {
182
-
position: static;
183
-
margin-top: -8px;
184
-
justify-content: flex-end;
185
-
}
186
}
187
188
.collection-detail-desc {
189
color: var(--text-secondary);
190
-
font-size: 1rem;
191
line-height: 1.5;
192
-
margin-bottom: 12px;
193
-
max-width: 600px;
194
-
overflow-wrap: break-word;
195
-
word-break: break-word;
196
}
197
198
.collection-detail-stats {
199
display: flex;
200
align-items: center;
201
-
gap: 8px;
202
-
font-size: 0.85rem;
203
color: var(--text-tertiary);
0
204
}
205
206
.collection-detail-actions {
207
position: absolute;
208
-
top: 20px;
209
-
right: 20px;
210
-
display: flex;
211
-
align-items: center;
212
-
gap: 8px;
213
-
}
214
-
215
-
.collection-detail-actions .share-menu-container {
216
display: flex;
217
-
align-items: center;
218
-
}
219
-
220
-
.collection-detail-actions .annotation-action {
221
-
padding: 10px;
222
-
color: var(--text-tertiary);
223
-
background: none;
224
-
border: none;
225
-
border-radius: var(--radius-sm);
226
-
cursor: pointer;
227
-
transition: all 0.15s ease;
228
-
}
229
-
230
-
.collection-detail-actions .annotation-action:hover {
231
-
color: var(--accent);
232
-
background: var(--bg-tertiary);
233
}
234
0
235
.collection-detail-edit,
236
.collection-detail-delete {
237
-
padding: 10px;
238
color: var(--text-tertiary);
239
-
background: none;
0
0
240
border: none;
241
-
border-radius: var(--radius-sm);
242
cursor: pointer;
243
-
transition: all 0.15s ease;
244
}
245
0
246
.collection-detail-edit:hover {
247
-
color: var(--accent);
248
-
background: var(--bg-tertiary);
249
}
250
251
.collection-detail-delete:hover {
252
-
color: var(--error);
253
-
background: rgba(239, 68, 68, 0.1);
254
-
}
255
-
256
-
.collection-item-wrapper {
257
-
position: relative;
258
-
}
259
-
260
-
.collection-item-remove {
261
-
position: absolute;
262
-
top: 12px;
263
-
left: -40px;
264
-
z-index: 10;
265
-
padding: 8px;
266
-
background: var(--bg-card);
267
-
border: 1px solid var(--border);
268
-
border-radius: var(--radius-sm);
269
-
color: var(--text-tertiary);
270
-
cursor: pointer;
271
-
opacity: 0;
272
-
transition: all 0.15s ease;
273
-
}
274
-
275
-
.collection-item-wrapper:hover .collection-item-remove {
276
-
opacity: 1;
277
-
}
278
-
279
-
.collection-item-remove:hover {
280
color: var(--error);
281
-
border-color: var(--error);
282
-
background: rgba(239, 68, 68, 0.05);
283
}
284
285
.collection-list-item {
286
width: 100%;
287
text-align: left;
288
-
padding: 12px 16px;
289
border-radius: var(--radius-md);
290
-
background: var(--bg-primary);
291
-
border: 1px solid transparent;
292
color: var(--text-primary);
293
-
transition: all 0.15s ease;
294
display: flex;
295
align-items: center;
296
justify-content: space-between;
297
cursor: pointer;
0
298
}
299
300
.collection-list-item:hover {
301
background: var(--bg-hover);
302
-
border-color: var(--border);
303
-
}
304
-
305
-
.collection-list-item:hover .collection-list-item-icon {
306
-
opacity: 1;
307
}
308
309
.collection-list-item:disabled {
310
-
opacity: 0.6;
311
cursor: not-allowed;
312
}
313
314
-
.item-delete-overlay {
0
0
0
0
315
position: absolute;
316
-
top: 16px;
317
-
right: 16px;
318
-
z-index: 10;
0
0
0
0
0
0
0
0
0
0
319
opacity: 0;
320
-
transition: opacity 0.15s ease;
321
}
322
323
-
.card:hover .item-delete-overlay,
324
-
div:hover > .item-delete-overlay {
325
opacity: 1;
326
}
0
0
0
0
0
0
···
1
+
.collection-feed-item {
2
+
display: flex;
3
+
flex-direction: column;
4
+
background: var(--bg-primary);
5
+
overflow: visible;
6
+
}
7
+
8
+
.collection-context-badge {
9
+
display: flex;
10
+
align-items: center;
11
+
justify-content: space-between;
12
+
gap: var(--spacing-sm);
13
+
padding: 10px 20px;
14
+
background: var(--bg-secondary);
15
+
border-bottom: 1px solid var(--border);
16
+
}
17
+
18
+
.collection-context-inner {
19
+
display: flex;
20
+
align-items: center;
21
+
gap: 8px;
22
+
font-size: 0.8rem;
23
+
color: var(--text-secondary);
24
+
}
25
+
26
+
.collection-context-avatar {
27
+
width: 20px;
28
+
height: 20px;
29
+
border-radius: var(--radius-full);
30
+
object-fit: cover;
31
+
}
32
+
33
+
.collection-context-text {
34
+
display: flex;
35
+
align-items: center;
36
+
gap: 4px;
37
+
flex-wrap: wrap;
38
+
}
39
+
40
+
.collection-context-author {
41
+
font-weight: 600;
42
+
color: var(--text-primary);
43
+
text-decoration: none;
44
+
}
45
+
46
+
.collection-context-author:hover {
47
+
text-decoration: underline;
48
+
}
49
+
50
+
.collection-context-link {
51
+
display: inline-flex;
52
+
align-items: center;
53
+
gap: 5px;
54
+
font-weight: 600;
55
+
color: var(--accent);
56
+
text-decoration: none;
57
+
background: var(--accent-subtle);
58
+
padding: 2px 8px;
59
+
border-radius: var(--radius-sm);
60
+
}
61
+
62
+
.collection-context-link:hover {
63
+
background: var(--accent);
64
+
color: var(--bg-primary);
65
+
}
66
+
67
.collections-list {
68
display: flex;
69
flex-direction: column;
70
+
gap: 12px;
71
+
}
72
+
73
+
.collections-list > * {
74
background: var(--bg-card);
75
border: 1px solid var(--border);
76
border-radius: var(--radius-lg);
0
77
}
78
79
.collection-row {
80
display: flex;
81
align-items: center;
0
82
transition: background 0.15s ease;
83
}
84
0
0
0
0
85
.collection-row:hover {
86
background: var(--bg-secondary);
87
}
···
90
flex: 1;
91
display: flex;
92
align-items: center;
93
+
gap: var(--spacing-md);
94
+
padding: var(--spacing-md);
95
text-decoration: none;
96
min-width: 0;
97
}
98
99
.collection-row-icon {
100
+
width: 40px;
101
+
height: 40px;
102
+
min-width: 40px;
103
display: flex;
104
align-items: center;
105
justify-content: center;
106
+
background: var(--bg-tertiary);
0
0
0
0
107
color: var(--accent);
108
border-radius: var(--radius-md);
109
+
transition: all 0.15s ease;
110
+
font-size: 1.1rem;
111
}
112
113
.collection-row:hover .collection-row-icon {
114
+
background: var(--accent-subtle);
0
0
0
0
0
115
}
116
117
.collection-row-info {
118
flex: 1;
119
min-width: 0;
120
+
display: flex;
121
+
flex-direction: column;
122
+
gap: 2px;
123
}
124
125
.collection-row-name {
126
+
font-size: 0.9rem;
127
font-weight: 600;
128
color: var(--text-primary);
0
129
white-space: nowrap;
130
overflow: hidden;
131
text-overflow: ellipsis;
132
}
133
0
0
0
0
134
.collection-row-desc {
135
+
font-size: 0.8rem;
136
color: var(--text-secondary);
0
137
white-space: nowrap;
138
overflow: hidden;
139
text-overflow: ellipsis;
···
142
.collection-row-arrow {
143
color: var(--text-tertiary);
144
opacity: 0;
145
+
transition: opacity 0.15s;
146
}
147
148
.collection-row:hover .collection-row-arrow {
149
opacity: 1;
0
0
150
}
151
152
.collection-row-edit {
153
+
padding: 8px;
154
+
margin-right: var(--spacing-sm);
155
color: var(--text-tertiary);
156
+
background: transparent;
0
157
border-radius: var(--radius-sm);
158
+
transition: all 0.15s;
159
opacity: 0;
160
+
border: none;
161
+
cursor: pointer;
162
}
163
164
.collection-row:hover .collection-row-edit {
···
166
}
167
168
.collection-row-edit:hover {
0
169
background: var(--bg-tertiary);
170
+
color: var(--text-primary);
171
}
172
173
.collection-detail-header {
174
display: flex;
175
+
flex-direction: column;
176
+
gap: var(--spacing-md);
177
+
padding: var(--spacing-lg);
178
+
background: var(--bg-secondary);
179
border: 1px solid var(--border);
180
border-radius: var(--radius-lg);
181
+
margin-bottom: var(--spacing-lg);
182
position: relative;
183
}
184
···
189
display: flex;
190
align-items: center;
191
justify-content: center;
192
+
background: var(--bg-tertiary);
0
0
0
0
193
color: var(--accent);
194
+
border-radius: var(--radius-lg);
195
+
font-size: 1.5rem;
196
}
197
198
.collection-detail-info {
199
+
display: flex;
200
+
flex-direction: column;
201
+
gap: 6px;
202
}
203
204
.collection-detail-visibility {
205
+
display: inline-flex;
206
align-items: center;
207
+
gap: 4px;
208
+
font-size: 0.65rem;
209
font-weight: 600;
210
+
letter-spacing: 0.05em;
211
+
text-transform: uppercase;
212
color: var(--accent);
213
+
padding: 2px 8px;
214
+
background: var(--accent-subtle);
215
+
border-radius: var(--radius-full);
216
+
width: fit-content;
217
}
218
219
.collection-detail-title {
220
+
font-family: var(--font-display);
221
font-size: 1.5rem;
222
font-weight: 700;
223
color: var(--text-primary);
224
+
line-height: 1.2;
225
+
letter-spacing: -0.02em;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
226
}
227
228
.collection-detail-desc {
229
color: var(--text-secondary);
230
+
font-size: 0.9rem;
231
line-height: 1.5;
0
0
0
0
232
}
233
234
.collection-detail-stats {
235
display: flex;
236
align-items: center;
237
+
gap: var(--spacing-md);
238
+
font-size: 0.8rem;
239
color: var(--text-tertiary);
240
+
margin-top: var(--spacing-xs);
241
}
242
243
.collection-detail-actions {
244
position: absolute;
245
+
top: var(--spacing-md);
246
+
right: var(--spacing-md);
0
0
0
0
0
0
247
display: flex;
248
+
gap: var(--spacing-xs);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
249
}
250
251
+
.collection-detail-actions .annotation-action,
252
.collection-detail-edit,
253
.collection-detail-delete {
254
+
padding: 6px;
255
color: var(--text-tertiary);
256
+
background: var(--bg-tertiary);
257
+
border-radius: var(--radius-sm);
258
+
transition: all 0.15s;
259
border: none;
0
260
cursor: pointer;
0
261
}
262
263
+
.collection-detail-actions .annotation-action:hover,
264
.collection-detail-edit:hover {
265
+
background: var(--bg-hover);
266
+
color: var(--text-primary);
267
}
268
269
.collection-detail-delete:hover {
270
+
background: rgba(255, 69, 58, 0.1);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
271
color: var(--error);
0
0
272
}
273
274
.collection-list-item {
275
width: 100%;
276
text-align: left;
277
+
padding: 12px 14px;
278
border-radius: var(--radius-md);
279
+
background: var(--bg-secondary);
280
+
border: 1px solid var(--border);
281
color: var(--text-primary);
282
+
transition: all 0.15s;
283
display: flex;
284
align-items: center;
285
justify-content: space-between;
286
cursor: pointer;
287
+
margin-bottom: var(--spacing-sm);
288
}
289
290
.collection-list-item:hover {
291
background: var(--bg-hover);
292
+
border-color: var(--accent);
0
0
0
0
293
}
294
295
.collection-list-item:disabled {
296
+
opacity: 0.5;
297
cursor: not-allowed;
298
}
299
300
+
.collection-item-wrapper {
301
+
position: relative;
302
+
}
303
+
304
+
.collection-item-remove {
305
position: absolute;
306
+
left: -40px;
307
+
top: 20px;
308
+
width: 28px;
309
+
height: 28px;
310
+
display: flex;
311
+
align-items: center;
312
+
justify-content: center;
313
+
background: var(--bg-secondary);
314
+
border: 1px solid var(--border);
315
+
border-radius: var(--radius-sm);
316
+
color: var(--text-tertiary);
317
+
cursor: pointer;
318
+
transition: all 0.15s ease;
319
opacity: 0;
0
320
}
321
322
+
.collection-item-wrapper:hover .collection-item-remove {
0
323
opacity: 1;
324
}
325
+
326
+
.collection-item-remove:hover {
327
+
background: rgba(255, 69, 58, 0.1);
328
+
border-color: rgba(255, 69, 58, 0.3);
329
+
color: var(--error);
330
+
}
+222
-109
web/src/css/feed.css
···
0
0
0
0
0
0
0
0
0
1
.feed {
2
display: flex;
3
flex-direction: column;
4
-
gap: 16px;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
5
}
6
7
.feed-header {
8
display: flex;
9
align-items: center;
10
justify-content: space-between;
11
-
margin-bottom: 8px;
12
}
13
14
.feed-title {
15
-
font-size: 1.5rem;
16
-
font-weight: 700;
0
0
17
}
18
19
.feed-filters {
20
display: flex;
21
-
gap: 8px;
22
-
margin-bottom: 24px;
23
-
padding: 4px;
24
-
background: var(--bg-tertiary);
25
-
border-radius: var(--radius-lg);
26
-
width: fit-content;
27
-
max-width: 100%;
28
flex-wrap: wrap;
29
}
30
31
.filter-tab {
32
-
padding: 8px 16px;
33
-
font-size: 0.9rem;
34
font-weight: 500;
35
-
color: var(--text-secondary);
36
background: transparent;
37
border: none;
38
border-radius: var(--radius-md);
···
41
}
42
43
.filter-tab:hover {
44
-
color: var(--text-primary);
45
-
background: var(--bg-hover);
46
}
47
48
.filter-tab.active {
49
color: var(--text-primary);
50
-
background: var(--bg-card);
51
-
box-shadow: var(--shadow-sm);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
52
}
53
54
.page-header {
55
-
margin-bottom: 32px;
56
}
57
58
.page-title {
0
59
font-size: 2rem;
60
font-weight: 700;
61
margin-bottom: 8px;
0
0
62
}
63
64
.page-description {
65
color: var(--text-secondary);
66
font-size: 1.1rem;
0
67
}
68
69
.url-input-wrapper {
70
-
margin-bottom: 24px;
0
71
}
72
73
.url-input-container {
74
display: flex;
75
-
gap: 12px;
76
}
77
78
.url-input {
79
width: 100%;
80
-
padding: 16px;
81
background: var(--bg-secondary);
82
border: 1px solid var(--border);
83
border-radius: var(--radius-md);
84
color: var(--text-primary);
85
-
font-size: 1.1rem;
86
-
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
87
-
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
88
}
89
90
.url-input:focus {
91
outline: none;
92
border-color: var(--accent);
93
-
box-shadow: 0 0 0 4px var(--accent-subtle);
94
-
background: var(--bg-primary);
95
}
96
97
.url-input::placeholder {
···
102
display: flex;
103
align-items: center;
104
justify-content: space-between;
105
-
margin-bottom: 16px;
106
-
flex-wrap: wrap;
107
-
gap: 12px;
108
}
109
110
.back-link {
111
display: inline-flex;
112
align-items: center;
113
-
gap: 8px;
114
color: var(--text-secondary);
115
-
font-size: 0.9rem;
0
116
text-decoration: none;
117
-
margin-bottom: 24px;
118
-
transition: color 0.15s;
0
0
0
119
}
120
121
.back-link:hover {
122
-
color: var(--accent);
123
-
}
124
-
125
-
.new-page {
126
-
max-width: 600px;
127
-
margin: 0 auto;
128
-
display: flex;
129
-
flex-direction: column;
130
-
gap: 32px;
131
-
}
132
-
133
-
@media (max-width: 640px) {
134
-
.main-content {
135
-
padding: 16px 12px;
136
-
}
137
-
138
-
.page-title {
139
-
font-size: 1.5rem;
140
-
}
141
-
}
142
-
143
-
.user-url-page {
144
-
max-width: 800px;
145
}
146
147
.url-target-info {
148
display: flex;
149
flex-direction: column;
150
gap: 4px;
151
-
padding: 16px;
152
background: var(--bg-secondary);
153
border: 1px solid var(--border);
154
border-radius: var(--radius-md);
155
-
margin-bottom: 24px;
156
}
157
158
.url-target-label {
159
-
font-size: 0.875rem;
160
-
color: var(--text-secondary);
0
0
0
161
}
162
163
.url-target-link {
164
color: var(--accent);
165
-
font-size: 0.95rem;
0
0
166
word-break: break-all;
167
-
text-decoration: none;
168
}
169
170
.url-target-link:hover {
···
175
display: flex;
176
align-items: center;
177
justify-content: space-between;
178
-
gap: 16px;
179
padding: 12px 16px;
180
-
background: var(--accent-subtle);
181
-
border: 1px solid var(--accent);
182
border-radius: var(--radius-md);
183
-
margin-bottom: 16px;
184
}
185
186
.share-notes-info {
187
display: flex;
188
align-items: center;
189
-
gap: 8px;
190
color: var(--text-primary);
191
-
font-size: 0.9rem;
0
192
}
193
194
.share-notes-actions {
195
display: flex;
196
-
gap: 8px;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
197
}
198
199
@media (max-width: 640px) {
200
-
.share-notes-banner {
201
-
flex-direction: column;
202
-
align-items: stretch;
203
}
204
205
-
.share-notes-actions {
206
-
justify-content: flex-end;
0
0
207
}
208
}
209
210
-
.feed-tab {
211
-
padding: 8px 16px;
212
-
font-size: 1rem;
213
-
font-weight: 500;
214
-
color: var(--text-secondary);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
215
background: transparent;
216
border: none;
217
-
border-bottom: 2px solid transparent;
0
218
cursor: pointer;
219
-
transition: all 0.2s ease;
220
-
margin-bottom: -1px;
221
-
}
222
-
223
-
.feed-tab:hover {
224
-
color: var(--text-primary);
225
}
226
227
-
.feed-tab.active {
228
-
color: var(--text-primary);
229
-
border-bottom-color: var(--text-primary);
230
-
font-weight: 600;
231
}
232
233
-
.filter-pill {
234
-
padding: 6px 16px;
235
-
font-size: 0.9rem;
236
-
font-weight: 500;
237
-
color: var(--text-secondary);
238
-
background: var(--bg-tertiary);
239
-
border: 1px solid transparent;
240
-
border-radius: 999px;
241
-
cursor: pointer;
242
-
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
243
}
244
245
-
.filter-pill:hover {
246
-
background: var(--bg-secondary);
247
-
color: var(--text-primary);
248
-
border-color: var(--border);
249
}
250
251
-
.filter-pill.active {
252
-
background: var(--text-primary);
253
-
color: var(--bg-primary);
254
-
font-weight: 600;
0
0
0
0
0
0
0
0
0
255
}
···
1
+
.feed-container {
2
+
background: var(--bg-elevated);
3
+
border: 1px solid var(--border-hover);
4
+
border-radius: var(--radius-xl);
5
+
overflow: visible;
6
+
padding: 8px;
7
+
position: relative;
8
+
}
9
+
10
.feed {
11
display: flex;
12
flex-direction: column;
13
+
gap: 0;
14
+
width: 100%;
15
+
overflow: visible;
16
+
border-radius: var(--radius-lg);
17
+
position: relative;
18
+
}
19
+
20
+
.feed > * {
21
+
border-bottom: 1px solid var(--border);
22
+
position: relative;
23
+
}
24
+
25
+
.feed > *:last-child {
26
+
border-bottom: none;
27
+
}
28
+
29
+
.feed > *:hover {
30
+
z-index: 10;
31
+
}
32
+
33
+
.feed-page {
34
+
animation: fadeIn 0.3s ease-out;
35
+
}
36
+
37
+
@keyframes fadeIn {
38
+
from {
39
+
opacity: 0;
40
+
}
41
+
to {
42
+
opacity: 1;
43
+
}
44
}
45
46
.feed-header {
47
display: flex;
48
align-items: center;
49
justify-content: space-between;
50
+
margin-bottom: 20px;
51
}
52
53
.feed-title {
54
+
font-family: var(--font-display);
55
+
font-size: 1.25rem;
56
+
font-weight: 600;
57
+
letter-spacing: -0.02em;
58
}
59
60
.feed-filters {
61
display: flex;
62
+
gap: 4px;
63
+
margin-bottom: 20px;
64
+
background: transparent;
65
+
padding: 0;
66
+
border: none;
0
0
67
flex-wrap: wrap;
68
}
69
70
.filter-tab {
71
+
padding: 8px 14px;
72
+
font-size: 0.875rem;
73
font-weight: 500;
74
+
color: var(--text-tertiary);
75
background: transparent;
76
border: none;
77
border-radius: var(--radius-md);
···
80
}
81
82
.filter-tab:hover {
83
+
color: var(--text-secondary);
84
+
background: var(--bg-tertiary);
85
}
86
87
.filter-tab.active {
88
color: var(--text-primary);
89
+
background: var(--bg-tertiary);
90
+
}
91
+
92
+
.filter-pill {
93
+
padding: 8px 14px;
94
+
font-size: 0.8rem;
95
+
font-weight: 600;
96
+
color: var(--text-secondary);
97
+
background: var(--bg-tertiary);
98
+
border: none;
99
+
border-radius: var(--radius-full);
100
+
cursor: pointer;
101
+
transition: all 0.15s;
102
+
}
103
+
104
+
.filter-pill:hover {
105
+
background: var(--bg-hover);
106
+
color: var(--text-primary);
107
+
}
108
+
109
+
.filter-pill.active {
110
+
background: var(--accent);
111
+
color: var(--bg-primary);
112
}
113
114
.page-header {
115
+
margin-bottom: 28px;
116
}
117
118
.page-title {
119
+
font-family: var(--font-display);
120
font-size: 2rem;
121
font-weight: 700;
122
margin-bottom: 8px;
123
+
letter-spacing: -0.02em;
124
+
color: var(--text-primary);
125
}
126
127
.page-description {
128
color: var(--text-secondary);
129
font-size: 1.1rem;
130
+
line-height: 1.5;
131
}
132
133
.url-input-wrapper {
134
+
margin-bottom: var(--spacing-lg);
135
+
position: relative;
136
}
137
138
.url-input-container {
139
display: flex;
140
+
gap: var(--spacing-sm);
141
}
142
143
.url-input {
144
width: 100%;
145
+
padding: 12px 16px;
146
background: var(--bg-secondary);
147
border: 1px solid var(--border);
148
border-radius: var(--radius-md);
149
color: var(--text-primary);
150
+
font-size: 0.9rem;
151
+
transition: all 0.15s ease;
0
152
}
153
154
.url-input:focus {
155
outline: none;
156
border-color: var(--accent);
157
+
box-shadow: 0 0 0 3px var(--accent-subtle);
0
158
}
159
160
.url-input::placeholder {
···
165
display: flex;
166
align-items: center;
167
justify-content: space-between;
168
+
margin-bottom: var(--spacing-md);
0
0
169
}
170
171
.back-link {
172
display: inline-flex;
173
align-items: center;
174
+
gap: 6px;
175
color: var(--text-secondary);
176
+
font-size: 0.8rem;
177
+
font-weight: 500;
178
text-decoration: none;
179
+
margin-bottom: var(--spacing-lg);
180
+
padding: 6px 12px;
181
+
background: var(--bg-tertiary);
182
+
border-radius: var(--radius-sm);
183
+
transition: all 0.15s;
184
}
185
186
.back-link:hover {
187
+
background: var(--bg-hover);
188
+
color: var(--text-primary);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
189
}
190
191
.url-target-info {
192
display: flex;
193
flex-direction: column;
194
gap: 4px;
195
+
padding: 12px 16px;
196
background: var(--bg-secondary);
197
border: 1px solid var(--border);
198
border-radius: var(--radius-md);
199
+
margin-bottom: var(--spacing-lg);
200
}
201
202
.url-target-label {
203
+
font-size: 0.65rem;
204
+
text-transform: uppercase;
205
+
letter-spacing: 0.05em;
206
+
font-weight: 600;
207
+
color: var(--text-tertiary);
208
}
209
210
.url-target-link {
211
color: var(--accent);
212
+
font-size: 0.85rem;
213
+
font-weight: 500;
214
+
text-decoration: none;
215
word-break: break-all;
216
+
line-height: 1.4;
217
}
218
219
.url-target-link:hover {
···
224
display: flex;
225
align-items: center;
226
justify-content: space-between;
227
+
gap: var(--spacing-md);
228
padding: 12px 16px;
229
+
background: var(--bg-secondary);
230
+
border: 1px solid var(--border);
231
border-radius: var(--radius-md);
232
+
margin-bottom: var(--spacing-md);
233
}
234
235
.share-notes-info {
236
display: flex;
237
align-items: center;
238
+
gap: var(--spacing-sm);
239
color: var(--text-primary);
240
+
font-size: 0.85rem;
241
+
font-weight: 500;
242
}
243
244
.share-notes-actions {
245
display: flex;
246
+
gap: var(--spacing-sm);
247
+
}
248
+
249
+
.empty-state {
250
+
display: flex;
251
+
flex-direction: column;
252
+
align-items: center;
253
+
justify-content: center;
254
+
padding: 48px 24px;
255
+
text-align: center;
256
+
}
257
+
258
+
.empty-state-icon {
259
+
width: 56px;
260
+
height: 56px;
261
+
display: flex;
262
+
align-items: center;
263
+
justify-content: center;
264
+
background: var(--bg-tertiary);
265
+
border-radius: var(--radius-lg);
266
+
color: var(--text-tertiary);
267
+
margin-bottom: 16px;
268
+
}
269
+
270
+
.empty-state-title {
271
+
font-size: 1.1rem;
272
+
font-weight: 600;
273
+
color: var(--text-primary);
274
+
margin-bottom: 6px;
275
+
}
276
+
277
+
.empty-state-text {
278
+
font-size: 0.9rem;
279
+
color: var(--text-secondary);
280
+
max-width: 300px;
281
+
line-height: 1.5;
282
}
283
284
@media (max-width: 640px) {
285
+
.feed-filters {
286
+
gap: 4px;
0
287
}
288
289
+
.filter-tab,
290
+
.filter-pill {
291
+
padding: 6px 10px;
292
+
font-size: 0.75rem;
293
}
294
}
295
296
+
.feed-controls {
297
+
display: flex;
298
+
flex-direction: column;
299
+
gap: var(--spacing-sm);
300
+
margin-bottom: var(--spacing-lg);
301
+
}
302
+
303
+
.active-filter-banner {
304
+
display: inline-flex;
305
+
align-items: center;
306
+
gap: var(--spacing-sm);
307
+
padding: 6px 10px 6px 12px;
308
+
background: var(--accent-subtle);
309
+
border: 1px solid var(--accent);
310
+
border-radius: var(--radius-full);
311
+
font-size: 0.8rem;
312
+
color: var(--accent);
313
+
margin-bottom: var(--spacing-md);
314
+
width: fit-content;
315
+
}
316
+
317
+
.active-filter-banner strong {
318
+
color: var(--accent-text);
319
+
}
320
+
321
+
.active-filter-clear {
322
+
display: flex;
323
+
align-items: center;
324
+
justify-content: center;
325
+
width: 20px;
326
+
height: 20px;
327
background: transparent;
328
border: none;
329
+
border-radius: var(--radius-full);
330
+
color: var(--accent);
331
cursor: pointer;
332
+
transition: all 0.15s;
0
0
0
0
0
333
}
334
335
+
.active-filter-clear:hover {
336
+
background: var(--accent);
337
+
color: white;
0
338
}
339
340
+
.keyboard-hint {
341
+
display: none;
342
+
align-items: center;
343
+
gap: 4px;
344
+
font-size: 0.7rem;
345
+
color: var(--text-tertiary);
346
+
margin-left: auto;
0
0
0
347
}
348
349
+
@media (min-width: 768px) {
350
+
.keyboard-hint {
351
+
display: flex;
352
+
}
353
}
354
355
+
.kbd {
356
+
display: inline-flex;
357
+
align-items: center;
358
+
justify-content: center;
359
+
min-width: 20px;
360
+
height: 20px;
361
+
padding: 0 6px;
362
+
background: var(--bg-tertiary);
363
+
border: 1px solid var(--border);
364
+
border-radius: var(--radius-xs);
365
+
font-size: 0.65rem;
366
+
font-family: var(--font-mono);
367
+
color: var(--text-secondary);
368
}
+310
-342
web/src/css/layout.css
···
1
-
.layout {
2
-
display: flex;
3
min-height: 100vh;
4
background: var(--bg-primary);
5
}
6
7
-
.sidebar {
8
-
position: fixed;
9
-
left: 0;
10
top: 0;
11
-
bottom: 0;
12
-
width: 240px;
13
-
background: var(--bg-primary);
14
-
border-right: 1px solid var(--border);
0
0
0
0
0
0
0
0
15
display: flex;
16
-
flex-direction: column;
17
-
z-index: 50;
18
-
padding-bottom: 20px;
19
}
20
21
-
.sidebar-header {
22
-
height: 64px;
23
display: flex;
24
align-items: center;
25
-
padding: 0 20px;
26
-
margin-bottom: 12px;
27
text-decoration: none;
28
color: var(--text-primary);
29
-
}
30
-
31
-
.sidebar-logo {
32
-
width: 24px;
33
-
height: 24px;
34
-
object-fit: contain;
35
-
margin-right: 12px;
36
}
37
38
-
.sidebar-brand {
39
-
font-size: 1rem;
40
-
font-weight: 600;
41
-
color: var(--text-primary);
42
-
letter-spacing: -0.01em;
43
}
44
45
-
.sidebar-nav {
46
-
flex: 1;
47
display: flex;
48
-
flex-direction: column;
49
gap: 4px;
50
-
padding: 0 12px;
51
-
overflow-y: auto;
52
}
53
54
-
.sidebar-link {
55
-
display: flex;
56
-
align-items: center;
57
-
gap: 12px;
58
-
padding: 8px 12px;
59
-
border-radius: var(--radius-md);
60
color: var(--text-secondary);
61
text-decoration: none;
62
font-size: 0.9rem;
63
font-weight: 500;
64
-
transition: all 0.15s ease;
0
65
}
66
67
-
.sidebar-link:hover {
68
-
background: var(--bg-tertiary);
69
color: var(--text-primary);
0
70
}
71
72
-
.sidebar-link.active {
0
73
background: var(--bg-tertiary);
74
-
color: var(--text-primary);
75
}
76
77
-
.sidebar-link svg {
78
-
width: 18px;
79
-
height: 18px;
80
-
color: var(--text-tertiary);
81
-
transition: color 0.15s ease;
82
}
83
84
-
.sidebar-link:hover svg,
85
-
.sidebar-link.active svg {
86
-
color: var(--text-primary);
87
-
}
88
-
89
-
.sidebar-section-title {
90
-
padding: 24px 12px 8px;
91
-
font-size: 0.75rem;
92
-
font-weight: 600;
93
-
color: var(--text-tertiary);
94
-
text-transform: uppercase;
95
-
letter-spacing: 0.05em;
96
-
}
97
-
98
-
.notification-badge {
99
-
background: var(--accent);
100
-
color: white;
101
-
font-size: 0.7rem;
102
-
font-weight: 600;
103
-
padding: 0 6px;
104
-
height: 18px;
105
-
border-radius: 99px;
106
display: flex;
107
align-items: center;
108
-
justify-content: center;
109
-
margin-left: auto;
110
}
111
112
-
.sidebar-new-btn {
113
display: flex;
114
align-items: center;
115
-
gap: 10px;
116
-
margin: 0 12px 16px;
117
-
padding: 10px 16px;
118
-
background: var(--text-primary);
119
-
color: var(--bg-primary);
120
border-radius: var(--radius-md);
121
-
font-size: 0.9rem;
122
-
font-weight: 600;
0
0
0
0
123
text-decoration: none;
124
-
transition: opacity 0.15s;
125
-
justify-content: center;
126
}
127
128
-
.sidebar-new-btn:hover {
129
-
opacity: 0.9;
0
130
}
131
132
-
.sidebar-footer {
133
-
padding: 0 12px;
134
-
margin-top: auto;
0
0
0
0
0
0
135
}
136
137
-
.sidebar-user {
138
display: flex;
139
align-items: center;
140
-
gap: 10px;
141
-
padding: 8px 12px;
0
0
142
border-radius: var(--radius-md);
143
-
cursor: pointer;
144
-
transition: background 0.15s ease;
0
0
145
}
146
147
-
.sidebar-user:hover,
148
-
.sidebar-user.active {
149
-
background: var(--bg-tertiary);
150
}
151
152
-
.sidebar-avatar {
153
-
width: 32px;
154
-
height: 32px;
155
-
border-radius: 50%;
156
background: var(--bg-tertiary);
0
0
0
157
display: flex;
158
align-items: center;
159
justify-content: center;
160
color: var(--text-secondary);
161
font-size: 0.8rem;
162
-
font-weight: 500;
163
-
overflow: hidden;
164
-
flex-shrink: 0;
165
-
border: 1px solid var(--border);
166
}
167
168
-
.sidebar-avatar img {
0
0
0
0
169
width: 100%;
170
height: 100%;
171
object-fit: cover;
172
}
173
174
-
.sidebar-user-info {
175
-
flex: 1;
176
-
min-width: 0;
177
-
display: flex;
178
-
flex-direction: column;
179
-
}
180
-
181
-
.sidebar-user-name {
182
-
font-size: 0.85rem;
183
-
font-weight: 500;
184
color: var(--text-primary);
0
185
}
186
187
-
.sidebar-user-handle {
188
-
font-size: 0.75rem;
189
-
color: var(--text-tertiary);
190
}
191
192
-
.sidebar-dropdown {
193
position: absolute;
194
-
bottom: 74px;
195
-
left: 12px;
196
-
width: 216px;
197
-
background: var(--bg-card);
198
border: 1px solid var(--border);
199
-
border-radius: var(--radius-md);
0
200
box-shadow: var(--shadow-lg);
201
-
padding: 4px;
202
-
z-index: 1000;
203
-
overflow: hidden;
204
-
animation: scaleIn 0.1s ease-out;
205
-
transform-origin: bottom center;
206
}
207
208
-
@keyframes scaleIn {
209
-
from {
210
-
opacity: 0;
211
-
transform: scale(0.95);
212
-
}
213
-
214
-
to {
215
-
opacity: 1;
216
-
transform: scale(1);
217
-
}
218
}
219
220
-
.sidebar-dropdown-item {
221
display: flex;
222
align-items: center;
223
gap: 10px;
224
width: 100%;
225
-
padding: 8px 12px;
226
-
font-size: 0.85rem;
227
color: var(--text-secondary);
0
0
228
text-decoration: none;
229
-
background: transparent;
230
-
cursor: pointer;
231
-
border-radius: var(--radius-sm);
232
transition: all 0.15s;
0
233
border: none;
0
0
234
}
235
236
-
.sidebar-dropdown-item:hover {
237
-
background: var(--bg-tertiary);
238
color: var(--text-primary);
239
}
240
241
-
.sidebar-dropdown-item.danger:hover {
242
-
background: rgba(239, 68, 68, 0.1);
243
color: var(--error);
244
}
245
246
-
.main-layout {
247
-
flex: 1;
248
-
margin-left: 240px;
249
-
margin-right: 280px;
250
-
min-height: 100vh;
251
}
252
253
-
.main-content-wrapper {
254
-
max-width: 640px;
255
-
margin: 0 auto;
256
-
padding: 40px 24px;
257
-
}
258
-
259
-
.right-sidebar {
260
-
position: fixed;
261
-
right: 0;
262
-
top: 0;
263
-
bottom: 0;
264
-
width: 280px;
265
-
background: var(--bg-primary);
266
-
border-left: 1px solid var(--border);
267
-
padding: 32px 24px;
268
-
overflow-y: auto;
269
display: flex;
270
-
flex-direction: column;
271
-
gap: 32px;
272
}
273
274
-
.right-section {
275
-
display: flex;
276
-
flex-direction: column;
277
-
gap: 12px;
0
0
278
}
279
280
-
.right-section-title {
281
-
font-size: 0.75rem;
282
-
font-weight: 600;
283
-
color: var(--text-primary);
284
-
margin-bottom: 4px;
285
}
286
287
-
.right-section-desc {
288
-
font-size: 0.85rem;
289
-
line-height: 1.5;
290
-
color: var(--text-secondary);
291
}
292
293
-
.right-extension-btn {
294
-
display: inline-flex;
295
-
align-items: center;
296
-
gap: 8px;
297
-
padding: 8px 12px;
298
-
background: var(--bg-primary);
299
-
border: 1px solid var(--border);
300
-
border-radius: var(--radius-md);
301
-
color: var(--text-primary);
302
-
font-size: 0.85rem;
303
-
font-weight: 500;
304
-
text-decoration: none;
305
-
transition: all 0.15s ease;
306
-
width: fit-content;
307
}
308
309
-
.right-extension-btn:hover {
310
-
border-color: var(--text-tertiary);
311
-
background: var(--bg-tertiary);
0
312
}
313
314
-
.right-links {
0
315
display: flex;
316
flex-direction: column;
317
-
gap: 4px;
318
-
}
319
-
320
-
.right-link {
321
-
display: flex;
322
-
align-items: center;
323
-
justify-content: space-between;
324
-
padding: 6px 0;
325
-
color: var(--text-secondary);
326
-
font-size: 0.9rem;
327
-
transition: color 0.15s;
328
-
text-decoration: none;
329
}
330
331
-
.right-link:hover {
0
332
color: var(--text-primary);
0
333
}
334
335
-
.right-link svg {
336
-
width: 16px;
337
-
height: 16px;
338
color: var(--text-tertiary);
339
-
transition: all 0.15s;
340
}
341
342
-
.right-link:hover svg {
343
-
color: var(--text-secondary);
0
0
344
}
345
346
-
.tangled-icon {
347
-
width: 16px;
348
-
height: 16px;
349
-
background-color: var(--text-tertiary);
350
-
-webkit-mask: url("../assets/tangled.svg") no-repeat center / contain;
351
-
mask: url("../assets/tangled.svg") no-repeat center / contain;
352
-
transition: background-color 0.15s;
353
-
}
354
-
355
-
.right-link:hover .tangled-icon {
356
-
background-color: var(--text-secondary);
357
}
358
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);
0
0
0
0
0
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);
0
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 {
405
display: none;
406
position: fixed;
407
bottom: 0;
···
411
backdrop-filter: blur(12px);
412
-webkit-backdrop-filter: blur(12px);
413
border-top: 1px solid var(--border);
414
-
padding: 8px 16px;
415
-
padding-bottom: calc(8px + env(safe-area-inset-bottom, 0));
416
z-index: 100;
417
}
418
419
-
.mobile-nav-inner {
420
-
display: flex;
421
-
justify-content: space-between;
422
align-items: center;
423
}
424
425
-
.mobile-nav-item {
426
display: flex;
427
flex-direction: column;
428
align-items: center;
429
-
justify-content: center;
430
gap: 4px;
0
431
color: var(--text-tertiary);
432
text-decoration: none;
433
font-size: 0.65rem;
434
font-weight: 500;
435
-
width: 60px;
436
transition: color 0.15s;
0
437
}
438
439
-
.mobile-nav-item.active {
440
-
color: var(--text-primary);
441
}
442
443
-
.mobile-nav-item svg {
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
444
width: 24px;
445
height: 24px;
0
0
446
}
447
448
-
.mobile-nav-new {
449
-
width: 48px;
450
-
height: 36px;
451
-
border-radius: var(--radius-md);
452
-
background: var(--text-primary);
453
-
color: var(--bg-primary);
0
0
0
0
0
0
0
0
0
0
0
454
display: flex;
455
align-items: center;
456
justify-content: center;
0
0
457
}
458
459
-
.mobile-nav-new svg {
460
-
width: 20px;
461
-
height: 20px;
0
0
0
0
0
0
0
0
0
0
462
}
463
464
-
@media (max-width: 1200px) {
465
-
.right-sidebar {
466
-
display: none;
467
-
}
468
469
-
.main-layout {
470
-
margin-right: 0;
471
-
}
472
}
473
474
-
@media (max-width: 768px) {
475
-
.sidebar {
476
-
display: none;
477
-
}
0
0
478
479
-
.main-layout {
480
-
margin-left: 0;
481
-
padding-bottom: 80px;
482
-
width: 100%;
483
-
min-width: 0;
484
-
}
0
0
0
0
0
0
0
0
485
486
-
.main-content-wrapper {
487
-
padding: 20px 16px;
488
-
max-width: 100%;
489
-
width: 100%;
490
-
overflow-x: hidden;
491
-
min-width: 0;
492
-
}
493
494
-
.mobile-nav {
0
495
display: block;
496
-
max-width: 100vw;
497
}
0
498
499
-
.card,
500
-
.annotation-card,
501
-
.collection-card,
502
-
.profile-header,
503
-
.api-keys-section {
504
-
overflow-x: hidden;
505
-
max-width: 100%;
506
}
507
508
-
code {
509
-
word-break: break-all;
510
-
overflow-wrap: break-word;
511
-
}
512
-
513
-
pre {
514
-
overflow-x: auto;
515
-
max-width: 100%;
516
}
517
518
-
input,
519
-
textarea {
520
-
max-width: 100%;
521
}
522
523
-
.flex-row,
524
-
[style*="display: flex"][style*="gap"] {
525
-
flex-wrap: wrap;
526
}
0
527
528
-
.static-page {
529
-
overflow-x: hidden;
0
530
}
531
532
-
.static-page ol,
533
-
.static-page ul {
534
-
padding-left: 1.25rem;
535
}
536
537
-
.static-page code {
538
-
font-size: 0.75rem;
539
-
word-break: break-all;
540
}
541
}
···
1
+
.app {
0
2
min-height: 100vh;
3
background: var(--bg-primary);
4
}
5
6
+
.top-nav {
7
+
position: sticky;
0
8
top: 0;
9
+
z-index: 100;
10
+
background: var(--nav-bg);
11
+
backdrop-filter: blur(12px);
12
+
-webkit-backdrop-filter: blur(12px);
13
+
border-bottom: 1px solid var(--border);
14
+
}
15
+
16
+
.top-nav-inner {
17
+
max-width: 1200px;
18
+
margin: 0 auto;
19
+
padding: 0 32px;
20
+
height: 56px;
21
display: flex;
22
+
align-items: center;
23
+
gap: 32px;
0
24
}
25
26
+
.top-nav-logo {
0
27
display: flex;
28
align-items: center;
29
+
gap: 10px;
0
30
text-decoration: none;
31
color: var(--text-primary);
32
+
font-weight: 700;
33
+
font-size: 1.1rem;
34
+
flex-shrink: 0;
0
0
0
0
35
}
36
37
+
.top-nav-logo img {
38
+
width: 26px;
39
+
height: 26px;
0
0
40
}
41
42
+
.top-nav-links {
0
43
display: flex;
44
+
align-items: center;
45
gap: 4px;
46
+
flex: 1;
0
47
}
48
49
+
.top-nav-link {
50
+
padding: 8px 14px;
0
0
0
0
51
color: var(--text-secondary);
52
text-decoration: none;
53
font-size: 0.9rem;
54
font-weight: 500;
55
+
border-radius: var(--radius-md);
56
+
transition: all 0.15s;
57
}
58
59
+
.top-nav-link:hover {
0
60
color: var(--text-primary);
61
+
background: var(--bg-hover);
62
}
63
64
+
.top-nav-link.active {
65
+
color: var(--text-primary);
66
background: var(--bg-tertiary);
0
67
}
68
69
+
.top-nav-link.extension-link {
70
+
display: flex;
71
+
align-items: center;
72
+
gap: 6px;
0
73
}
74
75
+
.top-nav-actions {
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
76
display: flex;
77
align-items: center;
78
+
gap: 8px;
0
79
}
80
81
+
.top-nav-icon-btn {
82
display: flex;
83
align-items: center;
84
+
justify-content: center;
85
+
width: 36px;
86
+
height: 36px;
0
0
87
border-radius: var(--radius-md);
88
+
background: transparent;
89
+
border: none;
90
+
color: var(--text-secondary);
91
+
cursor: pointer;
92
+
transition: all 0.15s;
93
+
position: relative;
94
text-decoration: none;
0
0
95
}
96
97
+
.top-nav-icon-btn:hover {
98
+
background: var(--bg-hover);
99
+
color: var(--text-primary);
100
}
101
102
+
.notif-dot {
103
+
position: absolute;
104
+
top: 6px;
105
+
right: 6px;
106
+
width: 8px;
107
+
height: 8px;
108
+
background: var(--accent);
109
+
border-radius: 50%;
110
+
border: 2px solid var(--bg-primary);
111
}
112
113
+
.top-nav-new-btn {
114
display: flex;
115
align-items: center;
116
+
gap: 6px;
117
+
padding: 8px 16px;
118
+
background: var(--accent);
119
+
color: var(--bg-primary);
120
border-radius: var(--radius-md);
121
+
font-size: 0.875rem;
122
+
font-weight: 600;
123
+
text-decoration: none;
124
+
transition: all 0.15s;
125
}
126
127
+
.top-nav-new-btn:hover {
128
+
background: var(--accent-hover);
0
129
}
130
131
+
.top-nav-avatar {
132
+
width: 34px;
133
+
height: 34px;
134
+
border-radius: var(--radius-md);
135
background: var(--bg-tertiary);
136
+
border: none;
137
+
cursor: pointer;
138
+
overflow: hidden;
139
display: flex;
140
align-items: center;
141
justify-content: center;
142
color: var(--text-secondary);
143
font-size: 0.8rem;
144
+
font-weight: 600;
145
+
transition: opacity 0.15s;
0
0
146
}
147
148
+
.top-nav-avatar:hover {
149
+
opacity: 0.85;
150
+
}
151
+
152
+
.top-nav-avatar img {
153
width: 100%;
154
height: 100%;
155
object-fit: cover;
156
}
157
158
+
.top-nav-mobile-toggle {
159
+
display: none;
160
+
align-items: center;
161
+
justify-content: center;
162
+
width: 40px;
163
+
height: 40px;
164
+
border: none;
165
+
background: transparent;
0
0
166
color: var(--text-primary);
167
+
cursor: pointer;
168
}
169
170
+
.top-nav-dropdown {
171
+
position: relative;
0
172
}
173
174
+
.dropdown-menu {
175
position: absolute;
176
+
top: calc(100% + 8px);
177
+
min-width: 200px;
178
+
background: var(--bg-elevated);
0
179
border: 1px solid var(--border);
180
+
border-radius: var(--radius-lg);
181
+
padding: 6px;
182
box-shadow: var(--shadow-lg);
183
+
z-index: 200;
0
0
0
0
184
}
185
186
+
.dropdown-right {
187
+
right: 0;
0
0
0
0
0
0
0
0
188
}
189
190
+
.dropdown-item {
191
display: flex;
192
align-items: center;
193
gap: 10px;
194
width: 100%;
195
+
padding: 10px 12px;
196
+
border-radius: var(--radius-md);
197
color: var(--text-secondary);
198
+
font-size: 0.875rem;
199
+
font-weight: 500;
200
text-decoration: none;
0
0
0
201
transition: all 0.15s;
202
+
background: none;
203
border: none;
204
+
cursor: pointer;
205
+
text-align: left;
206
}
207
208
+
.dropdown-item:hover {
209
+
background: var(--bg-hover);
210
color: var(--text-primary);
211
}
212
213
+
.dropdown-item.danger:hover {
214
+
background: rgba(217, 119, 102, 0.12);
215
color: var(--error);
216
}
217
218
+
.dropdown-external {
219
+
margin-left: auto;
220
+
opacity: 0.4;
0
0
221
}
222
223
+
.tangled-icon-wrapper {
224
+
width: 16px;
225
+
height: 16px;
0
0
0
0
0
0
0
0
0
0
0
0
0
226
display: flex;
227
+
align-items: center;
228
+
justify-content: center;
229
}
230
231
+
.tangled-icon-wrapper img {
232
+
width: 16px;
233
+
height: 16px;
234
+
filter: grayscale(100%) brightness(1.5);
235
+
opacity: 0.6;
236
+
transition: all 0.15s;
237
}
238
239
+
.dropdown-item:hover .tangled-icon-wrapper img {
240
+
opacity: 0.9;
0
0
0
241
}
242
243
+
[data-theme="light"] .tangled-icon-wrapper img {
244
+
filter: grayscale(100%) brightness(0) invert(0.35);
245
+
opacity: 1;
0
246
}
247
248
+
[data-theme="light"] .dropdown-item:hover .tangled-icon-wrapper img {
249
+
filter: grayscale(100%) brightness(0) invert(0.1);
250
+
opacity: 1;
0
0
0
0
0
0
0
0
0
0
0
251
}
252
253
+
.dropdown-divider {
254
+
height: 1px;
255
+
background: var(--border);
256
+
margin: 6px 0;
257
}
258
259
+
.dropdown-user-info {
260
+
padding: 8px 12px;
261
display: flex;
262
flex-direction: column;
263
+
gap: 2px;
0
0
0
0
0
0
0
0
0
0
0
264
}
265
266
+
.dropdown-user-name {
267
+
font-weight: 600;
268
color: var(--text-primary);
269
+
font-size: 0.9rem;
270
}
271
272
+
.dropdown-user-handle {
0
0
273
color: var(--text-tertiary);
274
+
font-size: 0.8rem;
275
}
276
277
+
.main-content {
278
+
max-width: 1300px;
279
+
margin: 0 auto;
280
+
padding: 32px 56px 80px;
281
}
282
283
+
.mobile-menu {
284
+
display: none;
285
+
position: absolute;
286
+
top: 100%;
287
+
left: 0;
288
+
right: 0;
289
+
background: var(--bg-secondary);
290
+
border-bottom: 1px solid var(--border);
291
+
padding: 12px 16px;
0
0
292
}
293
294
+
.mobile-menu-link {
0
295
display: flex;
296
align-items: center;
297
+
gap: 12px;
298
+
padding: 12px 16px;
299
+
color: var(--text-secondary);
300
+
text-decoration: none;
301
+
font-size: 0.95rem;
302
+
font-weight: 500;
303
+
border-radius: var(--radius-md);
304
+
transition: all 0.15s;
305
}
306
307
+
.mobile-menu-link:hover,
308
+
.mobile-menu-link.active {
309
+
background: var(--bg-hover);
310
+
color: var(--text-primary);
0
0
311
}
312
313
+
.mobile-menu-link.active {
314
+
color: var(--accent);
0
315
}
316
317
+
.mobile-menu-divider {
318
+
height: 1px;
319
+
background: var(--border);
320
+
margin: 8px 0;
321
}
322
323
+
.notification-badge {
324
+
background: var(--accent);
325
+
color: var(--bg-primary);
326
+
font-size: 0.7rem;
327
+
font-weight: 700;
328
+
padding: 2px 6px;
329
+
border-radius: var(--radius-full);
330
+
margin-left: auto;
0
0
0
331
}
332
333
+
.mobile-bottom-nav {
0
0
0
0
0
334
display: none;
335
position: fixed;
336
bottom: 0;
···
340
backdrop-filter: blur(12px);
341
-webkit-backdrop-filter: blur(12px);
342
border-top: 1px solid var(--border);
343
+
padding: 8px 8px calc(8px + env(safe-area-inset-bottom));
0
344
z-index: 100;
345
}
346
347
+
.mobile-bottom-nav {
348
+
display: none;
349
+
justify-content: space-around;
350
align-items: center;
351
}
352
353
+
.mobile-bottom-nav-item {
354
display: flex;
355
flex-direction: column;
356
align-items: center;
0
357
gap: 4px;
358
+
padding: 6px 12px;
359
color: var(--text-tertiary);
360
text-decoration: none;
361
font-size: 0.65rem;
362
font-weight: 500;
0
363
transition: color 0.15s;
364
+
min-width: 56px;
365
}
366
367
+
.mobile-bottom-nav-item.active {
368
+
color: var(--accent);
369
}
370
371
+
.mobile-bottom-nav-item:active {
372
+
transform: scale(0.95);
373
+
}
374
+
375
+
.mobile-bottom-nav-new {
376
+
padding: 6px 16px;
377
+
}
378
+
379
+
.mobile-nav-new-btn {
380
+
display: flex;
381
+
align-items: center;
382
+
justify-content: center;
383
+
width: 44px;
384
+
height: 44px;
385
+
background: var(--accent);
386
+
color: var(--bg-primary);
387
+
border-radius: var(--radius-full);
388
+
box-shadow: var(--shadow-md);
389
+
}
390
+
391
+
.mobile-nav-avatar {
392
width: 24px;
393
height: 24px;
394
+
border-radius: var(--radius-full);
395
+
object-fit: cover;
396
}
397
398
+
.ios-shortcut-banner {
399
+
display: none;
400
+
position: relative;
401
+
padding: 20px;
402
+
margin-bottom: 12px;
403
+
text-align: center;
404
+
}
405
+
406
+
.ios-shortcut-banner-close {
407
+
position: absolute;
408
+
top: 8px;
409
+
right: 8px;
410
+
background: none;
411
+
border: none;
412
+
color: var(--text-tertiary);
413
+
cursor: pointer;
414
+
padding: 6px;
415
display: flex;
416
align-items: center;
417
justify-content: center;
418
+
opacity: 0.5;
419
+
transition: opacity 0.15s;
420
}
421
422
+
.ios-shortcut-banner-close:hover {
423
+
opacity: 1;
424
+
}
425
+
426
+
.ios-shortcut-banner-content {
427
+
display: flex;
428
+
flex-direction: column;
429
+
align-items: center;
430
+
gap: 12px;
431
+
}
432
+
433
+
.ios-shortcut-banner-icon {
434
+
display: none;
435
}
436
437
+
.ios-shortcut-banner-text {
438
+
text-align: center;
439
+
}
0
440
441
+
.ios-shortcut-banner-text strong {
442
+
display: none;
0
443
}
444
445
+
.ios-shortcut-banner-text p {
446
+
font-size: 0.8rem;
447
+
color: var(--text-tertiary);
448
+
margin: 0;
449
+
line-height: 1.4;
450
+
}
451
452
+
.ios-shortcut-banner-btn {
453
+
display: inline-flex;
454
+
align-items: center;
455
+
gap: 6px;
456
+
padding: 10px 20px;
457
+
background: transparent;
458
+
color: var(--text-secondary);
459
+
font-size: 0.85rem;
460
+
font-weight: 500;
461
+
border: 1px solid var(--border);
462
+
border-radius: 100px;
463
+
text-decoration: none;
464
+
transition: all 0.15s;
465
+
}
466
467
+
.ios-shortcut-banner-btn:hover {
468
+
background: var(--bg-hover);
469
+
color: var(--text-primary);
470
+
}
0
0
0
471
472
+
@media (max-width: 768px) {
473
+
.ios-shortcut-banner {
474
display: block;
0
475
}
476
+
}
477
478
+
@media (max-width: 768px) {
479
+
.top-nav {
480
+
display: none;
0
0
0
0
481
}
482
483
+
.mobile-bottom-nav {
484
+
display: flex;
0
0
0
0
0
0
485
}
486
487
+
.main-content {
488
+
padding: 16px 12px 100px;
0
489
}
490
491
+
.feed-container {
492
+
border-radius: var(--radius-md);
493
+
padding: 4px;
494
}
495
+
}
496
497
+
@media (max-width: 480px) {
498
+
.main-content {
499
+
padding: 16px 12px 100px;
500
}
501
502
+
.page-title {
503
+
font-size: 1.25rem;
0
504
}
505
506
+
.page-description {
507
+
font-size: 0.85rem;
0
508
}
509
}
+170
-174
web/src/css/modals.css
···
1
.modal-overlay {
2
position: fixed;
3
inset: 0;
4
-
background: rgba(0, 0, 0, 0.5);
5
display: flex;
6
align-items: center;
7
justify-content: center;
8
-
padding: 16px;
9
-
z-index: 50;
10
-
animation: fadeIn 0.2s ease-out;
11
}
12
13
.modal-container {
14
background: var(--bg-secondary);
15
border-radius: var(--radius-lg);
16
width: 100%;
17
-
max-width: 28rem;
18
border: 1px solid var(--border);
19
box-shadow: var(--shadow-lg);
20
-
animation: zoomIn 0.2s ease-out;
21
}
22
23
.modal-header {
24
display: flex;
25
align-items: center;
26
justify-content: space-between;
27
-
padding: 16px;
28
border-bottom: 1px solid var(--border);
29
}
30
31
.modal-title {
32
-
font-size: 1.25rem;
33
-
font-weight: 700;
34
color: var(--text-primary);
35
}
36
37
.modal-close-btn {
38
-
padding: 8px;
39
color: var(--text-tertiary);
40
-
border-radius: var(--radius-md);
41
-
transition: color 0.15s;
0
0
0
42
}
43
44
.modal-close-btn:hover {
45
color: var(--text-primary);
46
-
background: var(--bg-hover);
47
}
48
49
.modal-form {
50
-
padding: 16px;
51
display: flex;
52
flex-direction: column;
53
-
gap: 16px;
54
-
}
55
-
56
-
.icon-picker-tabs {
57
-
display: flex;
58
-
gap: 4px;
59
-
margin-bottom: 12px;
60
-
}
61
-
62
-
.icon-picker-tab {
63
-
flex: 1;
64
-
padding: 8px 12px;
65
-
background: var(--bg-primary);
66
-
border: 1px solid var(--border);
67
-
border-radius: var(--radius-md);
68
-
color: var(--text-secondary);
69
-
font-size: 0.85rem;
70
-
font-weight: 500;
71
-
cursor: pointer;
72
-
transition: all 0.15s ease;
73
-
}
74
-
75
-
.icon-picker-tab:hover {
76
-
background: var(--bg-tertiary);
77
}
78
79
-
.icon-picker-tab.active {
80
-
background: var(--accent);
81
-
border-color: var(--accent);
82
-
color: white;
83
-
}
84
-
85
-
.emoji-picker-wrapper {
86
display: flex;
87
flex-direction: column;
88
-
gap: 10px;
89
-
}
90
-
91
-
.emoji-custom-input input {
92
-
width: 100%;
93
-
}
94
-
95
-
.emoji-picker,
96
-
.icon-picker {
97
-
display: flex;
98
-
flex-wrap: wrap;
99
-
gap: 4px;
100
-
max-height: 120px;
101
-
overflow-y: auto;
102
-
padding: 8px;
103
-
background: var(--bg-primary);
104
-
border: 1px solid var(--border);
105
-
border-radius: var(--radius-md);
106
-
}
107
-
108
-
.emoji-option,
109
-
.icon-option {
110
-
width: 36px;
111
-
height: 36px;
112
-
display: flex;
113
-
align-items: center;
114
-
justify-content: center;
115
-
font-size: 1.2rem;
116
-
background: transparent;
117
-
border: 2px solid transparent;
118
-
border-radius: var(--radius-sm);
119
-
cursor: pointer;
120
-
transition: all 0.15s ease;
121
-
color: var(--text-secondary);
122
-
}
123
-
124
-
.emoji-option:hover,
125
-
.icon-option:hover {
126
-
background: var(--bg-tertiary);
127
-
transform: scale(1.1);
128
-
color: var(--text-primary);
129
-
}
130
-
131
-
.emoji-option.selected,
132
-
.icon-option.selected {
133
-
border-color: var(--accent);
134
-
background: var(--accent-subtle);
135
-
color: var(--accent);
136
}
137
138
.modal-actions {
139
display: flex;
140
justify-content: flex-end;
141
-
gap: 12px;
142
-
padding-top: 8px;
143
}
144
145
@keyframes fadeIn {
146
from {
147
opacity: 0;
148
}
149
-
150
to {
151
opacity: 1;
152
}
153
}
154
155
-
@keyframes zoomIn {
156
from {
157
opacity: 0;
158
-
transform: scale(0.95);
159
}
160
-
161
to {
162
opacity: 1;
163
-
transform: scale(1);
164
}
165
}
166
···
170
171
.form-label {
172
display: block;
173
-
font-size: 0.85rem;
174
-
font-weight: 600;
175
color: var(--text-secondary);
176
margin-bottom: 6px;
177
}
···
180
.form-textarea,
181
.form-select {
182
width: 100%;
183
-
padding: 8px 12px;
184
background: var(--bg-primary);
185
border: 1px solid var(--border);
186
border-radius: var(--radius-md);
187
color: var(--text-primary);
0
188
transition: all 0.15s;
189
}
190
···
198
199
.form-textarea {
200
resize: none;
0
201
}
202
203
.input {
204
width: 100%;
205
-
padding: 12px 14px;
206
-
font-size: 0.95rem;
207
color: var(--text-primary);
208
-
background: var(--bg-secondary);
209
border: 1px solid var(--border);
210
border-radius: var(--radius-md);
211
outline: none;
···
214
215
.input:focus {
216
border-color: var(--accent);
217
-
box-shadow: 0 0 0 3px var(--accent-subtle);
218
}
219
220
.input::placeholder {
221
color: var(--text-tertiary);
222
}
223
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
224
.color-input-container {
225
display: flex;
226
align-items: center;
227
-
gap: 12px;
228
background: var(--bg-tertiary);
229
padding: 8px 12px;
230
border-radius: var(--radius-md);
···
234
235
.color-input-wrapper {
236
position: relative;
237
-
width: 32px;
238
-
height: 32px;
239
border-radius: var(--radius-full);
240
overflow: hidden;
241
border: 2px solid var(--border);
···
262
}
263
264
.signup-modal {
265
-
background: var(--bg-card);
266
width: 100%;
267
-
max-width: 480px;
268
-
border-radius: 16px;
269
-
padding: 24px;
270
border: 1px solid var(--border);
271
position: relative;
272
max-height: 85vh;
273
overflow-y: auto;
274
-
overscroll-behavior: contain;
275
-
box-shadow: 0 10px 40px -10px rgba(0, 0, 0, 0.5);
276
}
277
278
.modal-close {
279
position: absolute;
280
-
top: 16px;
281
-
right: 16px;
282
background: none;
283
border: none;
284
color: var(--text-secondary);
285
cursor: pointer;
286
padding: 4px;
287
-
border-radius: 50%;
288
}
289
290
.modal-close:hover {
291
-
background: var(--bg-hover);
292
color: var(--text-primary);
293
}
294
295
.signup-step h2 {
296
-
font-size: 24px;
297
margin-bottom: 8px;
298
-
font-weight: 700;
299
}
300
301
.signup-subtitle {
302
color: var(--text-secondary);
303
-
margin-bottom: 24px;
0
304
}
305
306
.provider-grid {
307
display: grid;
308
grid-template-columns: 1fr;
309
-
gap: 12px;
310
}
311
312
.provider-card {
313
display: flex;
314
align-items: center;
315
-
gap: 16px;
316
-
padding: 16px;
317
border: 1px solid var(--border);
318
-
border-radius: 12px;
319
-
background: var(--bg-element);
320
cursor: pointer;
321
text-align: left;
322
-
transition: all 0.2s ease;
323
}
324
325
.provider-card:hover {
326
border-color: var(--accent);
327
-
background: var(--bg-hover);
328
-
transform: translateY(-1px);
329
}
330
331
.provider-icon {
332
-
width: 48px;
333
-
height: 48px;
334
-
border-radius: 10px;
335
-
background: var(--bg-card);
336
display: flex;
337
align-items: center;
338
justify-content: center;
···
343
344
.provider-icon.wide {
345
width: auto;
346
-
padding: 0 12px;
347
border: none;
348
background: transparent;
349
}
350
351
.provider-icon.wide img {
352
-
max-height: 40px !important;
353
-
height: 40px !important;
354
width: auto !important;
355
}
356
357
.provider-initial {
358
-
font-size: 20px;
359
-
font-weight: 700;
360
}
361
362
.provider-info {
···
365
366
.provider-info h3 {
367
font-weight: 600;
368
-
font-size: 16px;
369
margin-bottom: 2px;
370
}
371
372
.provider-info span {
373
color: var(--text-secondary);
374
-
font-size: 13px;
375
}
376
377
.provider-arrow {
···
381
.signup-form {
382
display: flex;
383
flex-direction: column;
384
-
gap: 16px;
385
}
386
387
.handle-input-group {
388
display: flex;
389
align-items: center;
390
-
gap: 8px;
391
}
392
393
.handle-suffix {
394
color: var(--text-tertiary);
395
-
font-size: 14px;
396
white-space: nowrap;
397
}
398
399
.error-message {
400
-
color: #ff4444;
401
-
background: rgba(255, 68, 68, 0.1);
402
-
padding: 12px;
403
-
border-radius: 8px;
404
-
font-size: 13px;
405
display: flex;
406
align-items: center;
407
-
gap: 8px;
408
}
409
410
.step-header {
411
display: flex;
412
align-items: center;
413
-
gap: 12px;
414
-
margin-bottom: 24px;
415
}
416
417
.step-header h2 {
418
margin: 0;
419
-
font-size: 20px;
420
}
421
422
.btn-back {
···
424
border: none;
425
color: var(--text-secondary);
426
cursor: pointer;
427
-
font-size: 14px;
428
padding: 0;
429
}
430
···
433
}
434
435
.legal-text {
436
-
font-size: 12px;
437
color: var(--text-tertiary);
438
text-align: center;
439
-
margin-top: 8px;
440
-
}
441
-
442
-
.modal-body {
443
-
padding: 16px;
444
-
display: flex;
445
-
flex-direction: column;
446
-
gap: 16px;
447
}
448
449
.links-input-group {
450
display: flex;
451
-
gap: 8px;
452
-
margin-bottom: 8px;
453
}
454
455
.links-input-group input {
···
462
margin: 0;
463
display: flex;
464
flex-direction: column;
465
-
gap: 8px;
466
}
467
468
.link-item {
469
display: flex;
470
align-items: center;
471
-
justify-content: map;
472
-
gap: 8px;
473
padding: 8px 12px;
474
background: var(--bg-tertiary);
475
border: 1px solid var(--border);
476
border-radius: var(--radius-md);
477
-
font-size: 0.9rem;
478
color: var(--text-primary);
479
word-break: break-all;
480
}
···
489
color: var(--text-tertiary);
490
cursor: pointer;
491
padding: 4px;
492
-
border-radius: 4px;
493
display: flex;
494
align-items: center;
495
justify-content: center;
496
-
font-size: 1.1rem;
497
line-height: 1;
498
}
499
500
.btn-icon-sm:hover {
501
background: var(--bg-hover);
502
-
color: #ff4444;
503
}
504
505
.char-count {
506
text-align: right;
507
-
font-size: 0.75rem;
508
color: var(--text-tertiary);
509
margin-top: 4px;
510
}
···
1
.modal-overlay {
2
position: fixed;
3
inset: 0;
4
+
background: rgba(0, 0, 0, 0.6);
5
display: flex;
6
align-items: center;
7
justify-content: center;
8
+
padding: var(--spacing-md);
9
+
z-index: 100;
10
+
animation: fadeIn 0.15s ease-out;
11
}
12
13
.modal-container {
14
background: var(--bg-secondary);
15
border-radius: var(--radius-lg);
16
width: 100%;
17
+
max-width: 420px;
18
border: 1px solid var(--border);
19
box-shadow: var(--shadow-lg);
20
+
animation: modalIn 0.2s ease-out;
21
}
22
23
.modal-header {
24
display: flex;
25
align-items: center;
26
justify-content: space-between;
27
+
padding: var(--spacing-md);
28
border-bottom: 1px solid var(--border);
29
}
30
31
.modal-title {
32
+
font-size: 1rem;
33
+
font-weight: 600;
34
color: var(--text-primary);
35
}
36
37
.modal-close-btn {
38
+
padding: 6px;
39
color: var(--text-tertiary);
40
+
border-radius: var(--radius-sm);
41
+
transition: all 0.15s;
42
+
background: none;
43
+
border: none;
44
+
cursor: pointer;
45
}
46
47
.modal-close-btn:hover {
48
color: var(--text-primary);
49
+
background: var(--bg-tertiary);
50
}
51
52
.modal-form {
53
+
padding: var(--spacing-md);
54
display: flex;
55
flex-direction: column;
56
+
gap: var(--spacing-md);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
57
}
58
59
+
.modal-body {
60
+
padding: var(--spacing-md);
0
0
0
0
0
61
display: flex;
62
flex-direction: column;
63
+
gap: var(--spacing-md);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
64
}
65
66
.modal-actions {
67
display: flex;
68
justify-content: flex-end;
69
+
gap: var(--spacing-sm);
70
+
padding-top: var(--spacing-sm);
71
}
72
73
@keyframes fadeIn {
74
from {
75
opacity: 0;
76
}
0
77
to {
78
opacity: 1;
79
}
80
}
81
82
+
@keyframes modalIn {
83
from {
84
opacity: 0;
85
+
transform: scale(0.96) translateY(-8px);
86
}
0
87
to {
88
opacity: 1;
89
+
transform: scale(1) translateY(0);
90
}
91
}
92
···
96
97
.form-label {
98
display: block;
99
+
font-size: 0.8rem;
100
+
font-weight: 500;
101
color: var(--text-secondary);
102
margin-bottom: 6px;
103
}
···
106
.form-textarea,
107
.form-select {
108
width: 100%;
109
+
padding: 10px 12px;
110
background: var(--bg-primary);
111
border: 1px solid var(--border);
112
border-radius: var(--radius-md);
113
color: var(--text-primary);
114
+
font-size: 0.875rem;
115
transition: all 0.15s;
116
}
117
···
125
126
.form-textarea {
127
resize: none;
128
+
min-height: 80px;
129
}
130
131
.input {
132
width: 100%;
133
+
padding: 10px 12px;
134
+
font-size: 0.875rem;
135
color: var(--text-primary);
136
+
background: var(--bg-primary);
137
border: 1px solid var(--border);
138
border-radius: var(--radius-md);
139
outline: none;
···
142
143
.input:focus {
144
border-color: var(--accent);
145
+
box-shadow: 0 0 0 2px var(--accent-subtle);
146
}
147
148
.input::placeholder {
149
color: var(--text-tertiary);
150
}
151
152
+
.icon-picker-tabs {
153
+
display: flex;
154
+
gap: 4px;
155
+
margin-bottom: var(--spacing-sm);
156
+
}
157
+
158
+
.icon-picker-tab {
159
+
flex: 1;
160
+
padding: 8px 12px;
161
+
background: var(--bg-tertiary);
162
+
border: none;
163
+
border-radius: var(--radius-sm);
164
+
color: var(--text-secondary);
165
+
font-size: 0.8rem;
166
+
font-weight: 500;
167
+
cursor: pointer;
168
+
transition: all 0.15s ease;
169
+
}
170
+
171
+
.icon-picker-tab:hover {
172
+
background: var(--bg-hover);
173
+
}
174
+
175
+
.icon-picker-tab.active {
176
+
background: var(--accent);
177
+
color: white;
178
+
}
179
+
180
+
.emoji-picker-wrapper {
181
+
display: flex;
182
+
flex-direction: column;
183
+
gap: var(--spacing-sm);
184
+
}
185
+
186
+
.emoji-picker,
187
+
.icon-picker {
188
+
display: flex;
189
+
flex-wrap: wrap;
190
+
gap: 4px;
191
+
max-height: 120px;
192
+
overflow-y: auto;
193
+
padding: var(--spacing-sm);
194
+
background: var(--bg-primary);
195
+
border: 1px solid var(--border);
196
+
border-radius: var(--radius-md);
197
+
}
198
+
199
+
.emoji-option,
200
+
.icon-option {
201
+
width: 32px;
202
+
height: 32px;
203
+
display: flex;
204
+
align-items: center;
205
+
justify-content: center;
206
+
font-size: 1rem;
207
+
background: transparent;
208
+
border: 2px solid transparent;
209
+
border-radius: var(--radius-sm);
210
+
cursor: pointer;
211
+
transition: all 0.15s ease;
212
+
color: var(--text-secondary);
213
+
}
214
+
215
+
.emoji-option:hover,
216
+
.icon-option:hover {
217
+
background: var(--bg-tertiary);
218
+
color: var(--text-primary);
219
+
}
220
+
221
+
.emoji-option.selected,
222
+
.icon-option.selected {
223
+
border-color: var(--accent);
224
+
background: var(--accent-subtle);
225
+
color: var(--accent);
226
+
}
227
+
228
.color-input-container {
229
display: flex;
230
align-items: center;
231
+
gap: var(--spacing-sm);
232
background: var(--bg-tertiary);
233
padding: 8px 12px;
234
border-radius: var(--radius-md);
···
238
239
.color-input-wrapper {
240
position: relative;
241
+
width: 28px;
242
+
height: 28px;
243
border-radius: var(--radius-full);
244
overflow: hidden;
245
border: 2px solid var(--border);
···
266
}
267
268
.signup-modal {
269
+
background: var(--bg-secondary);
270
width: 100%;
271
+
max-width: 440px;
272
+
border-radius: var(--radius-lg);
273
+
padding: var(--spacing-lg);
274
border: 1px solid var(--border);
275
position: relative;
276
max-height: 85vh;
277
overflow-y: auto;
278
+
box-shadow: var(--shadow-lg);
0
279
}
280
281
.modal-close {
282
position: absolute;
283
+
top: var(--spacing-md);
284
+
right: var(--spacing-md);
285
background: none;
286
border: none;
287
color: var(--text-secondary);
288
cursor: pointer;
289
padding: 4px;
290
+
border-radius: var(--radius-sm);
291
}
292
293
.modal-close:hover {
294
+
background: var(--bg-tertiary);
295
color: var(--text-primary);
296
}
297
298
.signup-step h2 {
299
+
font-size: 1.25rem;
300
margin-bottom: 8px;
301
+
font-weight: 600;
302
}
303
304
.signup-subtitle {
305
color: var(--text-secondary);
306
+
font-size: 0.875rem;
307
+
margin-bottom: var(--spacing-lg);
308
}
309
310
.provider-grid {
311
display: grid;
312
grid-template-columns: 1fr;
313
+
gap: var(--spacing-sm);
314
}
315
316
.provider-card {
317
display: flex;
318
align-items: center;
319
+
gap: var(--spacing-md);
320
+
padding: var(--spacing-md);
321
border: 1px solid var(--border);
322
+
border-radius: var(--radius-md);
323
+
background: var(--bg-primary);
324
cursor: pointer;
325
text-align: left;
326
+
transition: all 0.15s ease;
327
}
328
329
.provider-card:hover {
330
border-color: var(--accent);
331
+
background: var(--bg-tertiary);
0
332
}
333
334
.provider-icon {
335
+
width: 40px;
336
+
height: 40px;
337
+
border-radius: var(--radius-md);
338
+
background: var(--bg-tertiary);
339
display: flex;
340
align-items: center;
341
justify-content: center;
···
346
347
.provider-icon.wide {
348
width: auto;
349
+
padding: 0 10px;
350
border: none;
351
background: transparent;
352
}
353
354
.provider-icon.wide img {
355
+
max-height: 36px !important;
356
+
height: 36px !important;
357
width: auto !important;
358
}
359
360
.provider-initial {
361
+
font-size: 1rem;
362
+
font-weight: 600;
363
}
364
365
.provider-info {
···
368
369
.provider-info h3 {
370
font-weight: 600;
371
+
font-size: 0.9rem;
372
margin-bottom: 2px;
373
}
374
375
.provider-info span {
376
color: var(--text-secondary);
377
+
font-size: 0.8rem;
378
}
379
380
.provider-arrow {
···
384
.signup-form {
385
display: flex;
386
flex-direction: column;
387
+
gap: var(--spacing-md);
388
}
389
390
.handle-input-group {
391
display: flex;
392
align-items: center;
393
+
gap: var(--spacing-sm);
394
}
395
396
.handle-suffix {
397
color: var(--text-tertiary);
398
+
font-size: 0.85rem;
399
white-space: nowrap;
400
}
401
402
.error-message {
403
+
color: var(--error);
404
+
background: rgba(255, 69, 58, 0.1);
405
+
padding: 10px 12px;
406
+
border-radius: var(--radius-md);
407
+
font-size: 0.8rem;
408
display: flex;
409
align-items: center;
410
+
gap: var(--spacing-sm);
411
}
412
413
.step-header {
414
display: flex;
415
align-items: center;
416
+
gap: var(--spacing-sm);
417
+
margin-bottom: var(--spacing-lg);
418
}
419
420
.step-header h2 {
421
margin: 0;
422
+
font-size: 1.1rem;
423
}
424
425
.btn-back {
···
427
border: none;
428
color: var(--text-secondary);
429
cursor: pointer;
430
+
font-size: 0.85rem;
431
padding: 0;
432
}
433
···
436
}
437
438
.legal-text {
439
+
font-size: 0.75rem;
440
color: var(--text-tertiary);
441
text-align: center;
442
+
margin-top: var(--spacing-sm);
0
0
0
0
0
0
0
443
}
444
445
.links-input-group {
446
display: flex;
447
+
gap: var(--spacing-sm);
448
+
margin-bottom: var(--spacing-sm);
449
}
450
451
.links-input-group input {
···
458
margin: 0;
459
display: flex;
460
flex-direction: column;
461
+
gap: var(--spacing-sm);
462
}
463
464
.link-item {
465
display: flex;
466
align-items: center;
467
+
justify-content: space-between;
468
+
gap: var(--spacing-sm);
469
padding: 8px 12px;
470
background: var(--bg-tertiary);
471
border: 1px solid var(--border);
472
border-radius: var(--radius-md);
473
+
font-size: 0.85rem;
474
color: var(--text-primary);
475
word-break: break-all;
476
}
···
485
color: var(--text-tertiary);
486
cursor: pointer;
487
padding: 4px;
488
+
border-radius: var(--radius-sm);
489
display: flex;
490
align-items: center;
491
justify-content: center;
492
+
font-size: 1rem;
493
line-height: 1;
494
}
495
496
.btn-icon-sm:hover {
497
background: var(--bg-hover);
498
+
color: var(--error);
499
}
500
501
.char-count {
502
text-align: right;
503
+
font-size: 0.7rem;
504
color: var(--text-tertiary);
505
margin-top: 4px;
506
}
+29
-31
web/src/css/skeleton.css
···
2
0% {
3
background-position: -200% 0;
4
}
5
-
6
100% {
7
background-position: 200% 0;
8
}
···
12
background: linear-gradient(
13
90deg,
14
var(--bg-tertiary) 25%,
15
-
var(--bg-secondary) 50%,
16
var(--bg-tertiary) 75%
17
);
18
background-size: 200% 100%;
···
21
}
22
23
.skeleton-card {
24
-
padding: 24px 0;
25
-
border-bottom: 1px solid var(--border);
26
display: flex;
27
flex-direction: column;
28
-
gap: 16px;
29
}
30
31
.skeleton-header {
32
display: flex;
33
align-items: center;
34
-
gap: 12px;
35
}
36
37
.skeleton-avatar {
38
-
width: 36px;
39
-
height: 36px;
40
-
border-radius: 50%;
0
41
}
42
43
.skeleton-meta {
44
display: flex;
45
flex-direction: column;
46
-
gap: 6px;
47
}
48
49
.skeleton-name {
50
-
width: 120px;
51
-
height: 14px;
52
}
53
54
.skeleton-handle {
55
-
width: 80px;
56
-
height: 12px;
57
}
58
59
.skeleton-content {
60
display: flex;
61
flex-direction: column;
62
-
gap: 12px;
63
-
padding-left: 48px;
64
}
65
66
.skeleton-source {
67
-
width: 180px;
68
-
height: 24px;
69
-
border-radius: var(--radius-full);
70
}
71
72
.skeleton-highlight {
73
width: 100%;
74
-
height: 60px;
75
-
border-left: 2px solid var(--border);
76
}
77
78
.skeleton-text-1 {
79
-
width: 90%;
80
-
height: 14px;
81
}
82
83
.skeleton-text-2 {
84
-
width: 60%;
85
-
height: 14px;
86
}
87
88
.skeleton-actions {
89
display: flex;
90
-
gap: 24px;
91
-
padding-left: 48px;
92
-
margin-top: 4px;
93
}
94
95
.skeleton-action {
96
-
width: 24px;
97
-
height: 24px;
98
border-radius: var(--radius-sm);
99
}
100
101
-
@media (max-width: 600px) {
102
.skeleton-content,
103
.skeleton-actions {
104
padding-left: 0;
···
2
0% {
3
background-position: -200% 0;
4
}
0
5
100% {
6
background-position: 200% 0;
7
}
···
11
background: linear-gradient(
12
90deg,
13
var(--bg-tertiary) 25%,
14
+
var(--bg-hover) 50%,
15
var(--bg-tertiary) 75%
16
);
17
background-size: 200% 100%;
···
20
}
21
22
.skeleton-card {
23
+
padding: var(--spacing-md);
0
24
display: flex;
25
flex-direction: column;
26
+
gap: var(--spacing-sm);
27
}
28
29
.skeleton-header {
30
display: flex;
31
align-items: center;
32
+
gap: var(--spacing-sm);
33
}
34
35
.skeleton-avatar {
36
+
width: 32px;
37
+
height: 32px;
38
+
border-radius: var(--radius-full);
39
+
flex-shrink: 0;
40
}
41
42
.skeleton-meta {
43
display: flex;
44
flex-direction: column;
45
+
gap: 4px;
46
}
47
48
.skeleton-name {
49
+
width: 100px;
50
+
height: 12px;
51
}
52
53
.skeleton-handle {
54
+
width: 70px;
55
+
height: 10px;
56
}
57
58
.skeleton-content {
59
display: flex;
60
flex-direction: column;
61
+
gap: var(--spacing-sm);
62
+
padding-left: 40px;
63
}
64
65
.skeleton-source {
66
+
width: 140px;
67
+
height: 10px;
0
68
}
69
70
.skeleton-highlight {
71
width: 100%;
72
+
height: 48px;
73
+
border-radius: var(--radius-sm);
74
}
75
76
.skeleton-text-1 {
77
+
width: 85%;
78
+
height: 12px;
79
}
80
81
.skeleton-text-2 {
82
+
width: 55%;
83
+
height: 12px;
84
}
85
86
.skeleton-actions {
87
display: flex;
88
+
gap: var(--spacing-md);
89
+
padding-left: 40px;
90
+
margin-top: var(--spacing-xs);
91
}
92
93
.skeleton-action {
94
+
width: 20px;
95
+
height: 20px;
96
border-radius: var(--radius-sm);
97
}
98
99
+
@media (max-width: 768px) {
100
.skeleton-content,
101
.skeleton-actions {
102
padding-left: 0;
+9
-7
web/src/css/utilities.css
···
539
540
.share-menu-container {
541
position: relative;
0
542
}
543
544
.share-menu {
···
546
top: 100%;
547
right: 0;
548
margin-top: 8px;
549
-
background: var(--bg-primary);
550
border: 1px solid var(--border);
551
border-radius: var(--radius-lg);
552
-
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
553
-
min-width: 180px;
554
-
padding: 8px 0;
555
-
z-index: 100;
556
animation: fadeInUp 0.15s ease;
557
}
558
···
589
padding: 10px 14px;
590
background: none;
591
border: none;
0
592
width: 100%;
593
text-align: left;
594
-
font-size: 0.9rem;
595
color: var(--text-primary);
596
cursor: pointer;
597
transition: all 0.1s ease;
598
}
599
600
.share-menu-item:hover {
601
-
background: var(--bg-tertiary);
602
}
603
604
.share-menu-icon {
···
539
540
.share-menu-container {
541
position: relative;
542
+
z-index: 10;
543
}
544
545
.share-menu {
···
547
top: 100%;
548
right: 0;
549
margin-top: 8px;
550
+
background: var(--bg-elevated);
551
border: 1px solid var(--border);
552
border-radius: var(--radius-lg);
553
+
box-shadow: var(--shadow-lg);
554
+
min-width: 200px;
555
+
padding: 8px;
556
+
z-index: 1000;
557
animation: fadeInUp 0.15s ease;
558
}
559
···
590
padding: 10px 14px;
591
background: none;
592
border: none;
593
+
border-radius: var(--radius-md);
594
width: 100%;
595
text-align: left;
596
+
font-size: 0.875rem;
597
color: var(--text-primary);
598
cursor: pointer;
599
transition: all 0.1s ease;
600
}
601
602
.share-menu-item:hover {
603
+
background: var(--bg-hover);
604
}
605
606
.share-menu-icon {
+1
-1
web/src/index.css
···
1
@import "./css/layout.css";
2
@import "./css/base.css";
3
@import "./css/buttons.css";
4
-
@import "./css/buttons.css";
5
@import "./css/feed.css";
6
@import "./css/profile.css";
7
@import "./css/login.css";
···
1
@import "./css/layout.css";
2
@import "./css/base.css";
3
@import "./css/buttons.css";
4
+
@import "./css/cards.css";
5
@import "./css/feed.css";
6
@import "./css/profile.css";
7
@import "./css/login.css";
+48
-27
web/src/pages/Bookmarks.jsx
···
10
} from "../api/client";
11
import { BookmarkIcon } from "../components/Icons";
12
import BookmarkCard from "../components/BookmarkCard";
0
13
import AddToCollectionModal from "../components/AddToCollectionModal";
14
15
export default function Bookmarks() {
···
251
)}
252
253
{loadingBookmarks ? (
254
-
<div className="feed">
255
-
{[1, 2, 3].map((i) => (
256
-
<div key={i} className="card">
257
-
<div
258
-
className="skeleton skeleton-text"
259
-
style={{ width: "40%" }}
260
-
></div>
261
-
<div className="skeleton skeleton-text"></div>
262
-
<div
263
-
className="skeleton skeleton-text"
264
-
style={{ width: "60%" }}
265
-
></div>
266
-
</div>
267
-
))}
0
0
268
</div>
269
) : error ? (
270
<div className="empty-state">
···
284
</p>
285
</div>
286
) : (
287
-
<div className="feed">
288
-
{bookmarks.map((bookmark) => (
289
-
<BookmarkCard
290
-
key={bookmark.id}
291
-
bookmark={bookmark}
292
-
onDelete={handleDelete}
293
-
onAddToCollection={() =>
294
-
setCollectionModalState({
295
-
isOpen: true,
296
-
uri: bookmark.uri || bookmark.id,
297
-
})
0
0
0
0
0
298
}
299
-
/>
300
-
))}
0
0
0
0
0
0
0
0
0
0
0
0
0
301
</div>
302
)}
303
{collectionModalState.isOpen && (
···
10
} from "../api/client";
11
import { BookmarkIcon } from "../components/Icons";
12
import BookmarkCard from "../components/BookmarkCard";
13
+
import CollectionItemCard from "../components/CollectionItemCard";
14
import AddToCollectionModal from "../components/AddToCollectionModal";
15
16
export default function Bookmarks() {
···
252
)}
253
254
{loadingBookmarks ? (
255
+
<div className="feed-container">
256
+
<div className="feed">
257
+
{[1, 2, 3].map((i) => (
258
+
<div key={i} className="card">
259
+
<div
260
+
className="skeleton skeleton-text"
261
+
style={{ width: "40%" }}
262
+
></div>
263
+
<div className="skeleton skeleton-text"></div>
264
+
<div
265
+
className="skeleton skeleton-text"
266
+
style={{ width: "60%" }}
267
+
></div>
268
+
</div>
269
+
))}
270
+
</div>
271
</div>
272
) : error ? (
273
<div className="empty-state">
···
287
</p>
288
</div>
289
) : (
290
+
<div className="feed-container">
291
+
<div className="feed">
292
+
{bookmarks.map((bookmark) => {
293
+
if (bookmark.type === "CollectionItem") {
294
+
return (
295
+
<CollectionItemCard
296
+
key={bookmark.id}
297
+
item={bookmark}
298
+
onAddToCollection={(uri) =>
299
+
setCollectionModalState({
300
+
isOpen: true,
301
+
uri: uri,
302
+
})
303
+
}
304
+
/>
305
+
);
306
}
307
+
return (
308
+
<BookmarkCard
309
+
key={bookmark.id}
310
+
bookmark={bookmark}
311
+
onDelete={handleDelete}
312
+
onAddToCollection={() =>
313
+
setCollectionModalState({
314
+
isOpen: true,
315
+
uri: bookmark.uri || bookmark.id,
316
+
})
317
+
}
318
+
/>
319
+
);
320
+
})}
321
+
</div>
322
</div>
323
)}
324
{collectionModalState.isOpen && (
+41
-39
web/src/pages/CollectionDetail.jsx
···
256
</div>
257
</div>
258
259
-
<div className="feed">
260
-
{items.length === 0 ? (
261
-
<div className="empty-state card" style={{ borderStyle: "dashed" }}>
262
-
<div className="empty-state-icon">
263
-
<Plus size={32} />
0
0
0
0
0
0
0
0
264
</div>
265
-
<h3 className="empty-state-title">Collection is empty</h3>
266
-
<p className="empty-state-text">
267
-
{isOwner
268
-
? 'Add items to this collection from your feed or bookmarks using the "Collect" button.'
269
-
: "This collection has no items yet."}
270
-
</p>
271
-
</div>
272
-
) : (
273
-
items.map((item) => (
274
-
<div key={item.uri} className="collection-item-wrapper">
275
-
{isOwner &&
276
-
!collection.uri.includes("network.cosmik.collection") && (
277
-
<button
278
-
onClick={() => handleDeleteItem(item.uri)}
279
-
className="collection-item-remove"
280
-
title="Remove from collection"
281
-
>
282
-
<Trash2 size={14} />
283
-
</button>
284
-
)}
285
286
-
{item.annotation ? (
287
-
<AnnotationCard annotation={item.annotation} />
288
-
) : item.highlight ? (
289
-
<HighlightCard highlight={item.highlight} />
290
-
) : item.bookmark ? (
291
-
<BookmarkCard bookmark={item.bookmark} />
292
-
) : (
293
-
<div className="card" style={{ padding: "16px" }}>
294
-
<p className="text-secondary">Item could not be loaded</p>
295
-
</div>
296
-
)}
297
-
</div>
298
-
))
299
-
)}
0
300
</div>
301
302
{isOwner && (
···
256
</div>
257
</div>
258
259
+
<div className="feed-container">
260
+
<div className="feed">
261
+
{items.length === 0 ? (
262
+
<div className="empty-state card" style={{ borderStyle: "dashed" }}>
263
+
<div className="empty-state-icon">
264
+
<Plus size={32} />
265
+
</div>
266
+
<h3 className="empty-state-title">Collection is empty</h3>
267
+
<p className="empty-state-text">
268
+
{isOwner
269
+
? 'Add items to this collection from your feed or bookmarks using the "Collect" button.'
270
+
: "This collection has no items yet."}
271
+
</p>
272
</div>
273
+
) : (
274
+
items.map((item) => (
275
+
<div key={item.uri} className="collection-item-wrapper">
276
+
{isOwner &&
277
+
!collection.uri.includes("network.cosmik.collection") && (
278
+
<button
279
+
onClick={() => handleDeleteItem(item.uri)}
280
+
className="collection-item-remove"
281
+
title="Remove from collection"
282
+
>
283
+
<Trash2 size={14} />
284
+
</button>
285
+
)}
0
0
0
0
0
0
0
286
287
+
{item.annotation ? (
288
+
<AnnotationCard annotation={item.annotation} />
289
+
) : item.highlight ? (
290
+
<HighlightCard highlight={item.highlight} />
291
+
) : item.bookmark ? (
292
+
<BookmarkCard bookmark={item.bookmark} />
293
+
) : (
294
+
<div className="card" style={{ padding: "16px" }}>
295
+
<p className="text-secondary">Item could not be loaded</p>
296
+
</div>
297
+
)}
298
+
</div>
299
+
))
300
+
)}
301
+
</div>
302
</div>
303
304
{isOwner && (
+6
web/src/pages/Collections.jsx
···
38
setEditingCollection(null);
39
};
40
0
0
0
0
0
41
if (loading) {
42
return (
43
<div className="feed-page">
···
121
setEditingCollection(null);
122
}}
123
onSuccess={handleCreateSuccess}
0
124
collectionToEdit={editingCollection}
125
/>
126
</div>
···
38
setEditingCollection(null);
39
};
40
41
+
const handleDelete = () => {
42
+
fetchCollections();
43
+
setEditingCollection(null);
44
+
};
45
+
46
if (loading) {
47
return (
48
<div className="feed-page">
···
126
setEditingCollection(null);
127
}}
128
onSuccess={handleCreateSuccess}
129
+
onDelete={handleDelete}
130
collectionToEdit={editingCollection}
131
/>
132
</div>
+173
-221
web/src/pages/Feed.jsx
···
1
-
import { useState, useEffect } from "react";
2
import { useSearchParams } from "react-router-dom";
3
import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
4
import BookmarkCard from "../components/BookmarkCard";
5
import CollectionItemCard from "../components/CollectionItemCard";
6
import AnnotationSkeleton from "../components/AnnotationSkeleton";
0
7
import { getAnnotationFeed, deleteHighlight } from "../api/client";
8
import { AlertIcon, InboxIcon } from "../components/Icons";
9
import { useAuth } from "../context/AuthContext";
0
10
11
import AddToCollectionModal from "../components/AddToCollectionModal";
12
···
39
uri: null,
40
});
41
42
-
const [showIosBanner, setShowIosBanner] = useState(false);
43
-
44
-
useEffect(() => {
45
-
const isIOS =
46
-
/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
47
-
const hasDismissed = localStorage.getItem("iosBannerDismissed");
48
-
49
-
if (isIOS && !hasDismissed) {
50
-
setShowIosBanner(true);
51
-
}
52
-
}, []);
53
-
54
-
const dismissIosBanner = () => {
55
-
setShowIosBanner(false);
56
-
localStorage.setItem("iosBannerDismissed", "true");
57
-
};
58
-
59
const { user } = useAuth();
60
61
useEffect(() => {
···
74
}
75
}
76
0
0
0
0
0
0
0
77
const data = await getAnnotationFeed(
78
50,
79
0,
80
tagFilter || "",
81
creatorDid,
82
feedType,
83
-
filter !== "all" ? filter : "",
84
);
85
setAnnotations(data.items || []);
86
} catch (err) {
···
90
}
91
}
92
fetchFeed();
93
-
}, [tagFilter, filter, feedType, user]);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
94
95
const filteredAnnotations =
96
feedType === "all" ||
···
99
feedType === "margin" ||
100
feedType === "my-feed"
101
? filter === "all"
102
-
? annotations
103
-
: annotations.filter((a) => {
0
0
0
0
0
104
if (filter === "commenting")
105
return a.motivation === "commenting" || a.type === "Annotation";
106
if (filter === "highlighting")
···
109
return a.motivation === "bookmarking" || a.type === "Bookmark";
110
return a.motivation === filter;
111
})
112
-
: annotations;
113
114
return (
115
<div className="feed-page">
116
<div className="page-header">
117
<h1 className="page-title">Feed</h1>
118
<p className="page-description">
119
-
See what people are annotating, highlighting, and bookmarking
120
</p>
121
-
{tagFilter && (
122
-
<div
123
-
style={{
124
-
marginTop: "16px",
125
-
display: "flex",
126
-
alignItems: "center",
127
-
gap: "8px",
128
-
}}
0
0
0
0
0
0
0
0
0
129
>
130
-
<span
131
-
style={{ fontSize: "0.9rem", color: "var(--text-secondary)" }}
132
-
>
133
-
Filtering by tag: <strong>#{tagFilter}</strong>
134
-
</span>
0
0
0
0
0
0
0
0
0
135
<button
136
-
onClick={() =>
137
-
setSearchParams((prev) => {
138
-
const next = new URLSearchParams(prev);
139
-
next.delete("tag");
140
-
return next;
141
-
})
142
-
}
143
-
className="btn btn-sm"
144
-
style={{ padding: "2px 8px", fontSize: "0.8rem" }}
145
>
146
-
Clear
147
</button>
148
-
</div>
149
-
)}
150
-
</div>
151
152
-
{showIosBanner && (
153
-
<div
154
-
className="ios-banner"
155
-
style={{
156
-
background: "var(--bg-secondary)",
157
-
border: "1px solid var(--border)",
158
-
borderRadius: "var(--radius-md)",
159
-
padding: "12px",
160
-
marginBottom: "20px",
161
-
display: "flex",
162
-
alignItems: "center",
163
-
justifyContent: "space-between",
164
-
gap: "12px",
165
-
}}
166
-
>
167
-
<div style={{ flex: 1 }}>
168
-
<h3
169
-
style={{
170
-
fontSize: "0.9rem",
171
-
fontWeight: 600,
172
-
marginBottom: "4px",
173
-
}}
174
-
>
175
-
Get the iOS Shortcut
176
-
</h3>
177
-
<p style={{ fontSize: "0.8rem", color: "var(--text-secondary)" }}>
178
-
Easily save links from Safari using our new shortcut.
179
-
</p>
180
-
</div>
181
-
<div style={{ display: "flex", gap: "8px", alignItems: "center" }}>
182
-
<a
183
-
href="https://www.icloud.com/shortcuts/21c87edf29b046db892c9e57dac6d1fd"
184
-
target="_blank"
185
-
rel="noopener noreferrer"
186
-
className="btn btn-primary btn-sm"
187
-
style={{ whiteSpace: "nowrap" }}
188
-
>
189
-
Get It
190
-
</a>
191
<button
192
-
className="btn btn-sm"
193
-
onClick={dismissIosBanner}
194
-
style={{
195
-
color: "var(--text-tertiary)",
196
-
padding: "4px",
197
-
height: "auto",
198
-
}}
199
>
200
-
✕
201
</button>
202
-
</div>
203
</div>
204
-
)}
205
-
206
-
{}
207
-
<div
208
-
className="feed-filters"
209
-
style={{
210
-
marginBottom: "12px",
211
-
borderBottom: "1px solid var(--border)",
212
-
}}
213
-
>
214
-
<button
215
-
className={`filter-tab ${feedType === "all" ? "active" : ""}`}
216
-
onClick={() => setFeedType("all")}
217
-
>
218
-
All
219
-
</button>
220
-
<button
221
-
className={`filter-tab ${feedType === "popular" ? "active" : ""}`}
222
-
onClick={() => setFeedType("popular")}
223
-
>
224
-
Popular
225
-
</button>
226
-
<button
227
-
className={`filter-tab ${feedType === "margin" ? "active" : ""}`}
228
-
onClick={() => setFeedType("margin")}
229
-
>
230
-
Margin
231
-
</button>
232
-
<button
233
-
className={`filter-tab ${feedType === "semble" ? "active" : ""}`}
234
-
onClick={() => setFeedType("semble")}
235
-
>
236
-
Semble
237
-
</button>
238
-
{user && (
239
-
<button
240
-
className={`filter-tab ${feedType === "my-feed" ? "active" : ""}`}
241
-
onClick={() => setFeedType("my-feed")}
242
-
>
243
-
My Feed
244
-
</button>
245
-
)}
246
</div>
247
248
-
<div className="feed-filters">
249
-
<button
250
-
className={`filter-pill ${filter === "all" ? "active" : ""}`}
251
-
onClick={() => setFilter("all")}
252
-
>
253
-
All Types
254
-
</button>
255
-
<button
256
-
className={`filter-pill ${filter === "commenting" ? "active" : ""}`}
257
-
onClick={() => setFilter("commenting")}
258
-
>
259
-
Annotations
260
-
</button>
261
-
<button
262
-
className={`filter-pill ${filter === "highlighting" ? "active" : ""}`}
263
-
onClick={() => setFilter("highlighting")}
264
-
>
265
-
Highlights
266
-
</button>
267
-
<button
268
-
className={`filter-pill ${filter === "bookmarking" ? "active" : ""}`}
269
-
onClick={() => setFilter("bookmarking")}
270
-
>
271
-
Bookmarks
272
-
</button>
273
-
</div>
274
275
{loading ? (
276
-
<div className="feed">
277
-
{[1, 2, 3, 4, 5].map((i) => (
278
-
<AnnotationSkeleton key={i} />
279
-
))}
0
0
280
</div>
281
) : (
282
<>
283
{error && (
284
<div className="empty-state">
285
<div className="empty-state-icon">
286
-
<AlertIcon size={32} />
287
</div>
288
<h3 className="empty-state-title">Something went wrong</h3>
289
<p className="empty-state-text">{error}</p>
···
293
{!error && filteredAnnotations.length === 0 && (
294
<div className="empty-state">
295
<div className="empty-state-icon">
296
-
<InboxIcon size={32} />
297
</div>
298
<h3 className="empty-state-title">No items yet</h3>
299
<p className="empty-state-text">
···
305
)}
306
307
{!error && filteredAnnotations.length > 0 && (
308
-
<div className="feed">
309
-
{filteredAnnotations.map((item) => {
310
-
if (item.type === "CollectionItem") {
311
-
return <CollectionItemCard key={item.id} item={item} />;
312
-
}
313
-
if (
314
-
item.type === "Highlight" ||
315
-
item.motivation === "highlighting"
316
-
) {
317
-
return (
318
-
<HighlightCard
319
-
key={item.id}
320
-
highlight={item}
321
-
onDelete={async (uri) => {
322
-
const rkey = uri.split("/").pop();
323
-
await deleteHighlight(rkey);
324
-
setAnnotations((prev) =>
325
-
prev.filter((a) => a.id !== item.id),
326
-
);
327
-
}}
328
-
onAddToCollection={() =>
329
-
setCollectionModalState({
330
-
isOpen: true,
331
-
uri: item.uri || item.id,
332
-
})
333
-
}
334
-
/>
335
-
);
336
-
}
337
-
if (
338
-
item.type === "Bookmark" ||
339
-
item.motivation === "bookmarking"
340
-
) {
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
341
return (
342
-
<BookmarkCard
343
key={item.id}
344
-
bookmark={item}
345
onAddToCollection={() =>
346
setCollectionModalState({
347
isOpen: true,
···
350
}
351
/>
352
);
353
-
}
354
-
return (
355
-
<AnnotationCard
356
-
key={item.id}
357
-
annotation={item}
358
-
onAddToCollection={() =>
359
-
setCollectionModalState({
360
-
isOpen: true,
361
-
uri: item.uri || item.id,
362
-
})
363
-
}
364
-
/>
365
-
);
366
-
})}
367
</div>
368
)}
369
</>
···
1
+
import { useState, useEffect, useMemo } from "react";
2
import { useSearchParams } from "react-router-dom";
3
import AnnotationCard, { HighlightCard } from "../components/AnnotationCard";
4
import BookmarkCard from "../components/BookmarkCard";
5
import CollectionItemCard from "../components/CollectionItemCard";
6
import AnnotationSkeleton from "../components/AnnotationSkeleton";
7
+
import IOSInstallBanner from "../components/IOSInstallBanner";
8
import { getAnnotationFeed, deleteHighlight } from "../api/client";
9
import { AlertIcon, InboxIcon } from "../components/Icons";
10
import { useAuth } from "../context/AuthContext";
11
+
import { X } from "lucide-react";
12
13
import AddToCollectionModal from "../components/AddToCollectionModal";
14
···
41
uri: null,
42
});
43
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
44
const { user } = useAuth();
45
46
useEffect(() => {
···
59
}
60
}
61
62
+
const motivationMap = {
63
+
commenting: "commenting",
64
+
highlighting: "highlighting",
65
+
bookmarking: "bookmarking",
66
+
};
67
+
const motivation = motivationMap[filter] || "";
68
+
69
const data = await getAnnotationFeed(
70
50,
71
0,
72
tagFilter || "",
73
creatorDid,
74
feedType,
75
+
motivation,
76
);
77
setAnnotations(data.items || []);
78
} catch (err) {
···
82
}
83
}
84
fetchFeed();
85
+
}, [tagFilter, feedType, filter, user]);
86
+
87
+
const deduplicatedAnnotations = useMemo(() => {
88
+
const inCollectionUris = new Set();
89
+
for (const item of annotations) {
90
+
if (item.type === "CollectionItem") {
91
+
const inner = item.annotation || item.highlight || item.bookmark;
92
+
if (inner) {
93
+
if (inner.uri) inCollectionUris.add(inner.uri.trim());
94
+
if (inner.id) inCollectionUris.add(inner.id.trim());
95
+
}
96
+
}
97
+
}
98
+
99
+
const result = [];
100
+
101
+
for (const item of annotations) {
102
+
if (item.type !== "CollectionItem") {
103
+
const itemUri = (item.uri || "").trim();
104
+
const itemId = (item.id || "").trim();
105
+
if (
106
+
(itemUri && inCollectionUris.has(itemUri)) ||
107
+
(itemId && inCollectionUris.has(itemId))
108
+
) {
109
+
continue;
110
+
}
111
+
}
112
+
113
+
result.push(item);
114
+
}
115
+
116
+
return result;
117
+
}, [annotations]);
118
119
const filteredAnnotations =
120
feedType === "all" ||
···
123
feedType === "margin" ||
124
feedType === "my-feed"
125
? filter === "all"
126
+
? deduplicatedAnnotations
127
+
: deduplicatedAnnotations.filter((a) => {
128
+
if (a.type === "CollectionItem") {
129
+
if (filter === "commenting") return !!a.annotation;
130
+
if (filter === "highlighting") return !!a.highlight;
131
+
if (filter === "bookmarking") return !!a.bookmark;
132
+
}
133
if (filter === "commenting")
134
return a.motivation === "commenting" || a.type === "Annotation";
135
if (filter === "highlighting")
···
138
return a.motivation === "bookmarking" || a.type === "Bookmark";
139
return a.motivation === filter;
140
})
141
+
: deduplicatedAnnotations;
142
143
return (
144
<div className="feed-page">
145
<div className="page-header">
146
<h1 className="page-title">Feed</h1>
147
<p className="page-description">
148
+
See what people are annotating and bookmarking
149
</p>
150
+
</div>
151
+
152
+
{tagFilter && (
153
+
<div className="active-filter-banner">
154
+
<span>
155
+
Filtering by <strong>#{tagFilter}</strong>
156
+
</span>
157
+
<button
158
+
onClick={() =>
159
+
setSearchParams((prev) => {
160
+
const next = new URLSearchParams(prev);
161
+
next.delete("tag");
162
+
return next;
163
+
})
164
+
}
165
+
className="active-filter-clear"
166
+
aria-label="Clear filter"
167
>
168
+
<X size={14} />
169
+
</button>
170
+
</div>
171
+
)}
172
+
173
+
<div className="feed-controls">
174
+
<div className="feed-filters">
175
+
{[
176
+
{ key: "all", label: "All" },
177
+
{ key: "popular", label: "Popular" },
178
+
{ key: "margin", label: "Margin" },
179
+
{ key: "semble", label: "Semble" },
180
+
...(user ? [{ key: "my-feed", label: "Mine" }] : []),
181
+
].map(({ key, label }) => (
182
<button
183
+
key={key}
184
+
className={`filter-tab ${feedType === key ? "active" : ""}`}
185
+
onClick={() => setFeedType(key)}
0
0
0
0
0
0
186
>
187
+
{label}
188
</button>
189
+
))}
190
+
</div>
0
191
192
+
<div className="feed-filters">
193
+
{[
194
+
{ key: "all", label: "All" },
195
+
{ key: "commenting", label: "Notes" },
196
+
{ key: "highlighting", label: "Highlights" },
197
+
{ key: "bookmarking", label: "Bookmarks" },
198
+
].map(({ key, label }) => (
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
199
<button
200
+
key={key}
201
+
className={`filter-pill ${filter === key ? "active" : ""}`}
202
+
onClick={() => setFilter(key)}
0
0
0
0
203
>
204
+
{label}
205
</button>
206
+
))}
207
</div>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
208
</div>
209
210
+
<IOSInstallBanner />
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
211
212
{loading ? (
213
+
<div className="feed-container">
214
+
<div className="feed">
215
+
{[1, 2, 3, 4, 5].map((i) => (
216
+
<AnnotationSkeleton key={i} />
217
+
))}
218
+
</div>
219
</div>
220
) : (
221
<>
222
{error && (
223
<div className="empty-state">
224
<div className="empty-state-icon">
225
+
<AlertIcon size={24} />
226
</div>
227
<h3 className="empty-state-title">Something went wrong</h3>
228
<p className="empty-state-text">{error}</p>
···
232
{!error && filteredAnnotations.length === 0 && (
233
<div className="empty-state">
234
<div className="empty-state-icon">
235
+
<InboxIcon size={24} />
236
</div>
237
<h3 className="empty-state-title">No items yet</h3>
238
<p className="empty-state-text">
···
244
)}
245
246
{!error && filteredAnnotations.length > 0 && (
247
+
<div className="feed-container">
248
+
<div className="feed">
249
+
{filteredAnnotations.map((item) => {
250
+
if (item.type === "CollectionItem") {
251
+
return (
252
+
<CollectionItemCard
253
+
key={item.id}
254
+
item={item}
255
+
onAddToCollection={(uri) =>
256
+
setCollectionModalState({
257
+
isOpen: true,
258
+
uri: uri,
259
+
})
260
+
}
261
+
/>
262
+
);
263
+
}
264
+
if (
265
+
item.type === "Highlight" ||
266
+
item.motivation === "highlighting"
267
+
) {
268
+
return (
269
+
<HighlightCard
270
+
key={item.id}
271
+
highlight={item}
272
+
onDelete={async (uri) => {
273
+
const rkey = uri.split("/").pop();
274
+
await deleteHighlight(rkey);
275
+
setAnnotations((prev) =>
276
+
prev.filter((a) => a.id !== item.id),
277
+
);
278
+
}}
279
+
onAddToCollection={() =>
280
+
setCollectionModalState({
281
+
isOpen: true,
282
+
uri: item.uri || item.id,
283
+
})
284
+
}
285
+
/>
286
+
);
287
+
}
288
+
if (
289
+
item.type === "Bookmark" ||
290
+
item.motivation === "bookmarking"
291
+
) {
292
+
return (
293
+
<BookmarkCard
294
+
key={item.id}
295
+
bookmark={item}
296
+
onAddToCollection={() =>
297
+
setCollectionModalState({
298
+
isOpen: true,
299
+
uri: item.uri || item.id,
300
+
})
301
+
}
302
+
/>
303
+
);
304
+
}
305
return (
306
+
<AnnotationCard
307
key={item.id}
308
+
annotation={item}
309
onAddToCollection={() =>
310
setCollectionModalState({
311
isOpen: true,
···
314
}
315
/>
316
);
317
+
})}
318
+
</div>
0
0
0
0
0
0
0
0
0
0
0
0
319
</div>
320
)}
321
</>
+26
-22
web/src/pages/Highlights.jsx
···
82
</div>
83
84
{loadingHighlights ? (
85
-
<div className="feed">
86
-
{[1, 2, 3].map((i) => (
87
-
<div key={i} className="card">
88
-
<div
89
-
className="skeleton skeleton-text"
90
-
style={{ width: "40%" }}
91
-
></div>
92
-
<div className="skeleton skeleton-text"></div>
93
-
<div
94
-
className="skeleton skeleton-text"
95
-
style={{ width: "60%" }}
96
-
></div>
97
-
</div>
98
-
))}
0
0
99
</div>
100
) : error ? (
101
<div className="empty-state">
···
114
</p>
115
</div>
116
) : (
117
-
<div className="feed">
118
-
{highlights.map((highlight) => (
119
-
<HighlightCard
120
-
key={highlight.id}
121
-
highlight={highlight}
122
-
onDelete={handleDelete}
123
-
/>
124
-
))}
0
0
125
</div>
126
)}
127
</div>
···
82
</div>
83
84
{loadingHighlights ? (
85
+
<div className="feed-container">
86
+
<div className="feed">
87
+
{[1, 2, 3].map((i) => (
88
+
<div key={i} className="card">
89
+
<div
90
+
className="skeleton skeleton-text"
91
+
style={{ width: "40%" }}
92
+
></div>
93
+
<div className="skeleton skeleton-text"></div>
94
+
<div
95
+
className="skeleton skeleton-text"
96
+
style={{ width: "60%" }}
97
+
></div>
98
+
</div>
99
+
))}
100
+
</div>
101
</div>
102
) : error ? (
103
<div className="empty-state">
···
116
</p>
117
</div>
118
) : (
119
+
<div className="feed-container">
120
+
<div className="feed">
121
+
{highlights.map((highlight) => (
122
+
<HighlightCard
123
+
key={highlight.id}
124
+
highlight={highlight}
125
+
onDelete={handleDelete}
126
+
/>
127
+
))}
128
+
</div>
129
</div>
130
)}
131
</div>
+37
-29
web/src/pages/Profile.jsx
···
181
if (authLoading) {
182
return (
183
<div className="profile-page">
184
-
<div className="feed">
185
-
{[1, 2, 3].map((i) => (
186
-
<div key={i} className="card">
187
-
<div
188
-
className="skeleton skeleton-text"
189
-
style={{ width: "40%" }}
190
-
/>
191
-
<div className="skeleton skeleton-text" />
192
-
<div
193
-
className="skeleton skeleton-text"
194
-
style={{ width: "60%" }}
195
-
/>
196
-
</div>
197
-
))}
0
0
198
</div>
199
</div>
200
);
···
594
</div>
595
596
{loading && (
597
-
<div className="feed">
598
-
{[1, 2, 3].map((i) => (
599
-
<div key={i} className="card">
600
-
<div
601
-
className="skeleton skeleton-text"
602
-
style={{ width: "40%" }}
603
-
/>
604
-
<div className="skeleton skeleton-text" />
605
-
<div
606
-
className="skeleton skeleton-text"
607
-
style={{ width: "60%" }}
608
-
/>
609
-
</div>
610
-
))}
0
0
611
</div>
612
)}
613
···
619
</div>
620
)}
621
622
-
{!loading && !error && <div className="feed">{renderContent()}</div>}
0
0
0
0
623
</div>
624
);
625
}
···
181
if (authLoading) {
182
return (
183
<div className="profile-page">
184
+
<div className="feed-container">
185
+
<div className="feed">
186
+
{[1, 2, 3].map((i) => (
187
+
<div key={i} className="card">
188
+
<div
189
+
className="skeleton skeleton-text"
190
+
style={{ width: "40%" }}
191
+
/>
192
+
<div className="skeleton skeleton-text" />
193
+
<div
194
+
className="skeleton skeleton-text"
195
+
style={{ width: "60%" }}
196
+
/>
197
+
</div>
198
+
))}
199
+
</div>
200
</div>
201
</div>
202
);
···
596
</div>
597
598
{loading && (
599
+
<div className="feed-container">
600
+
<div className="feed">
601
+
{[1, 2, 3].map((i) => (
602
+
<div key={i} className="card">
603
+
<div
604
+
className="skeleton skeleton-text"
605
+
style={{ width: "40%" }}
606
+
/>
607
+
<div className="skeleton skeleton-text" />
608
+
<div
609
+
className="skeleton skeleton-text"
610
+
style={{ width: "60%" }}
611
+
/>
612
+
</div>
613
+
))}
614
+
</div>
615
</div>
616
)}
617
···
623
</div>
624
)}
625
626
+
{!loading && !error && (
627
+
<div className="feed-container">
628
+
<div className="feed">{renderContent()}</div>
629
+
</div>
630
+
)}
631
</div>
632
);
633
}
+3
-1
web/src/pages/Url.jsx
···
380
</div>
381
)}
382
383
-
<div className="feed">{renderResults()}</div>
0
0
384
</>
385
)}
386
</div>
···
380
</div>
381
)}
382
383
+
<div className="feed-container">
384
+
<div className="feed">{renderResults()}</div>
385
+
</div>
386
</>
387
)}
388
</div>
+19
-15
web/src/pages/UserUrl.jsx
···
163
</div>
164
165
{loading && (
166
-
<div className="feed">
167
-
{[1, 2, 3].map((i) => (
168
-
<div key={i} className="card">
169
-
<div
170
-
className="skeleton skeleton-text"
171
-
style={{ width: "40%" }}
172
-
/>
173
-
<div className="skeleton skeleton-text" />
174
-
<div
175
-
className="skeleton skeleton-text"
176
-
style={{ width: "60%" }}
177
-
/>
178
-
</div>
179
-
))}
0
0
180
</div>
181
)}
182
···
227
</button>
228
</div>
229
</div>
230
-
<div className="feed">{renderResults()}</div>
0
0
231
</>
232
)}
233
</div>
···
163
</div>
164
165
{loading && (
166
+
<div className="feed-container">
167
+
<div className="feed">
168
+
{[1, 2, 3].map((i) => (
169
+
<div key={i} className="card">
170
+
<div
171
+
className="skeleton skeleton-text"
172
+
style={{ width: "40%" }}
173
+
/>
174
+
<div className="skeleton skeleton-text" />
175
+
<div
176
+
className="skeleton skeleton-text"
177
+
style={{ width: "60%" }}
178
+
/>
179
+
</div>
180
+
))}
181
+
</div>
182
</div>
183
)}
184
···
229
</button>
230
</div>
231
</div>
232
+
<div className="feed-container">
233
+
<div className="feed">{renderResults()}</div>
234
+
</div>
235
</>
236
)}
237
</div>