+1
.gitignore
+1
.gitignore
+1
-1
package.json
+1
-1
package.json
+18
-12
src/entrypoints/background.ts
+18
-12
src/entrypoints/background.ts
···
1
import { expect } from "@/lib/result";
2
import {
3
type Fronter,
4
fronterGetSocialAppHref,
5
getFronter,
6
getSpFronters,
···
80
browser.tabs.sendMessage(sender.tab?.id!, {
81
type: "TIMELINE_FRONTER",
82
results: new Map(
83
-
results.map((fronter) => [
84
-
fronterGetSocialAppHref(fronter, fronter.rkey),
85
-
fronter,
86
-
]),
87
),
88
});
89
};
···
126
(await Promise.allSettled(allPromises))
127
.filter((result) => result.status === "fulfilled")
128
.flatMap((result) => result.value ?? [])
129
-
.map((fronter) => [
130
-
fronterGetSocialAppHref(fronter, fronter.rkey),
131
-
fronter,
132
-
]),
133
);
134
browser.tabs.sendMessage(sender.tab?.id!, {
135
type: "TIMELINE_FRONTER",
···
170
(await Promise.allSettled(promises))
171
.filter((result) => result.status === "fulfilled")
172
.flatMap((result) => result.value ?? [])
173
-
.map((fronter) => [
174
-
fronterGetSocialAppHref(fronter, fronter.rkey, fronter.depth),
175
-
fronter,
176
-
]),
177
);
178
browser.tabs.sendMessage(sender.tab?.id!, {
179
type: "THREAD_FRONTER",
···
1
import { expect } from "@/lib/result";
2
import {
3
type Fronter,
4
+
fronterGetSocialAppHrefs,
5
fronterGetSocialAppHref,
6
getFronter,
7
getSpFronters,
···
81
browser.tabs.sendMessage(sender.tab?.id!, {
82
type: "TIMELINE_FRONTER",
83
results: new Map(
84
+
results.flatMap((fronter) =>
85
+
fronterGetSocialAppHrefs(fronter, fronter.rkey).map((href) => [
86
+
href,
87
+
fronter,
88
+
]),
89
+
),
90
),
91
});
92
};
···
129
(await Promise.allSettled(allPromises))
130
.filter((result) => result.status === "fulfilled")
131
.flatMap((result) => result.value ?? [])
132
+
.flatMap((fronter) =>
133
+
fronterGetSocialAppHrefs(fronter, fronter.rkey).map((href) => [
134
+
href,
135
+
fronter,
136
+
]),
137
+
),
138
);
139
browser.tabs.sendMessage(sender.tab?.id!, {
140
type: "TIMELINE_FRONTER",
···
175
(await Promise.allSettled(promises))
176
.filter((result) => result.status === "fulfilled")
177
.flatMap((result) => result.value ?? [])
178
+
.flatMap((fronter) =>
179
+
fronterGetSocialAppHrefs(fronter, fronter.rkey, fronter.depth).map(
180
+
(href) => [href, fronter],
181
+
),
182
+
),
183
);
184
browser.tabs.sendMessage(sender.tab?.id!, {
185
type: "THREAD_FRONTER",
+5
-1
src/entrypoints/content.ts
+5
-1
src/entrypoints/content.ts
···
2
import {
3
Fronter,
4
fronterGetSocialAppHref,
5
parseSocialAppPostUrl,
6
} from "@/lib/utils";
7
import { parseResourceUri, ResourceUri } from "@atcute/lexicons";
···
145
fronters.entries().flatMap(([uri, fronter]) => {
146
if (!fronter) return [];
147
const rkey = expect(parseResourceUri(uri)).rkey!;
148
-
return [[fronterGetSocialAppHref(fronter, rkey), fronter]];
149
}),
150
);
151
applyFrontersToPage(updated);
···
2
import {
3
Fronter,
4
fronterGetSocialAppHref,
5
+
fronterGetSocialAppHrefs,
6
parseSocialAppPostUrl,
7
} from "@/lib/utils";
8
import { parseResourceUri, ResourceUri } from "@atcute/lexicons";
···
146
fronters.entries().flatMap(([uri, fronter]) => {
147
if (!fronter) return [];
148
const rkey = expect(parseResourceUri(uri)).rkey!;
149
+
return fronterGetSocialAppHrefs(fronter, rkey).map((href) => [
150
+
href,
151
+
fronter,
152
+
]);
153
}),
154
);
155
applyFrontersToPage(updated);
+163
-99
src/entrypoints/popup/App.svelte
+163
-99
src/entrypoints/popup/App.svelte
···
9
let isQuerying = $state(false);
10
let fronterName = $state("");
11
let spToken = $state("");
12
13
const makeOutput = (fronter: any) => {
14
-
return `HANDLE: ${fronter.handle ?? "handle.invalid"}<br>FRONTER: ${fronter.fronterName}`;
15
};
16
17
const queryRecord = async (recordUri: ResourceUri) => {
···
50
const clearResult = () => {
51
queryResult = "";
52
recordAtUri = "";
53
};
54
55
onMount(async () => {
···
73
if (tabFronter) {
74
queryResult = makeOutput(tabFronter);
75
recordAtUri = tabFronter.recordUri;
76
}
77
});
78
</script>
79
80
<main>
81
<div class="container">
82
-
<header class="header">
83
-
<div class="title">AT_FRONTER</div>
84
-
</header>
85
-
86
<div class="content">
87
<section class="query-panel">
88
<div class="panel-header">
···
116
117
<div class="output-container">
118
<div class="output-header">
119
-
<span>OUTPUT</span>
120
<div class="clear-button-container">
121
{#if queryResult && !isQuerying}
122
<button
···
161
<span class="panel-title">CONFIGURATION</span>
162
<div class="panel-accent"></div>
163
</div>
164
-
165
-
<div class="config-row">
166
-
<span class="config-label">FRONTER_NAME</span>
167
-
<div class="config-input-wrapper">
168
<input
169
type="text"
170
placeholder="enter_identifier"
···
174
class:has-value={fronterName}
175
/>
176
</div>
177
-
</div>
178
-
179
-
<div class="config-row">
180
-
<span class="config-label">SP_TOKEN</span>
181
-
<div class="config-input-wrapper">
182
-
<input
183
-
type="password"
184
-
placeholder="enter_simply_plural_token"
185
-
oninput={updateSpToken}
186
-
bind:value={spToken}
187
-
class="config-input"
188
-
class:has-value={spToken}
189
-
/>
190
</div>
191
</div>
192
</section>
193
</div>
194
195
<footer class="footer">
196
-
<span
197
-
>SOURCE ON <a
198
-
href="https://tangled.sh/did:plc:dfl62fgb7wtjj3fcbb72naae/at-fronter"
199
-
>TANGLED</a
200
-
></span
201
>
202
</footer>
203
</div>
···
225
background: linear-gradient(180deg, #000000 0%, #0a0a0a 100%);
226
}
227
228
-
.header {
229
-
display: flex;
230
-
align-items: center;
231
-
justify-content: center;
232
-
padding: 20px 20px;
233
-
background: #000000;
234
-
border-bottom: 1px solid #333333;
235
-
position: relative;
236
-
}
237
-
238
-
.header::after {
239
-
content: "";
240
-
position: absolute;
241
-
bottom: 0;
242
-
left: 0;
243
-
width: 100%;
244
-
height: 1px;
245
-
background: linear-gradient(90deg, transparent, #555555, transparent);
246
-
}
247
-
248
.title {
249
-
font-size: 18px;
250
-
font-weight: 800;
251
-
letter-spacing: 3px;
252
-
color: #ffffff;
253
}
254
255
.content {
256
flex: 1;
257
display: flex;
258
flex-direction: column;
259
-
gap: 24px;
260
-
padding: 24px 20px;
261
overflow-y: auto;
262
}
263
···
270
.config-panel {
271
display: flex;
272
flex-direction: column;
273
-
gap: 16px;
274
}
275
276
.panel-header {
···
310
311
.record-input {
312
flex: 1;
313
-
padding: 16px 18px;
314
background: transparent;
315
border: none;
316
outline: none;
···
331
332
.exec-button {
333
position: relative;
334
-
padding: 16px 28px;
335
background: #2a2a2a;
336
border: none;
337
border-left: 1px solid #444444;
···
396
min-height: 32px;
397
}
398
399
.clear-button-container {
400
width: 60px;
401
display: flex;
···
435
}
436
437
.output-content {
438
-
padding: 18px;
439
height: 100%;
440
display: flex;
441
align-items: center;
···
495
496
.config-row {
497
display: flex;
498
-
flex-direction: column;
499
-
gap: 8px;
500
}
501
502
.config-label {
503
-
font-size: 11px;
504
-
color: #aaaaaa;
505
-
letter-spacing: 1.5px;
506
font-weight: 700;
507
-
}
508
-
509
-
.config-input-wrapper {
510
-
display: flex;
511
-
align-items: center;
512
}
513
514
.config-input {
515
flex: 1;
516
-
padding: 14px 18px;
517
background: #181818;
518
border: 1px solid #333333;
519
color: #ffffff;
520
font-family: inherit;
521
-
font-size: 13px;
522
font-weight: 500;
523
transition: all 0.2s ease;
524
position: relative;
···
540
541
.footer {
542
display: flex;
543
-
align-items: center;
544
justify-content: center;
545
-
padding: 16px 20px;
546
background: #000000;
547
-
border-top: 1px solid #333333;
548
-
font-size: 10px;
549
-
color: #888888;
550
-
font-weight: 600;
551
-
letter-spacing: 1px;
552
position: relative;
553
}
554
···
559
left: 0;
560
width: 100%;
561
height: 1px;
562
-
background: linear-gradient(90deg, transparent, #555555, transparent);
563
}
564
565
-
.footer a {
566
-
color: #aaaaaa;
567
text-decoration: none;
568
font-weight: 700;
569
transition: color 0.2s ease;
570
}
571
572
-
.footer a:hover {
573
-
color: #ffffff;
574
}
575
576
/* Animations */
···
591
100% {
592
left: 100%;
593
}
594
-
}
595
-
596
-
/* Scrollbar */
597
-
.content::-webkit-scrollbar {
598
-
width: 2px;
599
-
}
600
-
601
-
.content::-webkit-scrollbar-track {
602
-
background: #000000;
603
-
}
604
-
605
-
.content::-webkit-scrollbar-thumb {
606
-
background: #333333;
607
-
}
608
-
609
-
.content::-webkit-scrollbar-thumb:hover {
610
-
background: #555555;
611
}
612
</style>
···
9
let isQuerying = $state(false);
10
let fronterName = $state("");
11
let spToken = $state("");
12
+
let isFromCurrentTab = $state(false);
13
14
const makeOutput = (fronter: any) => {
15
+
return `HANDLE: ${fronter.handle ?? "handle.invalid"}<br>FRONTER(S): ${fronter.names.join(", ")}`;
16
};
17
18
const queryRecord = async (recordUri: ResourceUri) => {
···
51
const clearResult = () => {
52
queryResult = "";
53
recordAtUri = "";
54
+
isFromCurrentTab = false;
55
};
56
57
onMount(async () => {
···
75
if (tabFronter) {
76
queryResult = makeOutput(tabFronter);
77
recordAtUri = tabFronter.recordUri;
78
+
isFromCurrentTab = true;
79
}
80
});
81
</script>
82
83
<main>
84
<div class="container">
85
<div class="content">
86
<section class="query-panel">
87
<div class="panel-header">
···
115
116
<div class="output-container">
117
<div class="output-header">
118
+
<div class="output-header-left">
119
+
<span>OUTPUT</span>
120
+
{#if isFromCurrentTab}
121
+
<div class="tab-indicator">
122
+
<span class="tab-indicator-text"
123
+
>FROM_CURRENT_TAB</span
124
+
>
125
+
<div class="tab-indicator-accent"></div>
126
+
</div>
127
+
{/if}
128
+
</div>
129
<div class="clear-button-container">
130
{#if queryResult && !isQuerying}
131
<button
···
170
<span class="panel-title">CONFIGURATION</span>
171
<div class="panel-accent"></div>
172
</div>
173
+
<div class="config-card">
174
+
<div class="config-row">
175
+
<span class="config-label">SP_TOKEN</span>
176
+
<input
177
+
type="password"
178
+
placeholder="enter_simply_plural_token"
179
+
oninput={updateSpToken}
180
+
bind:value={spToken}
181
+
class="config-input"
182
+
class:has-value={spToken}
183
+
/>
184
+
</div>
185
+
<div class="config-note">
186
+
<span class="note-text">
187
+
token requires only read permissions
188
+
</span>
189
+
</div>
190
+
</div>
191
+
<div class="config-card">
192
+
<div class="config-row">
193
+
<span class="config-label">FRONTER_NAME</span>
194
<input
195
type="text"
196
placeholder="enter_identifier"
···
200
class:has-value={fronterName}
201
/>
202
</div>
203
+
<div class="config-note">
204
+
<span class="note-text">
205
+
overrides Simply Plural fronters when set
206
+
</span>
207
</div>
208
</div>
209
</section>
210
</div>
211
212
<footer class="footer">
213
+
<span class="title">AT_FRONTER</span>
214
+
<span class="footer-separator">•</span>
215
+
<span class="footer-source">SOURCE ON </span>
216
+
<a
217
+
href="https://tangled.sh/did:plc:dfl62fgb7wtjj3fcbb72naae/at-fronter"
218
+
class="footer-link">TANGLED</a
219
>
220
</footer>
221
</div>
···
243
background: linear-gradient(180deg, #000000 0%, #0a0a0a 100%);
244
}
245
246
.title {
247
+
font-size: 10px;
248
+
font-weight: 700;
249
+
letter-spacing: 2px;
250
+
color: #999999;
251
+
line-height: 1;
252
+
vertical-align: baseline;
253
}
254
255
.content {
256
flex: 1;
257
display: flex;
258
flex-direction: column;
259
+
gap: 20px;
260
+
padding: 18px 16px;
261
overflow-y: auto;
262
}
263
···
270
.config-panel {
271
display: flex;
272
flex-direction: column;
273
+
gap: 12px;
274
+
}
275
+
276
+
.config-card {
277
+
background: #0d0d0d;
278
+
border: 1px solid #2a2a2a;
279
+
border-left: 3px solid #444444;
280
+
padding: 10px;
281
+
display: flex;
282
+
flex-direction: column;
283
+
gap: 6px;
284
+
transition: border-left-color 0.2s ease;
285
+
}
286
+
287
+
.config-card:hover {
288
+
border-left-color: #555555;
289
+
}
290
+
291
+
.config-note {
292
+
padding: 0;
293
+
background: transparent;
294
+
border: none;
295
+
margin: 0;
296
+
}
297
+
298
+
.note-text {
299
+
font-size: 11px;
300
+
color: #bbbbbb;
301
+
line-height: 1.3;
302
+
font-weight: 500;
303
+
letter-spacing: 0.5px;
304
}
305
306
.panel-header {
···
340
341
.record-input {
342
flex: 1;
343
+
padding: 12px 14px;
344
background: transparent;
345
border: none;
346
outline: none;
···
361
362
.exec-button {
363
position: relative;
364
+
padding: 8px 10px;
365
background: #2a2a2a;
366
border: none;
367
border-left: 1px solid #444444;
···
426
min-height: 32px;
427
}
428
429
+
.output-header-left {
430
+
display: flex;
431
+
align-items: center;
432
+
gap: 12px;
433
+
}
434
+
435
+
.tab-indicator {
436
+
display: flex;
437
+
align-items: center;
438
+
gap: 6px;
439
+
padding: 4px 8px;
440
+
background: #1a1a1a;
441
+
border: 1px solid #333333;
442
+
position: relative;
443
+
overflow: hidden;
444
+
}
445
+
446
+
.tab-indicator-text {
447
+
font-size: 9px;
448
+
color: #00ff41;
449
+
font-weight: 700;
450
+
letter-spacing: 1px;
451
+
position: relative;
452
+
z-index: 1;
453
+
}
454
+
455
+
.tab-indicator-accent {
456
+
position: absolute;
457
+
left: 0;
458
+
bottom: 0;
459
+
width: 100%;
460
+
height: 1px;
461
+
background: #00ff41;
462
+
animation: pulse 2s ease-in-out infinite;
463
+
}
464
+
465
.clear-button-container {
466
width: 60px;
467
display: flex;
···
501
}
502
503
.output-content {
504
+
padding: 14px;
505
height: 100%;
506
display: flex;
507
align-items: center;
···
561
562
.config-row {
563
display: flex;
564
+
align-items: center;
565
+
gap: 12px;
566
+
margin-bottom: 0;
567
}
568
569
.config-label {
570
+
font-size: 12px;
571
+
color: #cccccc;
572
+
letter-spacing: 1px;
573
font-weight: 700;
574
+
white-space: nowrap;
575
+
min-width: 90px;
576
}
577
578
.config-input {
579
flex: 1;
580
+
padding: 10px 12px;
581
background: #181818;
582
border: 1px solid #333333;
583
color: #ffffff;
584
font-family: inherit;
585
+
font-size: 12px;
586
font-weight: 500;
587
transition: all 0.2s ease;
588
position: relative;
···
604
605
.footer {
606
display: flex;
607
+
align-items: baseline;
608
justify-content: center;
609
+
gap: 8px;
610
+
padding: 12px 16px;
611
background: #000000;
612
+
border-top: 1px solid #222222;
613
+
font-size: 9px;
614
+
color: #666666;
615
+
font-weight: 500;
616
+
letter-spacing: 0.5px;
617
+
line-height: 1;
618
position: relative;
619
}
620
···
625
left: 0;
626
width: 100%;
627
height: 1px;
628
+
background: linear-gradient(90deg, transparent, #333333, transparent);
629
+
}
630
+
631
+
.footer-separator {
632
+
color: #444444;
633
+
font-weight: 400;
634
+
line-height: 1;
635
+
vertical-align: baseline;
636
+
}
637
+
638
+
.footer-source {
639
+
color: #777777;
640
+
line-height: 1;
641
+
vertical-align: baseline;
642
}
643
644
+
.footer-link {
645
+
color: #999999;
646
text-decoration: none;
647
font-weight: 700;
648
transition: color 0.2s ease;
649
+
line-height: 1;
650
+
vertical-align: baseline;
651
}
652
653
+
.footer-link:hover {
654
+
color: #cccccc;
655
}
656
657
/* Animations */
···
672
100% {
673
left: 100%;
674
}
675
}
676
</style>
+68
-3
src/entrypoints/popup/app.css
+68
-3
src/entrypoints/popup/app.css
···
87
color: #ffffff;
88
}
89
90
/* Global scrollbar styling */
91
::-webkit-scrollbar {
92
-
width: 2px;
93
-
height: 2px;
94
}
95
96
::-webkit-scrollbar-track {
97
-
background: #000000;
98
}
99
100
::-webkit-scrollbar-thumb {
101
background: #333333;
102
border: none;
103
}
104
105
::-webkit-scrollbar-thumb:hover {
106
background: #555555;
107
}
108
109
::-webkit-scrollbar-corner {
110
background: #000000;
111
}
112
113
/* Animations */
···
87
color: #ffffff;
88
}
89
90
+
/* Cross-browser scrollbar styling */
91
+
92
+
/* Standard scrollbar properties (Firefox, Chrome 121+, Edge 121+) */
93
+
* {
94
+
scrollbar-width: thin;
95
+
scrollbar-color: #333333 #0a0a0a;
96
+
}
97
+
98
+
/* Content areas get even thinner scrollbars */
99
+
.content,
100
+
.output-content,
101
+
textarea,
102
+
input {
103
+
scrollbar-width: thin;
104
+
scrollbar-color: #2a2a2a #000000;
105
+
}
106
+
107
+
/* Webkit scrollbar styling for older browsers and better customization */
108
/* Global scrollbar styling */
109
::-webkit-scrollbar {
110
+
width: 8px;
111
+
height: 8px;
112
}
113
114
::-webkit-scrollbar-track {
115
+
background: #0a0a0a;
116
+
border-radius: 0;
117
}
118
119
::-webkit-scrollbar-thumb {
120
background: #333333;
121
+
border-radius: 0;
122
border: none;
123
+
transition: background 0.2s ease;
124
}
125
126
::-webkit-scrollbar-thumb:hover {
127
background: #555555;
128
}
129
130
+
::-webkit-scrollbar-thumb:active {
131
+
background: #666666;
132
+
}
133
+
134
::-webkit-scrollbar-corner {
135
+
background: #0a0a0a;
136
+
}
137
+
138
+
/* Scrollbar for specific containers */
139
+
.content::-webkit-scrollbar,
140
+
.output-content::-webkit-scrollbar,
141
+
textarea::-webkit-scrollbar,
142
+
input::-webkit-scrollbar {
143
+
width: 6px;
144
+
height: 6px;
145
+
}
146
+
147
+
.content::-webkit-scrollbar-track,
148
+
.output-content::-webkit-scrollbar-track,
149
+
textarea::-webkit-scrollbar-track,
150
+
input::-webkit-scrollbar-track {
151
background: #000000;
152
+
}
153
+
154
+
.content::-webkit-scrollbar-thumb,
155
+
.output-content::-webkit-scrollbar-thumb,
156
+
textarea::-webkit-scrollbar-thumb,
157
+
input::-webkit-scrollbar-thumb {
158
+
background: #2a2a2a;
159
+
border-radius: 0;
160
+
border: none;
161
+
transition: background 0.15s ease;
162
+
}
163
+
164
+
.content::-webkit-scrollbar-thumb:hover,
165
+
.output-content::-webkit-scrollbar-thumb:hover,
166
+
textarea::-webkit-scrollbar-thumb:hover,
167
+
input::-webkit-scrollbar-thumb:hover {
168
+
background: #444444;
169
+
}
170
+
171
+
.content::-webkit-scrollbar-thumb:active,
172
+
.output-content::-webkit-scrollbar-thumb:active,
173
+
textarea::-webkit-scrollbar-thumb:active,
174
+
input::-webkit-scrollbar-thumb:active {
175
+
background: #555555;
176
}
177
178
/* Animations */
+15
-5
src/lib/utils.ts
+15
-5
src/lib/utils.ts
···
265
}));
266
};
267
268
-
export const fronterGetSocialAppHref = (
269
fronter: Fronter,
270
rkey: RecordKey,
271
depth?: number,
272
) => {
273
-
// console.log("fronterGetSocialAppHref", fronter, rkey, depth);
274
-
return depth === 0
275
-
? `/profile/${fronter.handle ?? fronter.did}`
276
-
: `/profile/${fronter.handle ?? fronter.did}/post/${rkey}`;
277
};
278
279
export const parseSocialAppPostUrl = (url: string) => {
···
265
}));
266
};
267
268
+
export const fronterGetSocialAppHrefs = (
269
fronter: Fronter,
270
rkey: RecordKey,
271
depth?: number,
272
) => {
273
+
return [
274
+
fronter.handle
275
+
? [fronterGetSocialAppHref(fronter.handle, rkey, depth)]
276
+
: [],
277
+
fronterGetSocialAppHref(fronter.did, rkey, depth),
278
+
].flat();
279
+
};
280
+
281
+
export const fronterGetSocialAppHref = (
282
+
repo: string,
283
+
rkey: RecordKey,
284
+
depth?: number,
285
+
) => {
286
+
return depth === 0 ? `/profile/${repo}` : `/profile/${repo}/post/${rkey}`;
287
};
288
289
export const parseSocialAppPostUrl = (url: string) => {