atmosphere explorer pds.ls
tool typescript atproto

new homepage

handle.invalid 687eb3ea 0658d7cc

verified
+193 -231
+9 -3
src/components/dropdown.tsx
··· 37 37 class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 38 38 > 39 39 <Show when={props.icon}> 40 - <span class={"iconify shrink-0 " + props.icon}></span> 40 + <span 41 + class={"iconify shrink-0 text-neutral-500 dark:text-neutral-400 " + props.icon} 42 + ></span> 41 43 </Show> 42 44 <span class="whitespace-nowrap">{props.label}</span> 43 45 </button> ··· 64 66 > 65 67 <div class="flex items-center gap-2"> 66 68 <Show when={props.icon}> 67 - <span class={"iconify shrink-0 " + props.icon}></span> 69 + <span 70 + class={"iconify shrink-0 text-neutral-500 dark:text-neutral-400 " + props.icon} 71 + ></span> 68 72 </Show> 69 73 <span class="whitespace-nowrap">{props.label}</span> 70 74 </div> ··· 92 96 class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 93 97 > 94 98 <Show when={props.icon}> 95 - <span class={"iconify shrink-0 " + props.icon}></span> 99 + <span 100 + class={"iconify shrink-0 text-neutral-500 dark:text-neutral-400 " + props.icon} 101 + ></span> 96 102 </Show> 97 103 <span class="whitespace-nowrap">{props.label}</span> 98 104 </button>
+59 -22
src/components/search.tsx
··· 50 50 51 51 export const [showSearch, setShowSearch] = createSignal(false); 52 52 53 + const EXAMPLES: (RecentSearch & { prefix: string })[] = [ 54 + { 55 + path: "/at://did:plc:vwzwgnygau7ed7b7wt5ux7y2", 56 + label: "retr0.id", 57 + type: "handle", 58 + prefix: "@", 59 + }, 60 + { 61 + path: "/at://did:plc:ia76kvnndjutgedggx2ibrem/app.bsky.actor.profile/self", 62 + label: "mary.my.id/app.bsky.actor.profile/self", 63 + type: "at-uri", 64 + prefix: "at://", 65 + }, 66 + { path: "/npmx.social", label: "npmx.social", type: "pds", prefix: "pds:" }, 67 + { 68 + path: "/at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.post#schema", 69 + label: "app.bsky.feed.post", 70 + type: "lexicon", 71 + prefix: "lex:", 72 + }, 73 + { 74 + path: "/at://did:plc:oisofpd7lj26yvgiivf3lxsi/app.bsky.feed.post/3mfflamxxvk2t", 75 + label: "bsky.app/profile/hailey.at/post/3mfflamxxvk2t", 76 + type: "at-uri", 77 + prefix: "https://", 78 + }, 79 + ]; 80 + 53 81 const SEARCH_PREFIXES: { prefix: string; description: string }[] = [ 54 82 { prefix: "@", description: "example.com" }, 55 83 { prefix: "did:", description: "web:example.com" }, ··· 343 371 /> 344 372 </div> 345 373 346 - <Show when={getRecentSuggestions().length > 0 || search()?.length}> 374 + <Show 375 + when={ 376 + getRecentSuggestions().length > 0 || 377 + search()?.length || 378 + (!input() && recentSearches().length === 0) 379 + } 380 + > 347 381 <div 348 - class={`flex w-full flex-col overflow-hidden border-t border-neutral-200 dark:border-neutral-700 ${input() ? "rounded-b-md" : ""}`} 382 + class="flex w-full flex-col overflow-hidden rounded-b-md border-t border-neutral-200 dark:border-neutral-700" 349 383 onMouseDown={(e) => e.preventDefault()} 350 384 > 385 + {/* Suggestions (shown when no recents and no input) */} 386 + <Show when={!input() && recentSearches().length === 0}> 387 + <div class="mt-2 mb-1 flex px-3"> 388 + <span class="text-xs font-medium text-neutral-500 dark:text-neutral-400"> 389 + Examples 390 + </span> 391 + </div> 392 + <For each={EXAMPLES}> 393 + {(example) => ( 394 + <A 395 + href={example.path} 396 + class="dark:hover:bg-dark-100 flex items-center gap-2 px-3 py-2 text-sm hover:bg-neutral-100" 397 + onClick={() => setShowSearch(false)} 398 + > 399 + <span class="truncate"> 400 + <span class="text-neutral-500 dark:text-neutral-400">{example.prefix}</span> 401 + {example.label} 402 + </span> 403 + </A> 404 + )} 405 + </For> 406 + </Show> 407 + 351 408 {/* Recent searches */} 352 409 <Show when={getRecentSuggestions().length > 0}> 353 410 <div class="mt-2 mb-1 flex items-center justify-between px-3"> ··· 446 503 ); 447 504 }} 448 505 </For> 449 - </div> 450 - </Show> 451 - <Show when={!input()}> 452 - <div class="flex flex-col gap-1 border-t border-neutral-200 px-3 py-2 text-xs text-neutral-500 dark:border-neutral-700 dark:text-neutral-400"> 453 - <div class="flex flex-wrap gap-1.5"> 454 - <div> 455 - @<span class="text-neutral-400 dark:text-neutral-500">retr0.id</span> 456 - </div> 457 - <div>did:</div> 458 - <div>at://</div> 459 - <div> 460 - lex: 461 - <span class="text-neutral-400 dark:text-neutral-500">app.bsky.feed.post</span> 462 - </div> 463 - <div> 464 - pds: 465 - <span class="text-neutral-400 dark:text-neutral-500">tngl.sh</span> 466 - </div> 467 - </div> 468 - <span>Bluesky, Tangled, Pinksea, Popfeed, or Blento links</span> 469 506 </div> 470 507 </Show> 471 508 </form>
+1 -1
src/layout.tsx
··· 159 159 <DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-md p-1.5"> 160 160 <NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" /> 161 161 <NavMenu href="/firehose" label="Firehose" icon="lucide--rss" /> 162 - <NavMenu href="/spacedust" label="Spacedust" icon="lucide--orbit" /> 162 + <NavMenu href="/spacedust" label="Spacedust" icon="lucide--sparkles" /> 163 163 <MenuSeparator /> 164 164 <NavMenu href="/labels" label="Labels" icon="lucide--tag" /> 165 165 <NavMenu href="/car" label="Archive tools" icon="lucide--folder-archive" />
+124 -205
src/views/home.tsx
··· 1 1 import { A } from "@solidjs/router"; 2 - import { createSignal, For, JSX, onCleanup, onMount } from "solid-js"; 2 + import { JSX } from "solid-js"; 3 3 import { setOpenManager, setShowAddAccount } from "../auth/state"; 4 - import { Button } from "../components/button"; 5 - import { Favicon } from "../components/favicon"; 6 - import { JSONValue } from "../components/json"; 7 - import { SearchButton } from "../components/search"; 4 + import { setShowSearch } from "../components/search"; 8 5 9 - const SLIDES = ["Repository", "Record", "PDS"] as const; 6 + const baseCardClass = 7 + "group flex flex-col gap-1 rounded-lg border border-neutral-200 bg-neutral-50 p-3 text-neutral-700 transition-colors dark:border-neutral-700 dark:bg-neutral-800/50 dark:text-neutral-300 hover:bg-neutral-50/50 dark:hover:bg-neutral-800"; 10 8 11 - const slideLinks = [ 12 - "/at://did:plc:vwzwgnygau7ed7b7wt5ux7y2", 13 - "/at://did:plc:ia76kvnndjutgedggx2ibrem/app.bsky.feed.post/3kenlltlvus2u", 14 - "/npmx.social", 15 - ] as const; 9 + const accentCard = { 10 + blue: `${baseCardClass} hover:border-blue-500 dark:hover:border-blue-400`, 11 + orange: `${baseCardClass} hover:border-red-500 dark:hover:border-red-400`, 12 + violet: `${baseCardClass} hover:border-emerald-500 dark:hover:border-emerald-400`, 13 + }; 16 14 17 - const exampleRecord = { 18 - text: "ma'am do you know where the petard is, i'd like to hoist myself with it", 19 - $type: "app.bsky.feed.post", 20 - langs: ["en"], 21 - createdAt: "2023-11-20T21:44:21.000Z", 15 + const accentIcon = { 16 + blue: "text-neutral-400 dark:text-neutral-500 group-hover:text-blue-500 dark:group-hover:text-blue-400", 17 + orange: 18 + "text-neutral-400 dark:text-neutral-500 group-hover:text-red-500 dark:group-hover:text-red-400", 19 + violet: 20 + "text-neutral-400 dark:text-neutral-500 group-hover:text-emerald-500 dark:group-hover:text-emerald-400", 22 21 }; 23 22 24 - const exampleCollections = [ 25 - { authority: "app.bsky", nsids: ["actor.profile", "feed.post", "feed.like", "graph.follow"] }, 26 - { authority: "sh.tangled", nsids: ["actor.profile", "repo.pull"] }, 27 - { authority: "place.stream", nsids: ["chat.message"] }, 28 - ]; 23 + type Accent = "blue" | "orange" | "violet"; 29 24 30 - const exampleRepos = [ 31 - "did:plc:ty2jdjtqqq4jn7kk7p3mpwae", 32 - "did:plc:byfvayavc7z2ldyu6bu5myz2", 33 - "did:plc:n34gdj7o3o6ktuxp5qfbgllu", 34 - "did:plc:vh7y4mqklsu2uui5tlwl42dy", 35 - "did:plc:uz76j2yr2ps7apdxtlgqljwk", 36 - ]; 25 + const CardContent = (props: { 26 + icon: string; 27 + title: string; 28 + description: string; 29 + accent: Accent; 30 + }) => ( 31 + <> 32 + <span class="flex items-center gap-1.5 text-xs sm:text-sm"> 33 + <span class={`${props.icon} iconify shrink-0 ${accentIcon[props.accent]}`} /> 34 + <span class="font-medium">{props.title}</span> 35 + </span> 36 + <span class="text-xs text-neutral-500 dark:text-neutral-400">{props.description}</span> 37 + </> 38 + ); 37 39 38 - const ExplorerShowcase = () => { 39 - const [slide, setSlide] = createSignal(0); 40 + const ButtonCard = (props: { 41 + onClick: () => void; 42 + icon: string; 43 + title: string; 44 + description: string; 45 + accent: Accent; 46 + }) => ( 47 + <button onClick={props.onClick} class={`${accentCard[props.accent]} text-left`}> 48 + <CardContent 49 + icon={props.icon} 50 + title={props.title} 51 + description={props.description} 52 + accent={props.accent} 53 + /> 54 + </button> 55 + ); 40 56 41 - onMount(() => { 42 - const id = setInterval(() => setSlide((s) => (s + 1) % SLIDES.length), 5000); 43 - onCleanup(() => clearInterval(id)); 44 - }); 45 - 46 - return ( 47 - <div class="flex flex-col gap-1.5"> 48 - <A 49 - href={slideLinks[slide()]} 50 - class="relative block h-42 overflow-hidden rounded-lg border border-neutral-200 bg-neutral-50 transition-colors hover:border-neutral-300 dark:border-neutral-700 dark:bg-neutral-800 dark:hover:border-neutral-600" 51 - > 52 - {/* Collections slide */} 53 - <div 54 - class="pointer-events-none absolute inset-0 flex flex-col gap-1 overflow-hidden px-3 py-2 text-sm wrap-anywhere transition-opacity duration-700" 55 - classList={{ "opacity-0": slide() !== 0 }} 56 - > 57 - <For each={exampleCollections}> 58 - {({ authority, nsids }) => ( 59 - <div class="flex items-start gap-2"> 60 - <Favicon authority={authority} /> 61 - <div class="flex flex-col"> 62 - <For each={nsids}> 63 - {(nsid) => ( 64 - <span> 65 - <span class="text-neutral-500 dark:text-neutral-400">{authority}.</span> 66 - <span>{nsid}</span> 67 - </span> 68 - )} 69 - </For> 70 - </div> 71 - </div> 72 - )} 73 - </For> 74 - </div> 75 - 76 - {/* Record slide */} 77 - <div 78 - class="pointer-events-none absolute inset-0 overflow-hidden px-3 py-2 font-mono text-xs wrap-anywhere whitespace-pre-wrap transition-opacity duration-700 sm:text-sm" 79 - classList={{ "opacity-0": slide() !== 1 }} 80 - > 81 - <JSONValue data={exampleRecord as any} repo="did:plc:ia76kvnndjutgedggx2ibrem" /> 82 - </div> 83 - 84 - {/* Repos slide */} 85 - <div 86 - class="pointer-events-none absolute inset-0 overflow-hidden py-0.5 transition-opacity duration-700" 87 - classList={{ "opacity-0": slide() !== 2 }} 88 - > 89 - <For each={exampleRepos}> 90 - {(did) => ( 91 - <div class="flex min-w-0 items-center gap-2 p-1.5 font-mono text-sm"> 92 - <span class="flex shrink-0 items-center text-neutral-400 dark:text-neutral-500"> 93 - <span class="iconify lucide--chevron-right" /> 94 - </span> 95 - <span class="truncate text-blue-500 dark:text-blue-400">{did}</span> 96 - </div> 97 - )} 98 - </For> 99 - </div> 100 - </A> 101 - 102 - {/* Slide indicator */} 103 - <div class="flex items-center justify-between px-0.5"> 104 - <span class="text-xs text-neutral-400 dark:text-neutral-500">{SLIDES[slide()]}</span> 105 - <div class="flex gap-1"> 106 - <For each={SLIDES}> 107 - {(_, i) => ( 108 - <button 109 - onClick={() => setSlide(i())} 110 - class="h-1 rounded-full transition-all duration-300" 111 - classList={{ 112 - "w-4 bg-neutral-400 dark:bg-neutral-500": slide() === i(), 113 - "w-1.5 bg-neutral-300 dark:bg-neutral-600": slide() !== i(), 114 - }} 115 - /> 116 - )} 117 - </For> 118 - </div> 119 - </div> 120 - </div> 121 - ); 122 - }; 57 + const LinkCard = (props: { 58 + href: string; 59 + icon: string; 60 + title: string; 61 + description: string; 62 + accent: Accent; 63 + }) => ( 64 + <A href={props.href} class={accentCard[props.accent]}> 65 + <CardContent 66 + icon={props.icon} 67 + title={props.title} 68 + description={props.description} 69 + accent={props.accent} 70 + /> 71 + </A> 72 + ); 123 73 124 74 export const Home = () => { 125 75 const FooterLink = (props: { ··· 138 88 ); 139 89 140 90 return ( 141 - <div class="flex w-full flex-col gap-5 px-2 wrap-break-word"> 91 + <div class="flex w-full flex-col gap-6 px-2 wrap-break-word"> 142 92 {/* Welcome Section */} 143 - <div class="flex flex-col gap-4"> 144 - <div class="flex flex-col gap-1"> 145 - <h1 class="text-lg font-medium">Atmosphere Explorer</h1> 146 - <div class="text-sm text-neutral-600 dark:text-neutral-300"> 147 - <p> 148 - Browse the public data on the{" "} 149 - <a 150 - href="https://atproto.com" 151 - target="_blank" 152 - class="underline decoration-neutral-400 transition-colors hover:text-blue-500 hover:decoration-blue-500 dark:decoration-neutral-500 dark:hover:text-blue-400" 153 - > 154 - AT Protocol 155 - </a> 156 - </p> 157 - </div> 93 + <div class="flex flex-col gap-1"> 94 + <h1 class="text-lg font-medium">Atmosphere Explorer</h1> 95 + <div class="text-sm text-neutral-600 dark:text-neutral-300/80"> 96 + <p> 97 + Browse the public data on the{" "} 98 + <a 99 + href="https://atproto.com" 100 + target="_blank" 101 + class="underline decoration-neutral-400 transition-colors hover:text-blue-500 hover:decoration-blue-500 dark:decoration-neutral-500 dark:hover:text-blue-400" 102 + > 103 + AT Protocol 104 + </a> 105 + </p> 158 106 </div> 159 - 160 - <ExplorerShowcase /> 107 + </div> 161 108 162 - <div class="flex items-center gap-1.5 text-xs text-neutral-500 dark:text-neutral-400"> 163 - <SearchButton /> 164 - <span>to find any account</span> 165 - </div> 166 - <div class="flex items-center gap-1.5 text-xs text-neutral-500 dark:text-neutral-400"> 167 - <Button 109 + <div class="flex flex-col gap-3 text-sm"> 110 + <div class="grid grid-cols-2 gap-2 text-sm"> 111 + <ButtonCard 112 + onClick={() => setShowSearch(true)} 113 + icon="lucide--search" 114 + title="Search" 115 + description="Find any user or record" 116 + accent="blue" 117 + /> 118 + <ButtonCard 168 119 onClick={() => { 169 120 setOpenManager(true); 170 121 setShowAddAccount(true); 171 122 }} 172 - > 173 - <span class="iconify lucide--user-round"></span> 174 - Sign in 175 - </Button> 176 - <span>to manage records</span> 123 + icon="lucide--user-round" 124 + title="Sign in" 125 + description="Manage records" 126 + accent="blue" 127 + /> 177 128 </div> 178 - </div> 179 129 180 - <div class="flex flex-col gap-4 text-sm"> 181 - <div class="flex flex-col gap-2"> 182 - <A 130 + <div class="grid grid-cols-3 gap-2"> 131 + <LinkCard 183 132 href="/jetstream" 184 - class="group grid grid-cols-[auto_1fr] items-center gap-x-2 gap-y-0.5 text-neutral-700 transition-colors hover:text-blue-500 dark:text-neutral-300 dark:hover:text-blue-400" 185 - > 186 - <div class="iconify lucide--radio-tower" /> 187 - <span class="underline decoration-transparent group-hover:decoration-current"> 188 - Jetstream 189 - </span> 190 - <div /> 191 - <span class="text-xs text-neutral-500 dark:text-neutral-400"> 192 - Event stream with filtering 193 - </span> 194 - </A> 195 - <A 133 + icon="lucide--radio-tower" 134 + title="Jetstream" 135 + description="Simplified stream" 136 + accent="orange" 137 + /> 138 + <LinkCard 196 139 href="/firehose" 197 - class="group grid grid-cols-[auto_1fr] items-center gap-x-2 gap-y-0.5 text-neutral-700 transition-colors hover:text-blue-500 dark:text-neutral-300 dark:hover:text-blue-400" 198 - > 199 - <div class="iconify lucide--rss" /> 200 - <span class="underline decoration-transparent group-hover:decoration-current"> 201 - Firehose 202 - </span> 203 - <div /> 204 - <span class="text-xs text-neutral-500 dark:text-neutral-400"> 205 - Raw relay event stream 206 - </span> 207 - </A> 208 - <A 140 + icon="lucide--rss" 141 + title="Firehose" 142 + description="Raw event stream" 143 + accent="orange" 144 + /> 145 + <LinkCard 209 146 href="/spacedust" 210 - class="group grid grid-cols-[auto_1fr] items-center gap-x-2 gap-y-0.5 text-neutral-700 transition-colors hover:text-blue-500 dark:text-neutral-300 dark:hover:text-blue-400" 211 - > 212 - <div class="iconify lucide--orbit" /> 213 - <span class="underline decoration-transparent group-hover:decoration-current"> 214 - Spacedust 215 - </span> 216 - <div /> 217 - <span class="text-xs text-neutral-500 dark:text-neutral-400"> 218 - Interaction links stream 219 - </span> 220 - </A> 147 + icon="lucide--sparkles" 148 + title="Spacedust" 149 + description="Backlinks stream" 150 + accent="orange" 151 + /> 221 152 </div> 222 153 223 - <div class="flex flex-col gap-2"> 224 - <A 154 + <div class="grid grid-cols-2 gap-2"> 155 + <LinkCard 225 156 href="/labels" 226 - class="group grid grid-cols-[auto_1fr] items-center gap-x-2 gap-y-0.5 text-neutral-700 transition-colors hover:text-blue-500 dark:text-neutral-300 dark:hover:text-blue-400" 227 - > 228 - <div class="iconify lucide--tag" /> 229 - <span class="underline decoration-transparent group-hover:decoration-current"> 230 - Labels 231 - </span> 232 - <div /> 233 - <span class="text-xs text-neutral-500 dark:text-neutral-400"> 234 - Query labeler services 235 - </span> 236 - </A> 237 - <A 157 + icon="lucide--tag" 158 + title="Labels" 159 + description="Query labeler services" 160 + accent="violet" 161 + /> 162 + <LinkCard 238 163 href="/car" 239 - class="group grid grid-cols-[auto_1fr] items-center gap-x-2 gap-y-0.5 text-neutral-700 transition-colors hover:text-blue-500 dark:text-neutral-300 dark:hover:text-blue-400" 240 - > 241 - <div class="iconify lucide--folder-archive" /> 242 - <span class="underline decoration-transparent group-hover:decoration-current"> 243 - Archive 244 - </span> 245 - <div /> 246 - <span class="text-xs text-neutral-500 dark:text-neutral-400"> 247 - Explore and unpack CAR files 248 - </span> 249 - </A> 164 + icon="lucide--folder-archive" 165 + title="Archive" 166 + description="Explore CAR files" 167 + accent="violet" 168 + /> 250 169 </div> 251 170 </div> 252 171