tangled
alpha
login
or
join now
margin.at
/
margin
89
fork
atom
Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
89
fork
atom
overview
issues
4
pulls
1
pipelines
Landing Page
scanash.com
1 month ago
4f133c90
1257864d
+1705
-25
8 changed files
expand all
collapse all
unified
split
web
index.html
package-lock.json
package.json
src
App.jsx
components
TopNav.jsx
css
landing.css
index.css
pages
Landing.jsx
+21
-19
web/index.html
···
1
<!doctype html>
2
<html lang="en">
3
-
4
-
<head>
5
-
<meta charset="UTF-8" />
6
-
<link rel="icon" href="/favicon.ico" />
7
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
-
<meta name="description" content="Margin - Write in the margins of the web. Comment on any URL with AT Protocol." />
9
-
<title>Margin - Write in the margins of the web</title>
10
-
<link rel="preconnect" href="https://fonts.googleapis.com" />
11
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
-
<link
13
-
href="https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400..700;1,400..700&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap"
14
-
rel="stylesheet" />
15
-
</head>
16
-
17
-
<body>
18
-
<div id="root"></div>
19
-
<script type="module" src="/src/main.jsx"></script>
20
-
</body>
21
22
-
</html>
0
0
0
0
···
1
<!doctype html>
2
<html lang="en">
3
+
<head>
4
+
<meta charset="UTF-8" />
5
+
<link rel="icon" href="/favicon.ico" />
6
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+
<meta
8
+
name="description"
9
+
content="Margin - Write in the margins of the web. Comment on any URL with AT Protocol."
10
+
/>
11
+
<title>Margin - Write in the margins of the web</title>
12
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
13
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
14
+
<link
15
+
href="https://fonts.googleapis.com/css2?family=Instrument+Sans:ital,wght@0,400..700;1,400..700&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap"
16
+
rel="stylesheet"
17
+
/>
18
+
</head>
0
0
19
20
+
<body>
21
+
<div id="root"></div>
22
+
<script type="module" src="/src/main.jsx"></script>
23
+
</body>
24
+
</html>
+11
web/package-lock.json
···
8
"name": "margin-web",
9
"version": "0.0.1",
10
"dependencies": {
0
11
"lucide-react": "^0.562.0",
12
"react": "^18.3.1",
13
"react-dom": "^18.3.1",
···
1941
},
1942
"funding": {
1943
"url": "https://github.com/sponsors/ljharb"
0
0
0
0
0
0
0
0
0
0
1944
}
1945
},
1946
"node_modules/debug": {
···
8
"name": "margin-web",
9
"version": "0.0.1",
10
"dependencies": {
11
+
"date-fns": "^4.1.0",
12
"lucide-react": "^0.562.0",
13
"react": "^18.3.1",
14
"react-dom": "^18.3.1",
···
1942
},
1943
"funding": {
1944
"url": "https://github.com/sponsors/ljharb"
1945
+
}
1946
+
},
1947
+
"node_modules/date-fns": {
1948
+
"version": "4.1.0",
1949
+
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
1950
+
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
1951
+
"license": "MIT",
1952
+
"funding": {
1953
+
"type": "github",
1954
+
"url": "https://github.com/sponsors/kossnocorp"
1955
}
1956
},
1957
"node_modules/debug": {
+1
web/package.json
···
10
"preview": "vite preview"
11
},
12
"dependencies": {
0
13
"lucide-react": "^0.562.0",
14
"react": "^18.3.1",
15
"react-dom": "^18.3.1",
···
10
"preview": "vite preview"
11
},
12
"dependencies": {
13
+
"date-fns": "^4.1.0",
14
"lucide-react": "^0.562.0",
15
"react": "^18.3.1",
16
"react-dom": "^18.3.1",
+3
-1
web/src/App.jsx
···
17
import CollectionDetail from "./pages/CollectionDetail";
18
import Privacy from "./pages/Privacy";
19
import Terms from "./pages/Terms";
0
20
import ScrollToTop from "./components/ScrollToTop";
21
import { ThemeProvider } from "./context/ThemeContext";
22
···
35
<TopNav />
36
<main className="main-content">
37
<Routes>
38
-
<Route path="/" element={<Feed />} />
39
<Route path="/url" element={<Url />} />
40
<Route path="/new" element={<New />} />
41
<Route path="/bookmarks" element={<Bookmarks />} />
···
80
<ThemeProvider>
81
<AuthProvider>
82
<Routes>
0
83
<Route path="/*" element={<AppContent />} />
84
</Routes>
85
</AuthProvider>
···
17
import CollectionDetail from "./pages/CollectionDetail";
18
import Privacy from "./pages/Privacy";
19
import Terms from "./pages/Terms";
20
+
import Landing from "./pages/Landing";
21
import ScrollToTop from "./components/ScrollToTop";
22
import { ThemeProvider } from "./context/ThemeContext";
23
···
36
<TopNav />
37
<main className="main-content">
38
<Routes>
39
+
<Route path="/home" element={<Feed />} />
40
<Route path="/url" element={<Url />} />
41
<Route path="/new" element={<New />} />
42
<Route path="/bookmarks" element={<Bookmarks />} />
···
81
<ThemeProvider>
82
<AuthProvider>
83
<Routes>
84
+
<Route path="/" element={<Landing />} />
85
<Route path="/*" element={<AppContent />} />
86
</Routes>
87
</AuthProvider>
+5
-5
web/src/components/TopNav.jsx
···
123
return (
124
<header className="top-nav">
125
<div className="top-nav-inner">
126
-
<Link to="/" className="top-nav-logo">
127
<img src={logo} alt="Margin" />
128
<span>Margin</span>
129
</Link>
130
131
<nav className="top-nav-links">
132
<Link
133
-
to="/"
134
-
className={`top-nav-link ${isActive("/") ? "active" : ""}`}
135
>
136
Home
137
</Link>
···
337
{mobileMenuOpen && (
338
<div className="mobile-menu">
339
<Link
340
-
to="/"
341
-
className={`mobile-menu-link ${isActive("/") ? "active" : ""}`}
342
onClick={closeMobileMenu}
343
>
344
<Home size={20} /> Home
···
123
return (
124
<header className="top-nav">
125
<div className="top-nav-inner">
126
+
<Link to="/home" className="top-nav-logo">
127
<img src={logo} alt="Margin" />
128
<span>Margin</span>
129
</Link>
130
131
<nav className="top-nav-links">
132
<Link
133
+
to="/home"
134
+
className={`top-nav-link ${isActive("/home") ? "active" : ""}`}
135
>
136
Home
137
</Link>
···
337
{mobileMenuOpen && (
338
<div className="mobile-menu">
339
<Link
340
+
to="/home"
341
+
className={`mobile-menu-link ${isActive("/home") ? "active" : ""}`}
342
onClick={closeMobileMenu}
343
>
344
<Home size={20} /> Home
+925
web/src/css/landing.css
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
.landing-page {
2
+
min-height: 100vh;
3
+
background: var(--bg-primary);
4
+
}
5
+
6
+
.landing-nav {
7
+
display: flex;
8
+
justify-content: space-between;
9
+
align-items: center;
10
+
padding: 16px 32px;
11
+
max-width: 1200px;
12
+
margin: 0 auto;
13
+
}
14
+
15
+
.landing-logo {
16
+
display: flex;
17
+
align-items: center;
18
+
gap: 10px;
19
+
text-decoration: none;
20
+
color: var(--text-primary);
21
+
font-weight: 600;
22
+
font-size: 1.1rem;
23
+
}
24
+
25
+
.landing-logo img {
26
+
width: 28px;
27
+
height: 28px;
28
+
}
29
+
30
+
.landing-nav-links {
31
+
display: flex;
32
+
align-items: center;
33
+
gap: 24px;
34
+
}
35
+
36
+
.landing-nav-links a:not(.btn) {
37
+
color: var(--text-secondary);
38
+
text-decoration: none;
39
+
font-size: 0.9rem;
40
+
transition: color 0.15s;
41
+
}
42
+
43
+
.landing-nav-links a:not(.btn):hover {
44
+
color: var(--text-primary);
45
+
}
46
+
47
+
.landing-hero {
48
+
padding: 80px 32px 40px;
49
+
max-width: 800px;
50
+
margin: 0 auto;
51
+
text-align: center;
52
+
}
53
+
54
+
.landing-hero-content {
55
+
display: flex;
56
+
flex-direction: column;
57
+
align-items: center;
58
+
gap: 24px;
59
+
}
60
+
61
+
.landing-badge {
62
+
display: inline-flex;
63
+
align-items: center;
64
+
gap: 8px;
65
+
font-size: 0.8rem;
66
+
font-weight: 500;
67
+
color: var(--accent);
68
+
background: var(--accent-subtle);
69
+
padding: 6px 14px;
70
+
border-radius: var(--radius-full);
71
+
}
72
+
73
+
.landing-title {
74
+
font-size: 3.5rem;
75
+
font-weight: 700;
76
+
line-height: 1.1;
77
+
letter-spacing: -0.03em;
78
+
color: var(--text-primary);
79
+
margin: 0;
80
+
}
81
+
82
+
.landing-title-accent {
83
+
color: var(--accent);
84
+
}
85
+
86
+
.landing-subtitle {
87
+
font-size: 1.2rem;
88
+
line-height: 1.7;
89
+
color: var(--text-secondary);
90
+
max-width: 580px;
91
+
margin: 0;
92
+
}
93
+
94
+
.landing-cta {
95
+
display: flex;
96
+
gap: 12px;
97
+
flex-wrap: wrap;
98
+
justify-content: center;
99
+
margin-top: 8px;
100
+
}
101
+
102
+
.btn-lg {
103
+
padding: 10px 20px;
104
+
font-size: 0.95rem;
105
+
}
106
+
107
+
.landing-browsers {
108
+
font-size: 0.85rem;
109
+
color: var(--text-tertiary);
110
+
margin: 0;
111
+
}
112
+
113
+
.landing-browsers a {
114
+
color: var(--text-secondary);
115
+
text-decoration: underline;
116
+
text-underline-offset: 2px;
117
+
}
118
+
119
+
.landing-browsers a:hover {
120
+
color: var(--text-primary);
121
+
}
122
+
123
+
.landing-demo {
124
+
padding: 40px 32px 80px;
125
+
max-width: 1100px;
126
+
margin: 0 auto;
127
+
}
128
+
129
+
.demo-window {
130
+
background: var(--bg-secondary);
131
+
border: 1px solid var(--border);
132
+
border-radius: var(--radius-xl);
133
+
overflow: hidden;
134
+
box-shadow: var(--shadow-lg);
135
+
}
136
+
137
+
.demo-browser-bar {
138
+
display: flex;
139
+
align-items: center;
140
+
gap: 16px;
141
+
padding: 12px 16px;
142
+
background: var(--bg-tertiary);
143
+
border-bottom: 1px solid var(--border);
144
+
}
145
+
146
+
.demo-browser-dots {
147
+
display: flex;
148
+
gap: 6px;
149
+
}
150
+
151
+
.demo-browser-dots span {
152
+
width: 12px;
153
+
height: 12px;
154
+
border-radius: 50%;
155
+
background: var(--border);
156
+
}
157
+
158
+
.demo-browser-url {
159
+
flex: 1;
160
+
background: var(--bg-primary);
161
+
border-radius: var(--radius-md);
162
+
padding: 8px 14px;
163
+
font-size: 0.8rem;
164
+
color: var(--text-tertiary);
165
+
}
166
+
167
+
.demo-content {
168
+
display: grid;
169
+
grid-template-columns: 1fr 340px;
170
+
min-height: 380px;
171
+
}
172
+
173
+
.demo-article {
174
+
padding: 32px;
175
+
border-right: 1px solid var(--border);
176
+
}
177
+
178
+
.demo-text {
179
+
font-size: 1.05rem;
180
+
line-height: 1.9;
181
+
color: var(--text-primary);
182
+
margin: 0 0 20px 0;
183
+
}
184
+
185
+
.demo-text:last-child {
186
+
margin-bottom: 0;
187
+
}
188
+
189
+
.demo-highlight {
190
+
background-color: transparent;
191
+
color: inherit;
192
+
border-bottom: 2px solid var(--accent);
193
+
}
194
+
195
+
.demo-sidebar {
196
+
padding: 0;
197
+
background: var(--bg-primary);
198
+
display: flex;
199
+
flex-direction: column;
200
+
gap: 0;
201
+
overflow-y: auto;
202
+
font-family:
203
+
"IBM Plex Sans",
204
+
-apple-system,
205
+
BlinkMacSystemFont,
206
+
sans-serif;
207
+
}
208
+
209
+
.demo-sidebar-header {
210
+
display: flex;
211
+
align-items: center;
212
+
justify-content: space-between;
213
+
padding: 14px 16px;
214
+
border-bottom: 1px solid var(--border);
215
+
background: var(--bg-primary);
216
+
}
217
+
218
+
.demo-logo-section {
219
+
display: flex;
220
+
align-items: center;
221
+
gap: 10px;
222
+
}
223
+
224
+
.demo-logo-icon {
225
+
color: var(--accent);
226
+
display: flex;
227
+
align-items: center;
228
+
}
229
+
230
+
.demo-logo-text {
231
+
font-weight: 600;
232
+
font-size: 15px;
233
+
color: var(--text-primary);
234
+
letter-spacing: -0.02em;
235
+
}
236
+
237
+
.demo-user-section {
238
+
display: flex;
239
+
align-items: center;
240
+
gap: 8px;
241
+
}
242
+
243
+
.demo-user-handle {
244
+
font-size: 12px;
245
+
color: var(--text-secondary);
246
+
background: var(--bg-tertiary);
247
+
padding: 4px 10px;
248
+
border-radius: 9999px;
249
+
}
250
+
251
+
.demo-user-avatar {
252
+
width: 24px;
253
+
height: 24px;
254
+
border-radius: 50%;
255
+
background: var(--bg-hover);
256
+
color: var(--text-secondary);
257
+
display: flex;
258
+
align-items: center;
259
+
justify-content: center;
260
+
font-size: 12px;
261
+
font-weight: 600;
262
+
}
263
+
264
+
.demo-page-info {
265
+
display: flex;
266
+
align-items: center;
267
+
gap: 8px;
268
+
padding: 10px 16px;
269
+
background: var(--bg-primary);
270
+
border-bottom: 1px solid var(--border);
271
+
font-size: 12px;
272
+
color: var(--text-tertiary);
273
+
}
274
+
275
+
.demo-annotations-list {
276
+
display: flex;
277
+
flex-direction: column;
278
+
gap: 1px;
279
+
background: var(--border);
280
+
}
281
+
282
+
.demo-annotation {
283
+
background: var(--bg-primary);
284
+
border: none;
285
+
border-radius: 0;
286
+
padding: 14px 16px;
287
+
}
288
+
289
+
.demo-annotation-secondary {
290
+
opacity: 1;
291
+
}
292
+
293
+
.demo-annotation-header {
294
+
display: flex;
295
+
align-items: center;
296
+
gap: 10px;
297
+
margin-bottom: 8px;
298
+
}
299
+
300
+
.demo-avatar {
301
+
width: 26px;
302
+
height: 26px;
303
+
border-radius: 50%;
304
+
background: var(--accent);
305
+
color: var(--bg-primary);
306
+
display: flex;
307
+
align-items: center;
308
+
justify-content: center;
309
+
font-size: 10px;
310
+
font-weight: 600;
311
+
}
312
+
313
+
.demo-meta {
314
+
display: flex;
315
+
flex-direction: column;
316
+
gap: 0;
317
+
}
318
+
319
+
.demo-author {
320
+
font-size: 12px;
321
+
font-weight: 600;
322
+
color: var(--text-primary);
323
+
}
324
+
325
+
.demo-time {
326
+
font-size: 11px;
327
+
color: var(--text-tertiary);
328
+
}
329
+
330
+
.demo-quote {
331
+
font-size: 12px;
332
+
font-style: italic;
333
+
color: var(--text-secondary);
334
+
padding: 8px 12px;
335
+
border-left: 2px solid var(--accent);
336
+
margin: 0 0 8px 0;
337
+
background: var(--accent-subtle);
338
+
border-radius: 0 6px 6px 0;
339
+
line-height: 1.5;
340
+
}
341
+
342
+
.demo-comment {
343
+
font-size: 13px;
344
+
line-height: 1.5;
345
+
color: var(--text-primary);
346
+
margin: 0 0 12px 0;
347
+
}
348
+
349
+
.demo-jump-btn {
350
+
background: transparent;
351
+
border: none;
352
+
padding: 0;
353
+
color: var(--accent);
354
+
font-size: 11px;
355
+
font-weight: 500;
356
+
cursor: pointer;
357
+
display: inline-flex;
358
+
align-items: center;
359
+
margin-top: 4px;
360
+
}
361
+
362
+
.demo-jump-btn:hover {
363
+
text-decoration: underline;
364
+
text-underline-offset: 2px;
365
+
}
366
+
367
+
.landing-section {
368
+
padding: 80px 32px;
369
+
max-width: 1000px;
370
+
margin: 0 auto;
371
+
}
372
+
373
+
.landing-section-alt {
374
+
background: var(--bg-secondary);
375
+
max-width: none;
376
+
}
377
+
378
+
.landing-section-alt > * {
379
+
max-width: 1000px;
380
+
margin-left: auto;
381
+
margin-right: auto;
382
+
}
383
+
384
+
.landing-section-title {
385
+
font-size: 2rem;
386
+
font-weight: 700;
387
+
text-align: center;
388
+
margin: 0 0 48px 0;
389
+
color: var(--text-primary);
390
+
}
391
+
392
+
.landing-steps {
393
+
display: flex;
394
+
flex-direction: column;
395
+
gap: 32px;
396
+
}
397
+
398
+
.landing-step {
399
+
display: flex;
400
+
gap: 24px;
401
+
align-items: flex-start;
402
+
}
403
+
404
+
.landing-step-num {
405
+
width: 40px;
406
+
height: 40px;
407
+
border-radius: 50%;
408
+
background: var(--accent);
409
+
color: white;
410
+
display: flex;
411
+
align-items: center;
412
+
justify-content: center;
413
+
font-weight: 700;
414
+
font-size: 1.1rem;
415
+
flex-shrink: 0;
416
+
}
417
+
418
+
.landing-step-content h3 {
419
+
font-size: 1.15rem;
420
+
font-weight: 600;
421
+
margin: 0 0 8px 0;
422
+
color: var(--text-primary);
423
+
}
424
+
425
+
.landing-step-content p {
426
+
font-size: 1rem;
427
+
color: var(--text-secondary);
428
+
margin: 0;
429
+
line-height: 1.6;
430
+
}
431
+
432
+
.landing-features-grid {
433
+
display: grid;
434
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
435
+
gap: 32px;
436
+
}
437
+
438
+
.landing-feature {
439
+
text-align: center;
440
+
padding: 24px 16px;
441
+
}
442
+
443
+
.landing-feature-icon {
444
+
width: 52px;
445
+
height: 52px;
446
+
border-radius: var(--radius-lg);
447
+
background: var(--accent-subtle);
448
+
color: var(--accent);
449
+
display: flex;
450
+
align-items: center;
451
+
justify-content: center;
452
+
margin: 0 auto 16px;
453
+
}
454
+
455
+
.landing-feature h3 {
456
+
font-size: 1.05rem;
457
+
font-weight: 600;
458
+
margin: 0 0 8px 0;
459
+
color: var(--text-primary);
460
+
}
461
+
462
+
.landing-feature p {
463
+
font-size: 0.9rem;
464
+
color: var(--text-secondary);
465
+
margin: 0;
466
+
line-height: 1.6;
467
+
}
468
+
469
+
.landing-protocol {
470
+
background: var(--bg-secondary);
471
+
max-width: none;
472
+
border-top: 1px solid var(--border);
473
+
border-bottom: 1px solid var(--border);
474
+
}
475
+
476
+
.landing-protocol-grid {
477
+
display: grid;
478
+
grid-template-columns: 1fr 1fr;
479
+
gap: 64px;
480
+
align-items: center;
481
+
max-width: 1000px;
482
+
margin: 0 auto;
483
+
}
484
+
485
+
.landing-protocol-main h2 {
486
+
font-size: 1.75rem;
487
+
font-weight: 700;
488
+
margin: 0 0 16px 0;
489
+
color: var(--text-primary);
490
+
}
491
+
492
+
.landing-protocol-main p {
493
+
font-size: 1rem;
494
+
color: var(--text-secondary);
495
+
margin: 0 0 16px 0;
496
+
line-height: 1.7;
497
+
}
498
+
499
+
.landing-protocol-main a {
500
+
color: var(--accent);
501
+
text-decoration: underline;
502
+
text-underline-offset: 2px;
503
+
}
504
+
505
+
.landing-protocol-features {
506
+
display: flex;
507
+
flex-direction: column;
508
+
gap: 20px;
509
+
}
510
+
511
+
.landing-protocol-item {
512
+
display: flex;
513
+
gap: 16px;
514
+
align-items: flex-start;
515
+
color: var(--accent);
516
+
}
517
+
518
+
.landing-protocol-item div {
519
+
display: flex;
520
+
flex-direction: column;
521
+
}
522
+
523
+
.landing-protocol-item strong {
524
+
font-size: 0.95rem;
525
+
font-weight: 600;
526
+
color: var(--text-primary);
527
+
}
528
+
529
+
.landing-protocol-item span {
530
+
font-size: 0.85rem;
531
+
color: var(--text-tertiary);
532
+
}
533
+
534
+
.landing-final-cta {
535
+
text-align: center;
536
+
}
537
+
538
+
.landing-final-cta h2 {
539
+
font-size: 2rem;
540
+
font-weight: 700;
541
+
margin: 0 0 12px 0;
542
+
color: var(--text-primary);
543
+
}
544
+
545
+
.landing-final-cta p {
546
+
font-size: 1.1rem;
547
+
color: var(--text-secondary);
548
+
margin: 0 0 28px 0;
549
+
}
550
+
551
+
.landing-footer {
552
+
border-top: 1px solid var(--border);
553
+
padding: 48px 32px 32px;
554
+
}
555
+
556
+
.landing-footer-grid {
557
+
display: flex;
558
+
justify-content: space-between;
559
+
max-width: 1000px;
560
+
margin: 0 auto 40px;
561
+
}
562
+
563
+
.landing-footer-brand {
564
+
max-width: 280px;
565
+
}
566
+
567
+
.landing-footer-brand p {
568
+
font-size: 0.9rem;
569
+
color: var(--text-tertiary);
570
+
margin: 12px 0 0 0;
571
+
}
572
+
573
+
.landing-footer-links {
574
+
display: flex;
575
+
gap: 64px;
576
+
}
577
+
578
+
.landing-footer-col {
579
+
display: flex;
580
+
flex-direction: column;
581
+
gap: 10px;
582
+
}
583
+
584
+
.landing-footer-col h4 {
585
+
font-size: 0.75rem;
586
+
font-weight: 600;
587
+
text-transform: uppercase;
588
+
letter-spacing: 0.08em;
589
+
color: var(--text-tertiary);
590
+
margin: 0 0 4px 0;
591
+
}
592
+
593
+
.landing-footer-col a {
594
+
font-size: 0.9rem;
595
+
color: var(--text-secondary);
596
+
text-decoration: none;
597
+
}
598
+
599
+
.landing-footer-col a:hover {
600
+
color: var(--text-primary);
601
+
}
602
+
603
+
.landing-footer-bottom {
604
+
text-align: center;
605
+
padding-top: 24px;
606
+
border-top: 1px solid var(--border);
607
+
max-width: 1000px;
608
+
margin: 0 auto;
609
+
}
610
+
611
+
.landing-footer-bottom p {
612
+
font-size: 0.85rem;
613
+
color: var(--text-tertiary);
614
+
margin: 0;
615
+
}
616
+
617
+
@media (max-width: 900px) {
618
+
.demo-content {
619
+
grid-template-columns: 1fr;
620
+
}
621
+
622
+
.demo-article {
623
+
border-right: none;
624
+
border-bottom: 1px solid var(--border);
625
+
}
626
+
627
+
.demo-sidebar {
628
+
max-height: 340px;
629
+
}
630
+
631
+
.landing-protocol-grid {
632
+
grid-template-columns: 1fr;
633
+
gap: 40px;
634
+
}
635
+
}
636
+
637
+
@media (max-width: 768px) {
638
+
.landing-nav {
639
+
padding: 16px 20px;
640
+
}
641
+
642
+
.landing-nav-links a:not(.btn) {
643
+
display: none;
644
+
}
645
+
646
+
.landing-hero {
647
+
padding: 60px 20px 30px;
648
+
}
649
+
650
+
.landing-title {
651
+
font-size: 2.5rem;
652
+
}
653
+
654
+
.landing-subtitle {
655
+
font-size: 1.1rem;
656
+
}
657
+
658
+
.landing-cta {
659
+
flex-direction: column;
660
+
width: 100%;
661
+
}
662
+
663
+
.landing-cta .btn {
664
+
width: 100%;
665
+
justify-content: center;
666
+
}
667
+
668
+
.landing-demo {
669
+
padding: 30px 16px 60px;
670
+
}
671
+
672
+
.demo-browser-bar {
673
+
padding: 10px 12px;
674
+
}
675
+
676
+
.demo-browser-dots {
677
+
display: none;
678
+
}
679
+
680
+
.demo-article {
681
+
padding: 20px;
682
+
}
683
+
684
+
.demo-text {
685
+
font-size: 0.95rem;
686
+
}
687
+
688
+
.demo-sidebar {
689
+
padding: 16px;
690
+
}
691
+
692
+
.landing-section {
693
+
padding: 60px 20px;
694
+
}
695
+
696
+
.landing-section-title {
697
+
font-size: 1.5rem;
698
+
margin-bottom: 32px;
699
+
}
700
+
701
+
.landing-step {
702
+
gap: 16px;
703
+
}
704
+
705
+
.landing-step-num {
706
+
width: 32px;
707
+
height: 32px;
708
+
font-size: 0.95rem;
709
+
}
710
+
711
+
.landing-features-grid {
712
+
grid-template-columns: 1fr;
713
+
gap: 24px;
714
+
}
715
+
716
+
.landing-feature {
717
+
text-align: left;
718
+
display: flex;
719
+
gap: 16px;
720
+
padding: 16px 0;
721
+
}
722
+
723
+
.landing-feature-icon {
724
+
margin: 0;
725
+
width: 44px;
726
+
height: 44px;
727
+
flex-shrink: 0;
728
+
}
729
+
730
+
.landing-protocol-main h2 {
731
+
font-size: 1.5rem;
732
+
}
733
+
734
+
.landing-footer {
735
+
padding: 40px 20px 24px;
736
+
}
737
+
738
+
.landing-footer-grid {
739
+
flex-direction: column;
740
+
gap: 40px;
741
+
}
742
+
743
+
.landing-footer-links {
744
+
flex-wrap: wrap;
745
+
gap: 32px;
746
+
}
747
+
}
748
+
749
+
.demo-hover-indicator {
750
+
position: absolute;
751
+
display: flex;
752
+
align-items: center;
753
+
z-index: 100;
754
+
pointer-events: none;
755
+
background: transparent;
756
+
opacity: 0;
757
+
transform: scale(0.8);
758
+
transition:
759
+
opacity 0.15s ease-out,
760
+
transform 0.15s ease-out;
761
+
}
762
+
763
+
.demo-hover-indicator.visible {
764
+
opacity: 1;
765
+
transform: scale(1);
766
+
}
767
+
768
+
.demo-hover-avatar {
769
+
width: 28px;
770
+
height: 28px;
771
+
border-radius: 50%;
772
+
object-fit: cover;
773
+
border: 2px solid var(--bg-primary);
774
+
margin-left: -10px;
775
+
background: var(--bg-elevated);
776
+
}
777
+
778
+
.demo-hover-avatar:first-child {
779
+
margin-left: 0;
780
+
}
781
+
782
+
.demo-hover-avatar-fallback {
783
+
width: 28px;
784
+
height: 28px;
785
+
border-radius: 50%;
786
+
background: #6366f1;
787
+
color: white;
788
+
display: flex;
789
+
align-items: center;
790
+
justify-content: center;
791
+
font-size: 12px;
792
+
font-weight: 600;
793
+
font-family: -apple-system, sans-serif;
794
+
border: 2px solid var(--bg-primary);
795
+
margin-left: -10px;
796
+
}
797
+
798
+
.demo-hover-avatar-fallback:first-child {
799
+
margin-left: 0;
800
+
}
801
+
802
+
@keyframes demo-popover-in {
803
+
from {
804
+
opacity: 0;
805
+
transform: translateY(-4px);
806
+
}
807
+
808
+
to {
809
+
opacity: 1;
810
+
transform: translateY(0);
811
+
}
812
+
}
813
+
814
+
.demo-popover {
815
+
position: absolute;
816
+
width: 300px;
817
+
background: var(--bg-card);
818
+
border: 1px solid var(--border);
819
+
border-radius: 12px;
820
+
padding: 0;
821
+
box-shadow: var(--shadow-lg);
822
+
display: flex;
823
+
flex-direction: column;
824
+
z-index: 200;
825
+
font-family: inherit;
826
+
color: var(--text-primary);
827
+
opacity: 0;
828
+
animation: demo-popover-in 0.15s forwards;
829
+
max-height: 400px;
830
+
overflow: hidden;
831
+
}
832
+
833
+
.demo-popover-header {
834
+
padding: 10px 14px;
835
+
border-bottom: 1px solid var(--border);
836
+
display: flex;
837
+
justify-content: space-between;
838
+
align-items: center;
839
+
background: var(--bg-primary);
840
+
border-radius: 12px 12px 0 0;
841
+
font-weight: 500;
842
+
font-size: 11px;
843
+
color: var(--text-tertiary);
844
+
text-transform: uppercase;
845
+
letter-spacing: 0.5px;
846
+
}
847
+
848
+
.demo-popover-close {
849
+
background: none;
850
+
border: none;
851
+
color: var(--text-tertiary);
852
+
cursor: pointer;
853
+
padding: 2px;
854
+
font-size: 16px;
855
+
line-height: 1;
856
+
opacity: 0.6;
857
+
transition: opacity 0.15s;
858
+
}
859
+
860
+
.demo-popover-close:hover {
861
+
opacity: 1;
862
+
}
863
+
864
+
.demo-popover-scroll-area {
865
+
overflow-y: auto;
866
+
max-height: 340px;
867
+
}
868
+
869
+
.demo-comment-item {
870
+
padding: 12px 14px;
871
+
border-bottom: 1px solid var(--border);
872
+
}
873
+
874
+
.demo-comment-item:last-child {
875
+
border-bottom: none;
876
+
}
877
+
878
+
.demo-comment-header {
879
+
display: flex;
880
+
align-items: center;
881
+
gap: 8px;
882
+
margin-bottom: 6px;
883
+
}
884
+
885
+
.demo-comment-avatar {
886
+
width: 22px;
887
+
height: 22px;
888
+
border-radius: 50%;
889
+
object-fit: cover;
890
+
background: var(--accent);
891
+
}
892
+
893
+
.demo-comment-handle {
894
+
font-size: 12px;
895
+
font-weight: 600;
896
+
color: var(--text-primary);
897
+
}
898
+
899
+
.demo-comment-text {
900
+
font-size: 13px;
901
+
line-height: 1.5;
902
+
color: var(--text-primary);
903
+
margin-bottom: 8px;
904
+
}
905
+
906
+
.demo-comment-actions {
907
+
display: flex;
908
+
gap: 8px;
909
+
}
910
+
911
+
.demo-comment-action-btn {
912
+
background: none;
913
+
border: none;
914
+
padding: 4px 8px;
915
+
color: var(--text-tertiary);
916
+
font-size: 11px;
917
+
cursor: pointer;
918
+
border-radius: 4px;
919
+
transition: all 0.15s;
920
+
}
921
+
922
+
.demo-comment-action-btn:hover {
923
+
background: var(--bg-hover);
924
+
color: var(--text-secondary);
925
+
}
+1
web/src/index.css
···
11
@import "./css/notifications.css";
12
@import "./css/skeleton.css";
13
@import "./css/utilities.css";
0
···
11
@import "./css/notifications.css";
12
@import "./css/skeleton.css";
13
@import "./css/utilities.css";
14
+
@import "./css/landing.css";
+738
web/src/pages/Landing.jsx
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { useState, useEffect, useRef } from "react";
2
+
import { Link } from "react-router-dom";
3
+
import { useAuth } from "../context/AuthContext";
4
+
import {
5
+
MessageSquare,
6
+
Highlighter,
7
+
Users,
8
+
ArrowRight,
9
+
Github,
10
+
Database,
11
+
Shield,
12
+
Zap,
13
+
} from "lucide-react";
14
+
import { SiFirefox, SiGooglechrome, SiBluesky } from "react-icons/si";
15
+
import { FaEdge } from "react-icons/fa";
16
+
import logo from "../assets/logo.svg";
17
+
18
+
const isFirefox =
19
+
typeof navigator !== "undefined" && /Firefox/i.test(navigator.userAgent);
20
+
const isEdge =
21
+
typeof navigator !== "undefined" && /Edg/i.test(navigator.userAgent);
22
+
23
+
function getExtensionInfo() {
24
+
if (isFirefox) {
25
+
return {
26
+
url: "https://addons.mozilla.org/en-US/firefox/addon/margin/",
27
+
Icon: SiFirefox,
28
+
label: "Firefox",
29
+
};
30
+
}
31
+
if (isEdge) {
32
+
return {
33
+
url: "https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn",
34
+
Icon: FaEdge,
35
+
label: "Edge",
36
+
};
37
+
}
38
+
return {
39
+
url: "https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/",
40
+
Icon: SiGooglechrome,
41
+
label: "Chrome",
42
+
};
43
+
}
44
+
45
+
import { getAnnotations, normalizeAnnotation } from "../api/client";
46
+
import { formatDistanceToNow } from "date-fns";
47
+
48
+
function DemoAnnotation() {
49
+
const [annotations, setAnnotations] = useState([]);
50
+
const [loading, setLoading] = useState(true);
51
+
const [hoverPos, setHoverPos] = useState(null);
52
+
const [hoverVisible, setHoverVisible] = useState(false);
53
+
const [hoverAuthors, setHoverAuthors] = useState([]);
54
+
55
+
const [showPopover, setShowPopover] = useState(false);
56
+
const [popoverPos, setPopoverPos] = useState(null);
57
+
const [popoverAnnotations, setPopoverAnnotations] = useState([]);
58
+
59
+
const highlightRef = useRef(null);
60
+
const articleRef = useRef(null);
61
+
62
+
useEffect(() => {
63
+
getAnnotations({ source: "https://en.wikipedia.org/wiki/AT_Protocol" })
64
+
.then((res) => {
65
+
const rawItems = res.items || (Array.isArray(res) ? res : []);
66
+
const normalized = rawItems.map(normalizeAnnotation);
67
+
setAnnotations(normalized);
68
+
})
69
+
.catch((err) => {
70
+
console.error("Failed to fetch demo annotations:", err);
71
+
})
72
+
.finally(() => {
73
+
setLoading(false);
74
+
});
75
+
}, []);
76
+
77
+
useEffect(() => {
78
+
if (!showPopover) return;
79
+
const handleClickOutside = () => setShowPopover(false);
80
+
document.addEventListener("click", handleClickOutside);
81
+
return () => document.removeEventListener("click", handleClickOutside);
82
+
}, [showPopover]);
83
+
84
+
const getMatches = () => {
85
+
return annotations.filter(
86
+
(a) =>
87
+
(a.selector?.exact &&
88
+
a.selector.exact.includes("A handle serves as")) ||
89
+
(a.quote && a.quote.includes("A handle serves as")),
90
+
);
91
+
};
92
+
93
+
const handleMouseEnter = () => {
94
+
const matches = getMatches();
95
+
const authorsMap = new Map();
96
+
matches.forEach((a) => {
97
+
const author = a.author || a.creator || { handle: "unknown" };
98
+
const id = author.did || author.handle;
99
+
if (!authorsMap.has(id)) authorsMap.set(id, author);
100
+
});
101
+
const unique = Array.from(authorsMap.values());
102
+
103
+
setHoverAuthors(unique);
104
+
105
+
if (highlightRef.current && articleRef.current) {
106
+
const spanRect = highlightRef.current.getBoundingClientRect();
107
+
const articleRect = articleRef.current.getBoundingClientRect();
108
+
109
+
const visibleCount = Math.min(unique.length, 3);
110
+
const hasOverflow = unique.length > 3;
111
+
const countForCalc = visibleCount + (hasOverflow ? 1 : 0);
112
+
const width = countForCalc > 0 ? countForCalc * 18 + 10 : 0;
113
+
114
+
const top = spanRect.top - articleRect.top + spanRect.height / 2 - 14;
115
+
const left = spanRect.left - articleRect.left - width;
116
+
117
+
setHoverPos({ top, left });
118
+
setHoverVisible(true);
119
+
}
120
+
};
121
+
122
+
const handleMouseLeave = () => {
123
+
setHoverVisible(false);
124
+
};
125
+
126
+
const handleHighlightClick = (e) => {
127
+
e.stopPropagation();
128
+
const matches = getMatches();
129
+
setPopoverAnnotations(matches);
130
+
131
+
if (highlightRef.current && articleRef.current) {
132
+
const spanRect = highlightRef.current.getBoundingClientRect();
133
+
const articleRect = articleRef.current.getBoundingClientRect();
134
+
135
+
const top = spanRect.top - articleRect.top + spanRect.height + 10;
136
+
let left = spanRect.left - articleRect.left;
137
+
138
+
if (left + 300 > articleRect.width) {
139
+
left = articleRect.width - 300;
140
+
}
141
+
142
+
setPopoverPos({ top, left });
143
+
setShowPopover(true);
144
+
}
145
+
};
146
+
147
+
const maxShow = 3;
148
+
const displayHoverAuthors = hoverAuthors.slice(0, maxShow);
149
+
const hoverOverflow = hoverAuthors.length - maxShow;
150
+
151
+
return (
152
+
<div className="demo-window">
153
+
<div className="demo-browser-bar">
154
+
<div className="demo-browser-dots">
155
+
<span></span>
156
+
<span></span>
157
+
<span></span>
158
+
</div>
159
+
<div className="demo-browser-url">
160
+
<span>en.wikipedia.org/wiki/AT_Protocol</span>
161
+
</div>
162
+
</div>
163
+
<div className="demo-content">
164
+
<div
165
+
className="demo-article"
166
+
ref={articleRef}
167
+
style={{ position: "relative" }}
168
+
>
169
+
{hoverPos && hoverAuthors.length > 0 && (
170
+
<div
171
+
className={`demo-hover-indicator ${hoverVisible ? "visible" : ""}`}
172
+
style={{
173
+
top: hoverPos.top,
174
+
left: hoverPos.left,
175
+
cursor: "pointer",
176
+
}}
177
+
onClick={handleHighlightClick}
178
+
>
179
+
{displayHoverAuthors.map((author, i) =>
180
+
author.avatar ? (
181
+
<img
182
+
key={i}
183
+
src={author.avatar}
184
+
className="demo-hover-avatar"
185
+
alt={author.handle}
186
+
onError={(e) => {
187
+
e.target.style.display = "none";
188
+
e.target.nextSibling.style.display = "flex";
189
+
}}
190
+
/>
191
+
) : (
192
+
<div key={i} className="demo-hover-avatar-fallback">
193
+
{author.handle?.[0]?.toUpperCase() || "U"}
194
+
</div>
195
+
),
196
+
)}
197
+
{hoverOverflow > 0 && (
198
+
<div
199
+
className="demo-hover-avatar-fallback"
200
+
style={{
201
+
background: "var(--bg-elevated)",
202
+
color: "var(--text-secondary)",
203
+
fontSize: 10,
204
+
}}
205
+
>
206
+
+{hoverOverflow}
207
+
</div>
208
+
)}
209
+
</div>
210
+
)}
211
+
212
+
{showPopover && popoverPos && (
213
+
<div
214
+
className="demo-popover"
215
+
style={{
216
+
top: popoverPos.top,
217
+
left: popoverPos.left,
218
+
}}
219
+
onClick={(e) => e.stopPropagation()}
220
+
>
221
+
<div className="demo-popover-header">
222
+
<span>
223
+
{popoverAnnotations.length}{" "}
224
+
{popoverAnnotations.length === 1 ? "Comment" : "Comments"}
225
+
</span>
226
+
<button
227
+
className="demo-popover-close"
228
+
onClick={() => setShowPopover(false)}
229
+
>
230
+
✕
231
+
</button>
232
+
</div>
233
+
<div className="demo-popover-scroll-area">
234
+
{popoverAnnotations.length === 0 ? (
235
+
<div style={{ padding: 14, fontSize: 13, color: "#666" }}>
236
+
No comments
237
+
</div>
238
+
) : (
239
+
popoverAnnotations.map((ann, i) => (
240
+
<div key={ann.uri || i} className="demo-comment-item">
241
+
<div className="demo-comment-header">
242
+
<img
243
+
src={ann.author?.avatar || logo}
244
+
className="demo-comment-avatar"
245
+
onError={(e) => (e.target.src = logo)}
246
+
alt=""
247
+
/>
248
+
<span className="demo-comment-handle">
249
+
@{ann.author?.handle || "user"}
250
+
</span>
251
+
</div>
252
+
<div className="demo-comment-text">
253
+
{ann.text || ann.body?.value}
254
+
</div>
255
+
<div className="demo-comment-actions">
256
+
<button className="demo-comment-action-btn">
257
+
Reply
258
+
</button>
259
+
<button className="demo-comment-action-btn">
260
+
Share
261
+
</button>
262
+
</div>
263
+
</div>
264
+
))
265
+
)}
266
+
</div>
267
+
</div>
268
+
)}
269
+
<p className="demo-text">
270
+
The AT Protocol utilizes a dual identifier system: a mutable handle,
271
+
in the form of a domain name, and an immutable decentralized
272
+
identifier (DID).
273
+
</p>
274
+
<p className="demo-text">
275
+
<span
276
+
className="demo-highlight"
277
+
ref={highlightRef}
278
+
onMouseEnter={handleMouseEnter}
279
+
onMouseLeave={handleMouseLeave}
280
+
onClick={handleHighlightClick}
281
+
style={{ cursor: "pointer" }}
282
+
>
283
+
A handle serves as a verifiable user identifier.
284
+
</span>{" "}
285
+
Verification is by either of two equivalent methods proving control
286
+
of the domain name: Either a DNS query of a resource record with the
287
+
same name as the handle, or a request for a text file from a Web
288
+
service with the same name.
289
+
</p>
290
+
<p className="demo-text">
291
+
DIDs resolve to DID documents, which contain references to key user
292
+
metadata, such as the user's handle, public keys, and data
293
+
repository. While any DID method could, in theory, be used by the
294
+
protocol if its components provide support for the method, in
295
+
practice only two methods are supported ('blessed') by the
296
+
protocol's reference implementations: did:plc and did:web. The
297
+
validity of these identifiers can be verified by a registry which
298
+
hosts the DID's associated document and a file that is hosted
299
+
at a well-known location on the connected domain name, respectively.
300
+
</p>
301
+
</div>
302
+
<div className="demo-sidebar">
303
+
<div className="demo-sidebar-header">
304
+
<div className="demo-logo-section">
305
+
<span className="demo-logo-icon">
306
+
<img src={logo} alt="" style={{ width: 16, height: 16 }} />
307
+
</span>
308
+
<span className="demo-logo-text">Margin</span>
309
+
</div>
310
+
<div className="demo-user-section">
311
+
<span className="demo-user-handle">@margin.at</span>
312
+
</div>
313
+
</div>
314
+
<div className="demo-page-info">
315
+
<span>en.wikipedia.org</span>
316
+
</div>
317
+
<div className="demo-annotations-list">
318
+
{loading ? (
319
+
<div style={{ padding: 20, textAlign: "center", color: "#666" }}>
320
+
Loading...
321
+
</div>
322
+
) : annotations.length > 0 ? (
323
+
annotations.map((ann, i) => (
324
+
<div
325
+
key={ann.uri || i}
326
+
className={`demo-annotation ${i > 0 ? "demo-annotation-secondary" : ""}`}
327
+
>
328
+
<div className="demo-annotation-header">
329
+
<div
330
+
className="demo-avatar"
331
+
style={{ background: "transparent" }}
332
+
>
333
+
<img
334
+
src={ann.author?.avatar || logo}
335
+
alt={ann.author?.handle || "User"}
336
+
style={{
337
+
width: "100%",
338
+
height: "100%",
339
+
borderRadius: "50%",
340
+
}}
341
+
onError={(e) => {
342
+
e.target.src = logo;
343
+
}}
344
+
/>
345
+
</div>
346
+
<div className="demo-meta">
347
+
<span className="demo-author">
348
+
@{ann.author?.handle || "margin.at"}
349
+
</span>
350
+
<span className="demo-time">
351
+
{ann.createdAt
352
+
? formatDistanceToNow(new Date(ann.createdAt), {
353
+
addSuffix: true,
354
+
})
355
+
: "recently"}
356
+
</span>
357
+
</div>
358
+
</div>
359
+
{ann.selector?.exact && (
360
+
<p className="demo-quote">
361
+
“{ann.selector.exact}”
362
+
</p>
363
+
)}
364
+
<p className="demo-comment">{ann.text || ann.body?.value}</p>
365
+
<button className="demo-jump-btn">Jump to text →</button>
366
+
</div>
367
+
))
368
+
) : (
369
+
<div
370
+
style={{
371
+
padding: 20,
372
+
textAlign: "center",
373
+
color: "var(--text-tertiary)",
374
+
}}
375
+
>
376
+
No annotations found.
377
+
</div>
378
+
)}
379
+
</div>
380
+
</div>
381
+
</div>
382
+
</div>
383
+
);
384
+
}
385
+
386
+
export default function Landing() {
387
+
const { user } = useAuth();
388
+
const ext = getExtensionInfo();
389
+
390
+
return (
391
+
<div className="landing-page">
392
+
<nav className="landing-nav">
393
+
<Link to="/" className="landing-logo">
394
+
<img src={logo} alt="Margin" />
395
+
<span>Margin</span>
396
+
</Link>
397
+
<div className="landing-nav-links">
398
+
<a
399
+
href="https://github.com/margin-at/margin"
400
+
target="_blank"
401
+
rel="noreferrer"
402
+
>
403
+
GitHub
404
+
</a>
405
+
<a
406
+
href="https://tangled.org/margin.at/margin"
407
+
target="_blank"
408
+
rel="noreferrer"
409
+
>
410
+
Tangled
411
+
</a>
412
+
<a
413
+
href="https://bsky.app/profile/margin.at"
414
+
target="_blank"
415
+
rel="noreferrer"
416
+
>
417
+
Bluesky
418
+
</a>
419
+
{user ? (
420
+
<Link to="/home" className="btn btn-primary">
421
+
Open App
422
+
</Link>
423
+
) : (
424
+
<Link to="/login" className="btn btn-primary">
425
+
Sign In
426
+
</Link>
427
+
)}
428
+
</div>
429
+
</nav>
430
+
431
+
<section className="landing-hero">
432
+
<div className="landing-hero-content">
433
+
<div className="landing-badge">
434
+
<SiBluesky size={14} />
435
+
Built on ATProto
436
+
</div>
437
+
<h1 className="landing-title">
438
+
Write in the margins
439
+
<br />
440
+
<span className="landing-title-accent">of the web.</span>
441
+
</h1>
442
+
<p className="landing-subtitle">
443
+
Margin is a social layer for reading online. Highlight passages,
444
+
leave thoughts in the margins, and see what others are thinking
445
+
about the pages you read.
446
+
</p>
447
+
<div className="landing-cta">
448
+
<a
449
+
href={ext.url}
450
+
target="_blank"
451
+
rel="noreferrer"
452
+
className="btn btn-primary btn-lg"
453
+
>
454
+
<ext.Icon size={18} />
455
+
Install for {ext.label}
456
+
</a>
457
+
{user ? (
458
+
<Link to="/home" className="btn btn-secondary btn-lg">
459
+
Open App
460
+
<ArrowRight size={18} />
461
+
</Link>
462
+
) : (
463
+
<Link to="/login" className="btn btn-secondary btn-lg">
464
+
Sign In with ATProto
465
+
<ArrowRight size={18} />
466
+
</Link>
467
+
)}
468
+
</div>
469
+
<p className="landing-browsers">
470
+
Also available for{" "}
471
+
{isFirefox ? (
472
+
<>
473
+
<a
474
+
href="https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn"
475
+
target="_blank"
476
+
rel="noreferrer"
477
+
>
478
+
Edge
479
+
</a>{" "}
480
+
and{" "}
481
+
<a
482
+
href="https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/"
483
+
target="_blank"
484
+
rel="noreferrer"
485
+
>
486
+
Chrome
487
+
</a>
488
+
</>
489
+
) : isEdge ? (
490
+
<>
491
+
<a
492
+
href="https://addons.mozilla.org/en-US/firefox/addon/margin/"
493
+
target="_blank"
494
+
rel="noreferrer"
495
+
>
496
+
Firefox
497
+
</a>{" "}
498
+
and{" "}
499
+
<a
500
+
href="https://chromewebstore.google.com/detail/margin/cgpmbiiagnehkikhcbnhiagfomajncpa/"
501
+
target="_blank"
502
+
rel="noreferrer"
503
+
>
504
+
Chrome
505
+
</a>
506
+
</>
507
+
) : (
508
+
<>
509
+
<a
510
+
href="https://addons.mozilla.org/en-US/firefox/addon/margin/"
511
+
target="_blank"
512
+
rel="noreferrer"
513
+
>
514
+
Firefox
515
+
</a>{" "}
516
+
and{" "}
517
+
<a
518
+
href="https://microsoftedge.microsoft.com/addons/detail/margin/nfjnmllpdgcdnhmmggjihjbidmeadddn"
519
+
target="_blank"
520
+
rel="noreferrer"
521
+
>
522
+
Edge
523
+
</a>
524
+
</>
525
+
)}
526
+
</p>
527
+
</div>
528
+
</section>
529
+
530
+
<section className="landing-demo">
531
+
<DemoAnnotation />
532
+
</section>
533
+
534
+
<section className="landing-section">
535
+
<h2 className="landing-section-title">How it works</h2>
536
+
<div className="landing-steps">
537
+
<div className="landing-step">
538
+
<div className="landing-step-num">1</div>
539
+
<div className="landing-step-content">
540
+
<h3>Install & Login</h3>
541
+
<p>
542
+
Add Margin to your browser and sign in with your AT Protocol
543
+
handle. No new account needed, just your existing handle.
544
+
</p>
545
+
</div>
546
+
</div>
547
+
<div className="landing-step">
548
+
<div className="landing-step-num">2</div>
549
+
<div className="landing-step-content">
550
+
<h3>Annotate the Web</h3>
551
+
<p>
552
+
Highlight text on any page. Leave notes in the margins, ask
553
+
questions, or add context to the conversation precisely where it
554
+
belongs.
555
+
</p>
556
+
</div>
557
+
</div>
558
+
<div className="landing-step">
559
+
<div className="landing-step-num">3</div>
560
+
<div className="landing-step-content">
561
+
<h3>Share & Discover</h3>
562
+
<p>
563
+
Your annotations are published to your PDS. Discover what the
564
+
community is reading and discussing across the web.
565
+
</p>
566
+
</div>
567
+
</div>
568
+
</div>
569
+
</section>
570
+
571
+
<section className="landing-section landing-section-alt">
572
+
<div className="landing-features-grid">
573
+
<div className="landing-feature">
574
+
<div className="landing-feature-icon">
575
+
<Highlighter size={20} />
576
+
</div>
577
+
<h3>Universal Highlights</h3>
578
+
<p>
579
+
Save passages from any article, paper, or post. Your collection
580
+
travels with you, independent of any single platform.
581
+
</p>
582
+
</div>
583
+
<div className="landing-feature">
584
+
<div className="landing-feature-icon">
585
+
<MessageSquare size={20} />
586
+
</div>
587
+
<h3>Universal Notes</h3>
588
+
<p>
589
+
Move the discussion out of the comments section. Contextual
590
+
conversations that live right alongside the content.
591
+
</p>
592
+
</div>
593
+
<div className="landing-feature">
594
+
<div className="landing-feature-icon">
595
+
<Shield size={20} />
596
+
</div>
597
+
<h3>Open Identity</h3>
598
+
<p>
599
+
Your data, your handle, your graph. Built on the AT Protocol for
600
+
true ownership and portability.
601
+
</p>
602
+
</div>
603
+
<div className="landing-feature">
604
+
<div className="landing-feature-icon">
605
+
<Users size={20} />
606
+
</div>
607
+
<h3>Community Context</h3>
608
+
<p>
609
+
See the web with fresh eyes. Discover highlights and notes from
610
+
other readers directly on the page.
611
+
</p>
612
+
</div>
613
+
</div>
614
+
</section>
615
+
616
+
<section className="landing-section landing-protocol">
617
+
<div className="landing-protocol-grid">
618
+
<div className="landing-protocol-main">
619
+
<h2>Your data, your identity</h2>
620
+
<p>
621
+
Margin is built on the{" "}
622
+
<a href="https://atproto.com" target="_blank" rel="noreferrer">
623
+
AT Protocol
624
+
</a>
625
+
, the same open protocol that powers Bluesky. Sign in with your
626
+
existing Bluesky account or create a new one in your preferred
627
+
PDS.
628
+
</p>
629
+
<p>
630
+
Your annotations are stored in your PDS. You can export them
631
+
anytime, use them with other apps, or self-host your own server.
632
+
No vendor lock-in.
633
+
</p>
634
+
</div>
635
+
<div className="landing-protocol-features">
636
+
<div className="landing-protocol-item">
637
+
<Database size={20} />
638
+
<div>
639
+
<strong>Portable data</strong>
640
+
<span>Export or migrate anytime</span>
641
+
</div>
642
+
</div>
643
+
<div className="landing-protocol-item">
644
+
<Shield size={20} />
645
+
<div>
646
+
<strong>You own your identity</strong>
647
+
<span>Use your own domain as handle</span>
648
+
</div>
649
+
</div>
650
+
<div className="landing-protocol-item">
651
+
<Zap size={20} />
652
+
<div>
653
+
<strong>Interoperable</strong>
654
+
<span>Works with the ATProto ecosystem</span>
655
+
</div>
656
+
</div>
657
+
<div className="landing-protocol-item">
658
+
<Github size={20} />
659
+
<div>
660
+
<strong>Open source</strong>
661
+
<span>Audit, contribute, self-host</span>
662
+
</div>
663
+
</div>
664
+
</div>
665
+
</div>
666
+
</section>
667
+
668
+
<section className="landing-section landing-final-cta">
669
+
<h2>Start annotating today</h2>
670
+
<p>Free and open source. Sign in with ATProto to get started.</p>
671
+
<div className="landing-cta">
672
+
<a
673
+
href={ext.url}
674
+
target="_blank"
675
+
rel="noreferrer"
676
+
className="btn btn-primary btn-lg"
677
+
>
678
+
<ext.Icon size={18} />
679
+
Get the Extension
680
+
</a>
681
+
</div>
682
+
</section>
683
+
684
+
<footer className="landing-footer">
685
+
<div className="landing-footer-grid">
686
+
<div className="landing-footer-brand">
687
+
<Link to="/" className="landing-logo">
688
+
<img src={logo} alt="Margin" />
689
+
<span>Margin</span>
690
+
</Link>
691
+
<p>Write in the margins of the web.</p>
692
+
</div>
693
+
<div className="landing-footer-links">
694
+
<div className="landing-footer-col">
695
+
<h4>Product</h4>
696
+
<a href={ext.url} target="_blank" rel="noreferrer">
697
+
Browser Extension
698
+
</a>
699
+
<Link to="/home">Web App</Link>
700
+
</div>
701
+
<div className="landing-footer-col">
702
+
<h4>Community</h4>
703
+
<a
704
+
href="https://github.com/margin-at/margin"
705
+
target="_blank"
706
+
rel="noreferrer"
707
+
>
708
+
GitHub
709
+
</a>
710
+
<a
711
+
href="https://tangled.org/margin.at/margin"
712
+
target="_blank"
713
+
rel="noreferrer"
714
+
>
715
+
Tangled
716
+
</a>
717
+
<a
718
+
href="https://bsky.app/profile/margin.at"
719
+
target="_blank"
720
+
rel="noreferrer"
721
+
>
722
+
Bluesky
723
+
</a>
724
+
</div>
725
+
<div className="landing-footer-col">
726
+
<h4>Legal</h4>
727
+
<Link to="/privacy">Privacy Policy</Link>
728
+
<Link to="/terms">Terms of Service</Link>
729
+
</div>
730
+
</div>
731
+
</div>
732
+
<div className="landing-footer-bottom">
733
+
<p>© {new Date().getFullYear()} Margin. Open source under MIT.</p>
734
+
</div>
735
+
</footer>
736
+
</div>
737
+
);
738
+
}