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