view who was fronting when a record was made

feat: actually work properly on forks, refactor ui

ptr.pet 739597f6 823fd030

verified
Changed files
+271 -121
src
+1
.gitignore
··· 14 14 .wxt 15 15 .wxt-runner 16 16 web-ext.config.ts 17 + web-ext-artifacts 17 18 18 19 # Editor directories and files 19 20 .vscode/*
+1 -1
package.json
··· 2 2 "name": "at-fronter", 3 3 "description": "view who was fronting when a record was made", 4 4 "private": true, 5 - "version": "0.0.0", 5 + "version": "0.0.1", 6 6 "type": "module", 7 7 "scripts": { 8 8 "dev": "wxt",
+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
··· 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
··· 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
··· 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
··· 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) => {