atmosphere explorer pds.ls
tool typescript atproto

labels bottom bar

handle.invalid 2a2c4343 1bb22c59

verified
+87 -109
-43
src/components/sticky.tsx
··· 1 - import { createSignal, JSX, onCleanup, onMount } from "solid-js"; 2 - 3 - export const StickyOverlay = (props: { children?: JSX.Element }) => { 4 - const [filterStuck, setFilterStuck] = createSignal(false); 5 - 6 - return ( 7 - <> 8 - <div 9 - ref={(trigger) => { 10 - onMount(() => { 11 - const observer = new IntersectionObserver( 12 - ([entry]) => setFilterStuck(!entry.isIntersecting), 13 - { 14 - rootMargin: "-8px 0px 0px 0px", 15 - threshold: 0, 16 - }, 17 - ); 18 - 19 - observer.observe(trigger); 20 - 21 - onCleanup(() => { 22 - observer.unobserve(trigger); 23 - observer.disconnect(); 24 - }); 25 - }); 26 - }} 27 - class="pointer-events-none h-0" 28 - aria-hidden="true" 29 - /> 30 - 31 - <div 32 - class="sticky top-2 z-10 flex w-full flex-col items-center justify-center gap-2 rounded-lg border-[0.5px] p-3 transition-colors" 33 - classList={{ 34 - "bg-neutral-50 dark:bg-dark-300 border-neutral-300 dark:border-neutral-700 shadow-md": 35 - filterStuck(), 36 - "bg-transparent border-transparent shadow-none": !filterStuck(), 37 - }} 38 - > 39 - {props.children} 40 - </div> 41 - </> 42 - ); 43 - };
+16
src/utils/keyboard.ts
··· 1 + import { onCleanup } from "solid-js"; 2 + 3 + export const useFilterShortcut = (getRef: () => HTMLInputElement | undefined) => { 4 + const handleKeyDown = (e: KeyboardEvent) => { 5 + if ( 6 + e.key === "/" && 7 + !["INPUT", "TEXTAREA"].includes((e.target as HTMLElement)?.tagName) && 8 + !document.querySelector("[data-modal]") 9 + ) { 10 + e.preventDefault(); 11 + getRef()?.focus(); 12 + } 13 + }; 14 + document.addEventListener("keydown", handleKeyDown); 15 + onCleanup(() => document.removeEventListener("keydown", handleKeyDown)); 16 + };
+3 -13
src/views/collection.tsx
··· 4 4 import * as TID from "@atcute/tid"; 5 5 import { Title } from "@solidjs/meta"; 6 6 import { A, useBeforeLeave, useParams, useSearchParams } from "@solidjs/router"; 7 - import { createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js"; 7 + import { createMemo, createResource, createSignal, For, onMount, Show } from "solid-js"; 8 8 import { createStore } from "solid-js/store"; 9 9 import { agent } from "../auth/state"; 10 10 import { Button } from "../components/button.jsx"; ··· 17 17 import { canHover } from "../layout.jsx"; 18 18 import { resolvePDS } from "../utils/api.js"; 19 19 import { localDateFromTimestamp } from "../utils/date.js"; 20 + import { useFilterShortcut } from "../utils/keyboard.js"; 20 21 import { 21 22 clearCollectionCache, 22 23 getCollectionCache, ··· 104 105 }); 105 106 } 106 107 107 - const handleKeyDown = (e: KeyboardEvent) => { 108 - if ( 109 - e.key === "/" && 110 - !["INPUT", "TEXTAREA"].includes((e.target as HTMLElement)?.tagName) && 111 - !document.querySelector("[data-modal]") 112 - ) { 113 - e.preventDefault(); 114 - filterInputRef?.focus(); 115 - } 116 - }; 117 - document.addEventListener("keydown", handleKeyDown); 118 - onCleanup(() => document.removeEventListener("keydown", handleKeyDown)); 108 + useFilterShortcut(() => filterInputRef); 119 109 }); 120 110 121 111 useBeforeLeave((e) => {
+66 -39
src/views/labels.tsx
··· 8 8 import { Button } from "../components/button.jsx"; 9 9 import DidHoverCard from "../components/hover-card/did.jsx"; 10 10 import RecordHoverCard from "../components/hover-card/record.jsx"; 11 - import { StickyOverlay } from "../components/sticky.jsx"; 12 11 import { TextInput } from "../components/text-input.jsx"; 12 + import { canHover } from "../layout.jsx"; 13 13 import { labelerCache, resolveHandle, resolvePDS } from "../utils/api.js"; 14 14 import { localDateFromTimestamp } from "../utils/date.js"; 15 + import { useFilterShortcut } from "../utils/keyboard.js"; 15 16 16 17 const LABELS_PER_PAGE = 50; 17 18 const DEFAULT_LABELER_DID = "did:plc:ar7c4by46qjdydhdevvrndac"; ··· 67 68 68 69 let rpc: Client | undefined; 69 70 let formRef!: HTMLFormElement; 71 + let filterInputRef: HTMLInputElement | undefined; 70 72 71 73 const filteredLabels = createMemo(() => { 72 74 const filterValue = filter().trim(); ··· 116 118 const hasSearched = createMemo(() => Boolean(searchParams.uriPatterns)); 117 119 118 120 onMount(async () => { 121 + useFilterShortcut(() => filterInputRef); 122 + 119 123 if (searchParams.did && searchParams.uriPatterns) { 120 124 const formData = new FormData(); 121 125 formData.append("did", searchParams.did.toString()); ··· 128 132 let did = formData.get("did")?.toString()?.trim() || DEFAULT_LABELER_DID; 129 133 const uriPatterns = formData.get("uriPatterns")?.toString()?.trim(); 130 134 131 - if (!did || !uriPatterns) { 135 + if (!uriPatterns) { 132 136 setError("Please provide both DID and URI patterns"); 133 137 return; 134 138 } ··· 244 248 </form> 245 249 246 250 <Show when={hasSearched()}> 247 - <StickyOverlay> 248 - <div class="flex w-full items-center gap-x-2"> 249 - <TextInput 250 - placeholder="Filter labels (* for partial, -exclude)" 251 - name="filter" 252 - value={filter()} 253 - onInput={(e) => setFilter(e.currentTarget.value)} 254 - class="min-w-0 grow text-sm placeholder:text-xs" 255 - /> 256 - <div class="flex shrink-0 items-center gap-x-2 text-sm"> 257 - <Show when={labels().length > 0}> 258 - <span class="whitespace-nowrap text-neutral-600 dark:text-neutral-400"> 259 - {filteredLabels().length}/{labels().length} 260 - </span> 261 - </Show> 262 - 263 - <Show when={cursor()}> 264 - <Button 265 - onClick={handleLoadMore} 266 - disabled={loading()} 267 - classList={{ "w-20 h-7.5 justify-center": true }} 268 - > 269 - <Show 270 - when={!loading()} 271 - fallback={ 272 - <span class="iconify lucide--loader-circle animate-spin text-base" /> 273 - } 274 - > 275 - Load more 276 - </Show> 277 - </Button> 278 - </Show> 279 - </div> 280 - </div> 281 - </StickyOverlay> 282 - 283 - <div class="w-full max-w-3xl py-2"> 251 + <div class="w-full max-w-3xl py-2 pb-20"> 284 252 <Show when={loading() && labels().length === 0}> 285 253 <div class="flex flex-col items-center justify-center py-12 text-center"> 286 254 <span class="iconify lucide--loader-circle mb-3 animate-spin text-4xl text-neutral-400" /> ··· 311 279 </div> 312 280 </Show> 313 281 </Show> 282 + </div> 283 + 284 + <div class="dark:bg-dark-500 fixed bottom-0 z-10 flex w-full flex-col items-center gap-2 border-t border-neutral-200 bg-neutral-100 px-3 pt-3 pb-6 dark:border-neutral-700"> 285 + <div 286 + class="dark:bg-dark-200 flex w-full max-w-lg cursor-text items-center gap-2 rounded-lg border border-neutral-200 bg-white px-3 dark:border-neutral-700" 287 + onClick={(e) => { 288 + const input = e.currentTarget.querySelector("input"); 289 + if (e.target !== input) input?.focus(); 290 + }} 291 + > 292 + <span class="iconify lucide--filter text-neutral-500 dark:text-neutral-400" /> 293 + <input 294 + ref={filterInputRef} 295 + type="text" 296 + spellcheck={false} 297 + autocapitalize="off" 298 + autocomplete="off" 299 + class="grow py-2 select-none placeholder:text-sm focus:outline-none" 300 + placeholder="Filter labels... (* for partial, -exclude)" 301 + value={filter()} 302 + onInput={(e) => setFilter(e.currentTarget.value)} 303 + /> 304 + <Show when={canHover && !filter()}> 305 + <kbd class="rounded border border-neutral-200 bg-neutral-50 px-1.5 py-0.5 font-mono text-xs text-neutral-400 select-none dark:border-neutral-600 dark:bg-neutral-700"> 306 + / 307 + </kbd> 308 + </Show> 309 + </div> 310 + 311 + <div class="flex min-h-7.5 w-full max-w-lg items-center justify-between"> 312 + <div class="w-20" /> 313 + 314 + <div> 315 + <Show when={filter()}> 316 + <span>{filteredLabels().length}</span> 317 + <span>/</span> 318 + </Show> 319 + <span>{labels().length} labels</span> 320 + </div> 321 + 322 + <div class="flex w-20 items-center justify-end"> 323 + <Show when={cursor()}> 324 + <Button 325 + onClick={handleLoadMore} 326 + disabled={loading()} 327 + classList={{ "w-20 h-7.5 justify-center": true }} 328 + > 329 + <Show 330 + when={!loading()} 331 + fallback={ 332 + <span class="iconify lucide--loader-circle animate-spin text-base" /> 333 + } 334 + > 335 + Load more 336 + </Show> 337 + </Button> 338 + </Show> 339 + </div> 340 + </div> 314 341 </div> 315 342 </Show> 316 343 </div>
+2 -14
src/views/repo.tsx
··· 9 9 createSignal, 10 10 ErrorBoundary, 11 11 For, 12 - onCleanup, 13 12 onMount, 14 13 Show, 15 14 Suspense, ··· 42 41 validateHandle, 43 42 } from "../utils/api.js"; 44 43 import { detectDidKeyType, detectKeyType } from "../utils/key.js"; 44 + import { useFilterShortcut } from "../utils/keyboard.js"; 45 45 import { BlobView } from "./blob.jsx"; 46 46 import { PlcLogView } from "./logs.jsx"; 47 47 import { plcDirectory } from "./settings.jsx"; ··· 80 80 }); 81 81 82 82 onMount(() => { 83 - const handleKeyDown = (e: KeyboardEvent) => { 84 - if ( 85 - e.key === "/" && 86 - !["INPUT", "TEXTAREA"].includes((e.target as HTMLElement)?.tagName) && 87 - !document.querySelector("[data-modal]") 88 - ) { 89 - e.preventDefault(); 90 - filterInputRef?.focus(); 91 - } 92 - }; 93 - 94 - document.addEventListener("keydown", handleKeyDown); 95 - onCleanup(() => document.removeEventListener("keydown", handleKeyDown)); 83 + useFilterShortcut(() => filterInputRef); 96 84 }); 97 85 98 86 const RepoTab = (props: {