+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
1
import { expect } from "@/lib/result";
2
2
import {
3
3
type Fronter,
4
+
fronterGetSocialAppHrefs,
4
5
fronterGetSocialAppHref,
5
6
getFronter,
6
7
getSpFronters,
···
80
81
browser.tabs.sendMessage(sender.tab?.id!, {
81
82
type: "TIMELINE_FRONTER",
82
83
results: new Map(
83
-
results.map((fronter) => [
84
-
fronterGetSocialAppHref(fronter, fronter.rkey),
85
-
fronter,
86
-
]),
84
+
results.flatMap((fronter) =>
85
+
fronterGetSocialAppHrefs(fronter, fronter.rkey).map((href) => [
86
+
href,
87
+
fronter,
88
+
]),
89
+
),
87
90
),
88
91
});
89
92
};
···
126
129
(await Promise.allSettled(allPromises))
127
130
.filter((result) => result.status === "fulfilled")
128
131
.flatMap((result) => result.value ?? [])
129
-
.map((fronter) => [
130
-
fronterGetSocialAppHref(fronter, fronter.rkey),
131
-
fronter,
132
-
]),
132
+
.flatMap((fronter) =>
133
+
fronterGetSocialAppHrefs(fronter, fronter.rkey).map((href) => [
134
+
href,
135
+
fronter,
136
+
]),
137
+
),
133
138
);
134
139
browser.tabs.sendMessage(sender.tab?.id!, {
135
140
type: "TIMELINE_FRONTER",
···
170
175
(await Promise.allSettled(promises))
171
176
.filter((result) => result.status === "fulfilled")
172
177
.flatMap((result) => result.value ?? [])
173
-
.map((fronter) => [
174
-
fronterGetSocialAppHref(fronter, fronter.rkey, fronter.depth),
175
-
fronter,
176
-
]),
178
+
.flatMap((fronter) =>
179
+
fronterGetSocialAppHrefs(fronter, fronter.rkey, fronter.depth).map(
180
+
(href) => [href, fronter],
181
+
),
182
+
),
177
183
);
178
184
browser.tabs.sendMessage(sender.tab?.id!, {
179
185
type: "THREAD_FRONTER",
+5
-1
src/entrypoints/content.ts
+5
-1
src/entrypoints/content.ts
···
2
2
import {
3
3
Fronter,
4
4
fronterGetSocialAppHref,
5
+
fronterGetSocialAppHrefs,
5
6
parseSocialAppPostUrl,
6
7
} from "@/lib/utils";
7
8
import { parseResourceUri, ResourceUri } from "@atcute/lexicons";
···
145
146
fronters.entries().flatMap(([uri, fronter]) => {
146
147
if (!fronter) return [];
147
148
const rkey = expect(parseResourceUri(uri)).rkey!;
148
-
return [[fronterGetSocialAppHref(fronter, rkey), fronter]];
149
+
return fronterGetSocialAppHrefs(fronter, rkey).map((href) => [
150
+
href,
151
+
fronter,
152
+
]);
149
153
}),
150
154
);
151
155
applyFrontersToPage(updated);
+163
-99
src/entrypoints/popup/App.svelte
+163
-99
src/entrypoints/popup/App.svelte
···
9
9
let isQuerying = $state(false);
10
10
let fronterName = $state("");
11
11
let spToken = $state("");
12
+
let isFromCurrentTab = $state(false);
12
13
13
14
const makeOutput = (fronter: any) => {
14
-
return `HANDLE: ${fronter.handle ?? "handle.invalid"}<br>FRONTER: ${fronter.fronterName}`;
15
+
return `HANDLE: ${fronter.handle ?? "handle.invalid"}<br>FRONTER(S): ${fronter.names.join(", ")}`;
15
16
};
16
17
17
18
const queryRecord = async (recordUri: ResourceUri) => {
···
50
51
const clearResult = () => {
51
52
queryResult = "";
52
53
recordAtUri = "";
54
+
isFromCurrentTab = false;
53
55
};
54
56
55
57
onMount(async () => {
···
73
75
if (tabFronter) {
74
76
queryResult = makeOutput(tabFronter);
75
77
recordAtUri = tabFronter.recordUri;
78
+
isFromCurrentTab = true;
76
79
}
77
80
});
78
81
</script>
79
82
80
83
<main>
81
84
<div class="container">
82
-
<header class="header">
83
-
<div class="title">AT_FRONTER</div>
84
-
</header>
85
-
86
85
<div class="content">
87
86
<section class="query-panel">
88
87
<div class="panel-header">
···
116
115
117
116
<div class="output-container">
118
117
<div class="output-header">
119
-
<span>OUTPUT</span>
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>
120
129
<div class="clear-button-container">
121
130
{#if queryResult && !isQuerying}
122
131
<button
···
161
170
<span class="panel-title">CONFIGURATION</span>
162
171
<div class="panel-accent"></div>
163
172
</div>
164
-
165
-
<div class="config-row">
166
-
<span class="config-label">FRONTER_NAME</span>
167
-
<div class="config-input-wrapper">
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>
168
194
<input
169
195
type="text"
170
196
placeholder="enter_identifier"
···
174
200
class:has-value={fronterName}
175
201
/>
176
202
</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
-
/>
203
+
<div class="config-note">
204
+
<span class="note-text">
205
+
overrides Simply Plural fronters when set
206
+
</span>
190
207
</div>
191
208
</div>
192
209
</section>
193
210
</div>
194
211
195
212
<footer class="footer">
196
-
<span
197
-
>SOURCE ON <a
198
-
href="https://tangled.sh/did:plc:dfl62fgb7wtjj3fcbb72naae/at-fronter"
199
-
>TANGLED</a
200
-
></span
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
201
219
>
202
220
</footer>
203
221
</div>
···
225
243
background: linear-gradient(180deg, #000000 0%, #0a0a0a 100%);
226
244
}
227
245
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
246
.title {
249
-
font-size: 18px;
250
-
font-weight: 800;
251
-
letter-spacing: 3px;
252
-
color: #ffffff;
247
+
font-size: 10px;
248
+
font-weight: 700;
249
+
letter-spacing: 2px;
250
+
color: #999999;
251
+
line-height: 1;
252
+
vertical-align: baseline;
253
253
}
254
254
255
255
.content {
256
256
flex: 1;
257
257
display: flex;
258
258
flex-direction: column;
259
-
gap: 24px;
260
-
padding: 24px 20px;
259
+
gap: 20px;
260
+
padding: 18px 16px;
261
261
overflow-y: auto;
262
262
}
263
263
···
270
270
.config-panel {
271
271
display: flex;
272
272
flex-direction: column;
273
-
gap: 16px;
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;
274
304
}
275
305
276
306
.panel-header {
···
310
340
311
341
.record-input {
312
342
flex: 1;
313
-
padding: 16px 18px;
343
+
padding: 12px 14px;
314
344
background: transparent;
315
345
border: none;
316
346
outline: none;
···
331
361
332
362
.exec-button {
333
363
position: relative;
334
-
padding: 16px 28px;
364
+
padding: 8px 10px;
335
365
background: #2a2a2a;
336
366
border: none;
337
367
border-left: 1px solid #444444;
···
396
426
min-height: 32px;
397
427
}
398
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
+
399
465
.clear-button-container {
400
466
width: 60px;
401
467
display: flex;
···
435
501
}
436
502
437
503
.output-content {
438
-
padding: 18px;
504
+
padding: 14px;
439
505
height: 100%;
440
506
display: flex;
441
507
align-items: center;
···
495
561
496
562
.config-row {
497
563
display: flex;
498
-
flex-direction: column;
499
-
gap: 8px;
564
+
align-items: center;
565
+
gap: 12px;
566
+
margin-bottom: 0;
500
567
}
501
568
502
569
.config-label {
503
-
font-size: 11px;
504
-
color: #aaaaaa;
505
-
letter-spacing: 1.5px;
570
+
font-size: 12px;
571
+
color: #cccccc;
572
+
letter-spacing: 1px;
506
573
font-weight: 700;
507
-
}
508
-
509
-
.config-input-wrapper {
510
-
display: flex;
511
-
align-items: center;
574
+
white-space: nowrap;
575
+
min-width: 90px;
512
576
}
513
577
514
578
.config-input {
515
579
flex: 1;
516
-
padding: 14px 18px;
580
+
padding: 10px 12px;
517
581
background: #181818;
518
582
border: 1px solid #333333;
519
583
color: #ffffff;
520
584
font-family: inherit;
521
-
font-size: 13px;
585
+
font-size: 12px;
522
586
font-weight: 500;
523
587
transition: all 0.2s ease;
524
588
position: relative;
···
540
604
541
605
.footer {
542
606
display: flex;
543
-
align-items: center;
607
+
align-items: baseline;
544
608
justify-content: center;
545
-
padding: 16px 20px;
609
+
gap: 8px;
610
+
padding: 12px 16px;
546
611
background: #000000;
547
-
border-top: 1px solid #333333;
548
-
font-size: 10px;
549
-
color: #888888;
550
-
font-weight: 600;
551
-
letter-spacing: 1px;
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;
552
618
position: relative;
553
619
}
554
620
···
559
625
left: 0;
560
626
width: 100%;
561
627
height: 1px;
562
-
background: linear-gradient(90deg, transparent, #555555, transparent);
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;
563
642
}
564
643
565
-
.footer a {
566
-
color: #aaaaaa;
644
+
.footer-link {
645
+
color: #999999;
567
646
text-decoration: none;
568
647
font-weight: 700;
569
648
transition: color 0.2s ease;
649
+
line-height: 1;
650
+
vertical-align: baseline;
570
651
}
571
652
572
-
.footer a:hover {
573
-
color: #ffffff;
653
+
.footer-link:hover {
654
+
color: #cccccc;
574
655
}
575
656
576
657
/* Animations */
···
591
672
100% {
592
673
left: 100%;
593
674
}
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
675
}
612
676
</style>
+68
-3
src/entrypoints/popup/app.css
+68
-3
src/entrypoints/popup/app.css
···
87
87
color: #ffffff;
88
88
}
89
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 */
90
108
/* Global scrollbar styling */
91
109
::-webkit-scrollbar {
92
-
width: 2px;
93
-
height: 2px;
110
+
width: 8px;
111
+
height: 8px;
94
112
}
95
113
96
114
::-webkit-scrollbar-track {
97
-
background: #000000;
115
+
background: #0a0a0a;
116
+
border-radius: 0;
98
117
}
99
118
100
119
::-webkit-scrollbar-thumb {
101
120
background: #333333;
121
+
border-radius: 0;
102
122
border: none;
123
+
transition: background 0.2s ease;
103
124
}
104
125
105
126
::-webkit-scrollbar-thumb:hover {
106
127
background: #555555;
107
128
}
108
129
130
+
::-webkit-scrollbar-thumb:active {
131
+
background: #666666;
132
+
}
133
+
109
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 {
110
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;
111
176
}
112
177
113
178
/* Animations */
+15
-5
src/lib/utils.ts
+15
-5
src/lib/utils.ts
···
265
265
}));
266
266
};
267
267
268
-
export const fronterGetSocialAppHref = (
268
+
export const fronterGetSocialAppHrefs = (
269
269
fronter: Fronter,
270
270
rkey: RecordKey,
271
271
depth?: number,
272
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}`;
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}`;
277
287
};
278
288
279
289
export const parseSocialAppPostUrl = (url: string) => {