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