atmosphere explorer pdsls.dev
atproto tool typescript

dynamic titles

juli.ee e56b7c5f d5187fc8

verified
-1
index.html
··· 10 10 <meta property="og:description" content="Browse the public data on atproto" /> 11 11 <meta property="description" content="Browse the public data on atproto" /> 12 12 <link rel="manifest" href="/manifest.json" /> 13 - <title>PDSls</title> 14 13 <link rel="preconnect" href="https://fonts.bunny.net" /> 15 14 <link href="https://fonts.bunny.net/css?family=roboto-mono:400" rel="stylesheet" /> 16 15 <link href="https://fonts.cdnfonts.com/css/pecita" rel="stylesheet" />
+87 -86
src/layout.tsx
··· 1 1 import { Handle } from "@atcute/lexicons"; 2 - import { Meta, MetaProvider } from "@solidjs/meta"; 2 + import { Meta, MetaProvider, Title } from "@solidjs/meta"; 3 3 import { A, RouteSectionProps, useLocation, useNavigate } from "@solidjs/router"; 4 4 import { createEffect, ErrorBoundary, onCleanup, onMount, Show, Suspense } from "solid-js"; 5 5 import { AccountManager } from "./auth/account.jsx"; ··· 118 118 }); 119 119 120 120 return ( 121 - <div id="main" class="mx-auto mb-8 flex max-w-lg flex-col items-center p-3"> 122 - <MetaProvider> 123 - <Show when={location.pathname !== "/"}> 124 - <Meta name="robots" content="noindex, nofollow" /> 125 - </Show> 126 - </MetaProvider> 127 - <header 128 - class={`dark:shadow-dark-700 mb-3 flex w-full items-center justify-between rounded-xl border-[0.5px] border-neutral-300 bg-neutral-50 bg-size-[95%] bg-right bg-no-repeat p-2 pl-3 shadow-xs [--header-bg:#fafafa] [--trans-blue:#5BCEFA90] [--trans-pink:#F5A9B890] [--trans-white:#FFFFFF90] dark:border-neutral-700 dark:bg-neutral-800 dark:[--header-bg:#262626] dark:[--trans-blue:#5BCEFAa0] dark:[--trans-pink:#F5A9B8a0] dark:[--trans-white:#FFFFFFa0] ${localStorage.getItem("hrt") === "true" ? "bg-[linear-gradient(to_left,transparent_10%,var(--header-bg)_85%),linear-gradient(to_bottom,var(--trans-blue)_0%,var(--trans-blue)_20%,var(--trans-pink)_20%,var(--trans-pink)_40%,var(--trans-white)_40%,var(--trans-white)_60%,var(--trans-pink)_60%,var(--trans-pink)_80%,var(--trans-blue)_80%,var(--trans-blue)_100%)]" : ""}`} 129 - style={{ 130 - "background-image": 131 - props.params.repo && props.params.repo in headers ? 132 - `linear-gradient(to left, transparent 10%, var(--header-bg) 85%), url(/headers/${headers[props.params.repo]})` 133 - : undefined, 134 - }} 135 - > 136 - <A 137 - href="/" 138 - style='font-feature-settings: "cv05"' 139 - class="relative flex items-center gap-1 text-xl font-semibold" 121 + <MetaProvider> 122 + <Title>PDSls</Title> 123 + <Show when={location.pathname !== "/"}> 124 + <Meta name="robots" content="noindex, nofollow" /> 125 + </Show> 126 + <div id="main" class="mx-auto mb-8 flex max-w-lg flex-col items-center p-3"> 127 + <header 128 + class={`dark:shadow-dark-700 mb-3 flex w-full items-center justify-between rounded-xl border-[0.5px] border-neutral-300 bg-neutral-50 bg-size-[95%] bg-right bg-no-repeat p-2 pl-3 shadow-xs [--header-bg:#fafafa] [--trans-blue:#5BCEFA90] [--trans-pink:#F5A9B890] [--trans-white:#FFFFFF90] dark:border-neutral-700 dark:bg-neutral-800 dark:[--header-bg:#262626] dark:[--trans-blue:#5BCEFAa0] dark:[--trans-pink:#F5A9B8a0] dark:[--trans-white:#FFFFFFa0] ${localStorage.getItem("hrt") === "true" ? "bg-[linear-gradient(to_left,transparent_10%,var(--header-bg)_85%),linear-gradient(to_bottom,var(--trans-blue)_0%,var(--trans-blue)_20%,var(--trans-pink)_20%,var(--trans-pink)_40%,var(--trans-white)_40%,var(--trans-white)_60%,var(--trans-pink)_60%,var(--trans-pink)_80%,var(--trans-blue)_80%,var(--trans-blue)_100%)]" : ""}`} 129 + style={{ 130 + "background-image": 131 + props.params.repo && props.params.repo in headers ? 132 + `linear-gradient(to left, transparent 10%, var(--header-bg) 85%), url(/headers/${headers[props.params.repo]})` 133 + : undefined, 134 + }} 140 135 > 141 - <span class="iconify tabler--binary-tree-filled text-[#76c4e5]"></span> 142 - <span>PDSls</span> 143 - <Show when={localStorage.getItem("hrt") === "true"}> 144 - <img 145 - src="/ribbon.webp" 146 - alt="" 147 - class="pointer-events-none absolute -top-3 -right-4 w-8 rotate-15" 148 - /> 136 + <A 137 + href="/" 138 + style='font-feature-settings: "cv05"' 139 + class="relative flex items-center gap-1 text-xl font-semibold" 140 + > 141 + <span class="iconify tabler--binary-tree-filled text-[#76c4e5]"></span> 142 + <span>PDSls</span> 143 + <Show when={localStorage.getItem("hrt") === "true"}> 144 + <img 145 + src="/ribbon.webp" 146 + alt="" 147 + class="pointer-events-none absolute -top-3 -right-4 w-8 rotate-15" 148 + /> 149 + </Show> 150 + </A> 151 + <div class="relative flex items-center gap-0.5 rounded-lg bg-neutral-50/60 px-1 py-0.5 dark:bg-neutral-800/60"> 152 + <SearchButton /> 153 + <Show when={hasUserScope("create")}> 154 + <RecordEditor create={true} /> 155 + </Show> 156 + <AccountManager /> 157 + <MenuProvider> 158 + <DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-lg p-1.5"> 159 + <NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" /> 160 + <NavMenu href="/firehose" label="Firehose" icon="lucide--antenna" /> 161 + <NavMenu href="/labels" label="Labels" icon="lucide--tag" /> 162 + <NavMenu href="/settings" label="Settings" icon="lucide--settings" /> 163 + <MenuSeparator /> 164 + <NavMenu 165 + href="https://bsky.app/profile/did:plc:6q5daed5gutiyerimlrnojnz" 166 + label="Bluesky" 167 + icon="simple-icons--bluesky text-[#0085ff]" 168 + newTab 169 + /> 170 + <NavMenu 171 + href="https://tangled.org/@pdsls.dev/pdsls/" 172 + label="Source" 173 + icon="lucide--code" 174 + newTab 175 + /> 176 + </DropdownMenu> 177 + </MenuProvider> 178 + </div> 179 + </header> 180 + <div class="flex w-full flex-col items-center gap-3 text-pretty"> 181 + <Show when={showSearch() || location.pathname === "/"}> 182 + <Search /> 183 + </Show> 184 + <Show when={props.params.pds}> 185 + <NavBar params={props.params} /> 149 186 </Show> 150 - </A> 151 - <div class="relative flex items-center gap-0.5 rounded-lg bg-neutral-50/60 px-1 py-0.5 dark:bg-neutral-800/60"> 152 - <SearchButton /> 153 - <Show when={hasUserScope("create")}> 154 - <RecordEditor create={true} /> 187 + <Show keyed when={location.pathname}> 188 + <ErrorBoundary 189 + fallback={(err) => <div class="mt-3 wrap-anywhere">Error: {err.message}</div>} 190 + > 191 + <Suspense 192 + fallback={ 193 + <span class="iconify lucide--loader-circle mt-3 animate-spin text-xl"></span> 194 + } 195 + > 196 + {props.children} 197 + </Suspense> 198 + </ErrorBoundary> 155 199 </Show> 156 - <AccountManager /> 157 - <MenuProvider> 158 - <DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-lg p-1.5"> 159 - <NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" /> 160 - <NavMenu href="/firehose" label="Firehose" icon="lucide--antenna" /> 161 - <NavMenu href="/labels" label="Labels" icon="lucide--tag" /> 162 - <NavMenu href="/settings" label="Settings" icon="lucide--settings" /> 163 - <MenuSeparator /> 164 - <NavMenu 165 - href="https://bsky.app/profile/did:plc:6q5daed5gutiyerimlrnojnz" 166 - label="Bluesky" 167 - icon="simple-icons--bluesky text-[#0085ff]" 168 - newTab 169 - /> 170 - <NavMenu 171 - href="https://tangled.org/@pdsls.dev/pdsls/" 172 - label="Source" 173 - icon="lucide--code" 174 - newTab 175 - /> 176 - </DropdownMenu> 177 - </MenuProvider> 178 200 </div> 179 - </header> 180 - <div class="flex w-full flex-col items-center gap-3 text-pretty"> 181 - <Show when={showSearch() || location.pathname === "/"}> 182 - <Search /> 183 - </Show> 184 - <Show when={props.params.pds}> 185 - <NavBar params={props.params} /> 186 - </Show> 187 - <Show keyed when={location.pathname}> 188 - <ErrorBoundary 189 - fallback={(err) => <div class="mt-3 wrap-anywhere">Error: {err.message}</div>} 190 - > 191 - <Suspense 192 - fallback={ 193 - <span class="iconify lucide--loader-circle mt-3 animate-spin text-xl"></span> 194 - } 195 - > 196 - {props.children} 197 - </Suspense> 198 - </ErrorBoundary> 201 + <NotificationContainer /> 202 + <Show 203 + when={localStorage.plcDirectory && localStorage.plcDirectory !== "https://plc.directory"} 204 + > 205 + <div class="dark:bg-dark-500 fixed right-0 bottom-0 left-0 z-10 flex items-center justify-center bg-neutral-100 px-3 py-1 text-xs"> 206 + <span> 207 + PLC directory: <span class="font-medium">{localStorage.plcDirectory}</span> 208 + </span> 209 + </div> 199 210 </Show> 200 211 </div> 201 - <NotificationContainer /> 202 - <Show 203 - when={localStorage.plcDirectory && localStorage.plcDirectory !== "https://plc.directory"} 204 - > 205 - <div class="dark:bg-dark-500 fixed right-0 bottom-0 left-0 z-10 flex items-center justify-center bg-neutral-100 px-3 py-1 text-xs"> 206 - <span> 207 - PLC directory: <span class="font-medium">{localStorage.plcDirectory}</span> 208 - </span> 209 - </div> 210 - </Show> 211 - </div> 212 + </MetaProvider> 212 213 ); 213 214 }; 214 215
+157 -153
src/views/collection.tsx
··· 2 2 import { Client, simpleFetchHandler } from "@atcute/client"; 3 3 import { $type, ActorIdentifier, InferXRPCBodyOutput } from "@atcute/lexicons"; 4 4 import * as TID from "@atcute/tid"; 5 + import { Title } from "@solidjs/meta"; 5 6 import { A, useBeforeLeave, useParams } from "@solidjs/router"; 6 7 import { 7 8 createEffect, ··· 250 251 ); 251 252 252 253 return ( 253 - <Show when={records.length || response()}> 254 - <div class="-mt-2 flex w-full flex-col items-center"> 255 - <StickyOverlay> 256 - <div class="flex w-full flex-col gap-2"> 257 - <div class="flex items-center gap-1.5"> 258 - <Show when={agent() && agent()?.sub === did && hasUserScope("delete")}> 259 - <div class="flex items-center"> 260 - <Tooltip 261 - text={batchDelete() ? "Cancel" : "Delete"} 262 - children={ 263 - <button 264 - onclick={() => { 265 - setRecords({ from: 0, to: records.length - 1 }, "toDelete", false); 266 - setLastSelected(undefined); 267 - setBatchDelete(!batchDelete()); 268 - }} 269 - class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 270 - > 271 - <span 272 - class={`iconify ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `} 273 - ></span> 274 - </button> 275 - } 276 - /> 277 - <Show when={batchDelete()}> 254 + <> 255 + <Title>{params.collection} - PDSls</Title> 256 + <Show when={records.length || response()}> 257 + <div class="-mt-2 flex w-full flex-col items-center"> 258 + <StickyOverlay> 259 + <div class="flex w-full flex-col gap-2"> 260 + <div class="flex items-center gap-1.5"> 261 + <Show when={agent() && agent()?.sub === did && hasUserScope("delete")}> 262 + <div class="flex items-center"> 278 263 <Tooltip 279 - text="Select all" 264 + text={batchDelete() ? "Cancel" : "Delete"} 280 265 children={ 281 266 <button 282 - onclick={() => selectAll()} 267 + onclick={() => { 268 + setRecords({ from: 0, to: records.length - 1 }, "toDelete", false); 269 + setLastSelected(undefined); 270 + setBatchDelete(!batchDelete()); 271 + }} 283 272 class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 284 273 > 285 - <span class="iconify lucide--copy-check"></span> 274 + <span 275 + class={`iconify ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `} 276 + ></span> 286 277 </button> 287 278 } 288 279 /> 289 - <Show when={hasUserScope("create")}> 280 + <Show when={batchDelete()}> 281 + <Tooltip 282 + text="Select all" 283 + children={ 284 + <button 285 + onclick={() => selectAll()} 286 + class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 287 + > 288 + <span class="iconify lucide--copy-check"></span> 289 + </button> 290 + } 291 + /> 292 + <Show when={hasUserScope("create")}> 293 + <Tooltip 294 + text="Recreate" 295 + children={ 296 + <button 297 + onclick={() => { 298 + setRecreate(true); 299 + setOpenDelete(true); 300 + }} 301 + class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 302 + > 303 + <span class="iconify lucide--recycle text-green-500 dark:text-green-400"></span> 304 + </button> 305 + } 306 + /> 307 + </Show> 290 308 <Tooltip 291 - text="Recreate" 309 + text="Delete" 292 310 children={ 293 311 <button 294 312 onclick={() => { 295 - setRecreate(true); 313 + setRecreate(false); 296 314 setOpenDelete(true); 297 315 }} 298 316 class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 299 317 > 300 - <span class="iconify lucide--recycle text-green-500 dark:text-green-400"></span> 318 + <span class="iconify lucide--trash-2 text-red-500 dark:text-red-400"></span> 301 319 </button> 302 320 } 303 321 /> 304 322 </Show> 305 - <Tooltip 306 - text="Delete" 307 - children={ 308 - <button 309 - onclick={() => { 310 - setRecreate(false); 311 - setOpenDelete(true); 312 - }} 313 - class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 323 + </div> 324 + <Modal open={openDelete()} onClose={() => setOpenDelete(false)}> 325 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 326 + <h2 class="mb-2 font-semibold"> 327 + {recreate() ? "Recreate" : "Delete"}{" "} 328 + {records.filter((r) => r.toDelete).length} records? 329 + </h2> 330 + <div class="flex justify-end gap-2"> 331 + <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 332 + <Button 333 + onClick={deleteRecords} 334 + class={`dark:shadow-dark-700 rounded-lg px-2 py-1.5 text-xs text-white shadow-xs select-none ${recreate() ? "bg-green-500 hover:bg-green-400 dark:bg-green-600 dark:hover:bg-green-500" : "bg-red-500 hover:bg-red-400 active:bg-red-400"}`} 314 335 > 315 - <span class="iconify lucide--trash-2 text-red-500 dark:text-red-400"></span> 316 - </button> 317 - } 318 - /> 319 - </Show> 320 - </div> 321 - <Modal open={openDelete()} onClose={() => setOpenDelete(false)}> 322 - <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 323 - <h2 class="mb-2 font-semibold"> 324 - {recreate() ? "Recreate" : "Delete"}{" "} 325 - {records.filter((r) => r.toDelete).length} records? 326 - </h2> 327 - <div class="flex justify-end gap-2"> 328 - <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 329 - <Button 330 - onClick={deleteRecords} 331 - class={`dark:shadow-dark-700 rounded-lg px-2 py-1.5 text-xs text-white shadow-xs select-none ${recreate() ? "bg-green-500 hover:bg-green-400 dark:bg-green-600 dark:hover:bg-green-500" : "bg-red-500 hover:bg-red-400 active:bg-red-400"}`} 332 - > 333 - {recreate() ? "Recreate" : "Delete"} 334 - </Button> 336 + {recreate() ? "Recreate" : "Delete"} 337 + </Button> 338 + </div> 335 339 </div> 340 + </Modal> 341 + </Show> 342 + <TextInput 343 + name="Filter" 344 + placeholder="Filter by substring" 345 + onInput={(e) => setFilter(e.currentTarget.value)} 346 + class="grow" 347 + /> 348 + <Tooltip text="Jetstream"> 349 + <A 350 + href={`/jetstream?collections=${params.collection}&dids=${params.repo}`} 351 + class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 352 + > 353 + <span class="iconify lucide--radio-tower"></span> 354 + </A> 355 + </Tooltip> 356 + </div> 357 + <Show when={records.length > 1}> 358 + <div class="flex items-center justify-between gap-x-2"> 359 + <Button 360 + onClick={() => { 361 + setReverse(!reverse()); 362 + setCursor(undefined); 363 + clearCollectionCache(cacheKey()); 364 + refetch(); 365 + }} 366 + classList={{ 367 + "text-blue-500! dark:text-blue-400! border-blue-500! dark:border-blue-400!": 368 + reverse(), 369 + }} 370 + > 371 + <span 372 + class={`iconify ${reverse() ? "lucide--arrow-down-wide-narrow" : "lucide--arrow-up-narrow-wide"}`} 373 + ></span> 374 + Reverse 375 + </Button> 376 + <div> 377 + <Show when={batchDelete()}> 378 + <span>{records.filter((rec) => rec.toDelete).length}</span> 379 + <span>/</span> 380 + </Show> 381 + <span>{filter() ? filteredRecords().length : records.length} records</span> 336 382 </div> 337 - </Modal> 383 + <div class="flex w-20 items-center justify-end"> 384 + <Show when={cursor()}> 385 + <Show when={!response.loading}> 386 + <Button onClick={() => refetch()}>Load More</Button> 387 + </Show> 388 + <Show when={response.loading}> 389 + <div class="iconify lucide--loader-circle w-20 animate-spin text-xl" /> 390 + </Show> 391 + </Show> 392 + </div> 393 + </div> 338 394 </Show> 339 - <TextInput 340 - name="Filter" 341 - placeholder="Filter by substring" 342 - onInput={(e) => setFilter(e.currentTarget.value)} 343 - class="grow" 344 - /> 345 - <Tooltip text="Jetstream"> 346 - <A 347 - href={`/jetstream?collections=${params.collection}&dids=${params.repo}`} 348 - class="flex items-center rounded-md p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 349 - > 350 - <span class="iconify lucide--radio-tower"></span> 351 - </A> 352 - </Tooltip> 353 395 </div> 354 - <Show when={records.length > 1}> 355 - <div class="flex items-center justify-between gap-x-2"> 356 - <Button 357 - onClick={() => { 358 - setReverse(!reverse()); 359 - setCursor(undefined); 360 - clearCollectionCache(cacheKey()); 361 - refetch(); 362 - }} 363 - classList={{ 364 - "text-blue-500! dark:text-blue-400! border-blue-500! dark:border-blue-400!": 365 - reverse(), 366 - }} 367 - > 368 - <span 369 - class={`iconify ${reverse() ? "lucide--arrow-down-wide-narrow" : "lucide--arrow-up-narrow-wide"}`} 370 - ></span> 371 - Reverse 372 - </Button> 373 - <div> 374 - <Show when={batchDelete()}> 375 - <span>{records.filter((rec) => rec.toDelete).length}</span> 376 - <span>/</span> 377 - </Show> 378 - <span>{filter() ? filteredRecords().length : records.length} records</span> 379 - </div> 380 - <div class="flex w-20 items-center justify-end"> 381 - <Show when={cursor()}> 382 - <Show when={!response.loading}> 383 - <Button onClick={() => refetch()}>Load More</Button> 396 + </StickyOverlay> 397 + <div class="flex max-w-full flex-col px-2 font-mono"> 398 + <For each={filteredRecords()}> 399 + {(record, index) => { 400 + const rounding = () => { 401 + const recs = filteredRecords(); 402 + const prevSelected = recs[index() - 1]?.toDelete; 403 + const nextSelected = recs[index() + 1]?.toDelete; 404 + return `${!prevSelected ? "rounded-t" : ""} ${!nextSelected ? "rounded-b" : ""}`; 405 + }; 406 + return ( 407 + <> 408 + <Show when={batchDelete()}> 409 + <div 410 + class={`select-none ${ 411 + record.toDelete ? 412 + `bg-blue-200 hover:bg-blue-300/80 active:bg-blue-300 dark:bg-blue-700/30 dark:hover:bg-blue-700/50 dark:active:bg-blue-700/70 ${rounding()}` 413 + : "rounded hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 414 + }`} 415 + onclick={(e) => { 416 + handleSelectionClick(e, index()); 417 + setRecords(index(), "toDelete", !record.toDelete); 418 + }} 419 + > 420 + <RecordLink record={record} /> 421 + </div> 384 422 </Show> 385 - <Show when={response.loading}> 386 - <div class="iconify lucide--loader-circle w-20 animate-spin text-xl" /> 423 + <Show when={!batchDelete()}> 424 + <A 425 + href={`/at://${did}/${params.collection}/${record.rkey}`} 426 + class="rounded select-none hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 427 + > 428 + <RecordLink record={record} /> 429 + </A> 387 430 </Show> 388 - </Show> 389 - </div> 390 - </div> 391 - </Show> 431 + </> 432 + ); 433 + }} 434 + </For> 392 435 </div> 393 - </StickyOverlay> 394 - <div class="flex max-w-full flex-col px-2 font-mono"> 395 - <For each={filteredRecords()}> 396 - {(record, index) => { 397 - const rounding = () => { 398 - const recs = filteredRecords(); 399 - const prevSelected = recs[index() - 1]?.toDelete; 400 - const nextSelected = recs[index() + 1]?.toDelete; 401 - return `${!prevSelected ? "rounded-t" : ""} ${!nextSelected ? "rounded-b" : ""}`; 402 - }; 403 - return ( 404 - <> 405 - <Show when={batchDelete()}> 406 - <div 407 - class={`select-none ${ 408 - record.toDelete ? 409 - `bg-blue-200 hover:bg-blue-300/80 active:bg-blue-300 dark:bg-blue-700/30 dark:hover:bg-blue-700/50 dark:active:bg-blue-700/70 ${rounding()}` 410 - : "rounded hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 411 - }`} 412 - onclick={(e) => { 413 - handleSelectionClick(e, index()); 414 - setRecords(index(), "toDelete", !record.toDelete); 415 - }} 416 - > 417 - <RecordLink record={record} /> 418 - </div> 419 - </Show> 420 - <Show when={!batchDelete()}> 421 - <A 422 - href={`/at://${did}/${params.collection}/${record.rkey}`} 423 - class="rounded select-none hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 424 - > 425 - <RecordLink record={record} /> 426 - </A> 427 - </Show> 428 - </> 429 - ); 430 - }} 431 - </For> 432 436 </div> 433 - </div> 434 - </Show> 437 + </Show> 438 + </> 435 439 ); 436 440 }; 437 441
+114 -110
src/views/labels.tsx
··· 2 2 import { Client, simpleFetchHandler } from "@atcute/client"; 3 3 import { isAtprotoDid } from "@atcute/identity"; 4 4 import { Handle } from "@atcute/lexicons"; 5 + import { Title } from "@solidjs/meta"; 5 6 import { A, useSearchParams } from "@solidjs/router"; 6 7 import { createMemo, createSignal, For, onMount, Show } from "solid-js"; 7 8 import { Button } from "../components/button.jsx"; ··· 194 195 }; 195 196 196 197 return ( 197 - <div class="flex w-full flex-col items-center"> 198 - <form 199 - ref={formRef} 200 - class="flex w-full max-w-3xl flex-col gap-y-2 px-3 pb-2" 201 - onSubmit={(e) => { 202 - e.preventDefault(); 203 - handleSearch(); 204 - }} 205 - > 206 - <div class="flex flex-col gap-y-1.5"> 207 - <label class="flex w-full flex-col gap-y-1"> 208 - <span class="text-sm font-medium text-neutral-700 dark:text-neutral-300"> 209 - Labeler DID/Handle 210 - </span> 211 - <TextInput 212 - name="did" 213 - value={didInput()} 214 - onInput={(e) => setDidInput(e.currentTarget.value)} 215 - placeholder="did:plc:..." 216 - class="w-full" 217 - /> 218 - </label> 219 - 220 - <label class="flex w-full flex-col gap-y-1"> 221 - <span class="text-sm font-medium text-neutral-700 dark:text-neutral-300"> 222 - URI Patterns (comma-separated) 223 - </span> 224 - <textarea 225 - id="uriPatterns" 226 - name="uriPatterns" 227 - spellcheck={false} 228 - rows={2} 229 - value={searchParams.uriPatterns ?? "*"} 230 - placeholder="at://did:web:example.com/app.bsky.feed.post/*" 231 - class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1.5 text-sm outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400" 232 - /> 233 - </label> 234 - </div> 235 - 236 - <Button 237 - type="submit" 238 - disabled={loading()} 239 - class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 w-fit items-center justify-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 198 + <> 199 + <Title>Labels - PDSls</Title> 200 + <div class="flex w-full flex-col items-center"> 201 + <form 202 + ref={formRef} 203 + class="flex w-full max-w-3xl flex-col gap-y-2 px-3 pb-2" 204 + onSubmit={(e) => { 205 + e.preventDefault(); 206 + handleSearch(); 207 + }} 240 208 > 241 - <span class="iconify lucide--search" /> 242 - <span>Search Labels</span> 243 - </Button> 209 + <div class="flex flex-col gap-y-1.5"> 210 + <label class="flex w-full flex-col gap-y-1"> 211 + <span class="text-sm font-medium text-neutral-700 dark:text-neutral-300"> 212 + Labeler DID/Handle 213 + </span> 214 + <TextInput 215 + name="did" 216 + value={didInput()} 217 + onInput={(e) => setDidInput(e.currentTarget.value)} 218 + placeholder="did:plc:..." 219 + class="w-full" 220 + /> 221 + </label> 244 222 245 - <Show when={error()}> 246 - <div class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800 dark:border-red-800 dark:bg-red-900/20 dark:text-red-300"> 247 - {error()} 223 + <label class="flex w-full flex-col gap-y-1"> 224 + <span class="text-sm font-medium text-neutral-700 dark:text-neutral-300"> 225 + URI Patterns (comma-separated) 226 + </span> 227 + <textarea 228 + id="uriPatterns" 229 + name="uriPatterns" 230 + spellcheck={false} 231 + rows={2} 232 + value={searchParams.uriPatterns ?? "*"} 233 + placeholder="at://did:web:example.com/app.bsky.feed.post/*" 234 + class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1.5 text-sm outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400" 235 + /> 236 + </label> 248 237 </div> 249 - </Show> 250 - </form> 251 238 252 - <Show when={hasSearched()}> 253 - <StickyOverlay> 254 - <div class="flex w-full items-center gap-x-2"> 255 - <TextInput 256 - placeholder="Filter labels (* for partial, -exclude)" 257 - name="filter" 258 - value={filter()} 259 - onInput={(e) => setFilter(e.currentTarget.value)} 260 - class="min-w-0 grow text-sm placeholder:text-xs" 261 - /> 262 - <div class="flex shrink-0 items-center gap-x-2 text-sm"> 263 - <Show when={labels().length > 0}> 264 - <span class="whitespace-nowrap text-neutral-600 dark:text-neutral-400"> 265 - {filteredLabels().length}/{labels().length} 266 - </span> 267 - </Show> 239 + <Button 240 + type="submit" 241 + disabled={loading()} 242 + class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 w-fit items-center justify-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 243 + > 244 + <span class="iconify lucide--search" /> 245 + <span>Search Labels</span> 246 + </Button> 268 247 269 - <Show when={cursor()}> 270 - <Button 271 - onClick={handleLoadMore} 272 - disabled={loading()} 273 - class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 w-20 items-center justify-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 274 - > 275 - <Show 276 - when={!loading()} 277 - fallback={<span class="iconify lucide--loader-circle animate-spin" />} 278 - > 279 - Load More 280 - </Show> 281 - </Button> 282 - </Show> 248 + <Show when={error()}> 249 + <div class="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800 dark:border-red-800 dark:bg-red-900/20 dark:text-red-300"> 250 + {error()} 283 251 </div> 284 - </div> 285 - </StickyOverlay> 252 + </Show> 253 + </form> 286 254 287 - <div class="w-full max-w-3xl px-3 py-2"> 288 - <Show when={loading() && labels().length === 0}> 289 - <div class="flex flex-col items-center justify-center py-12 text-center"> 290 - <span class="iconify lucide--loader-circle mb-3 animate-spin text-4xl text-neutral-400" /> 291 - <p class="text-sm text-neutral-600 dark:text-neutral-400">Loading labels...</p> 255 + <Show when={hasSearched()}> 256 + <StickyOverlay> 257 + <div class="flex w-full items-center gap-x-2"> 258 + <TextInput 259 + placeholder="Filter labels (* for partial, -exclude)" 260 + name="filter" 261 + value={filter()} 262 + onInput={(e) => setFilter(e.currentTarget.value)} 263 + class="min-w-0 grow text-sm placeholder:text-xs" 264 + /> 265 + <div class="flex shrink-0 items-center gap-x-2 text-sm"> 266 + <Show when={labels().length > 0}> 267 + <span class="whitespace-nowrap text-neutral-600 dark:text-neutral-400"> 268 + {filteredLabels().length}/{labels().length} 269 + </span> 270 + </Show> 271 + 272 + <Show when={cursor()}> 273 + <Button 274 + onClick={handleLoadMore} 275 + disabled={loading()} 276 + class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 w-20 items-center justify-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 277 + > 278 + <Show 279 + when={!loading()} 280 + fallback={<span class="iconify lucide--loader-circle animate-spin" />} 281 + > 282 + Load More 283 + </Show> 284 + </Button> 285 + </Show> 286 + </div> 292 287 </div> 293 - </Show> 288 + </StickyOverlay> 294 289 295 - <Show when={!loading() || labels().length > 0}> 296 - <Show when={filteredLabels().length > 0}> 297 - <div class="grid gap-2"> 298 - <For each={filteredLabels()}>{(label) => <LabelCard label={label} />}</For> 290 + <div class="w-full max-w-3xl px-3 py-2"> 291 + <Show when={loading() && labels().length === 0}> 292 + <div class="flex flex-col items-center justify-center py-12 text-center"> 293 + <span class="iconify lucide--loader-circle mb-3 animate-spin text-4xl text-neutral-400" /> 294 + <p class="text-sm text-neutral-600 dark:text-neutral-400">Loading labels...</p> 299 295 </div> 300 296 </Show> 301 297 302 - <Show when={labels().length > 0 && filteredLabels().length === 0}> 303 - <div class="flex flex-col items-center justify-center py-8 text-center"> 304 - <span class="iconify lucide--search-x mb-2 text-3xl text-neutral-400" /> 305 - <p class="text-sm text-neutral-600 dark:text-neutral-400"> 306 - No labels match your filter 307 - </p> 308 - </div> 309 - </Show> 298 + <Show when={!loading() || labels().length > 0}> 299 + <Show when={filteredLabels().length > 0}> 300 + <div class="grid gap-2"> 301 + <For each={filteredLabels()}>{(label) => <LabelCard label={label} />}</For> 302 + </div> 303 + </Show> 304 + 305 + <Show when={labels().length > 0 && filteredLabels().length === 0}> 306 + <div class="flex flex-col items-center justify-center py-8 text-center"> 307 + <span class="iconify lucide--search-x mb-2 text-3xl text-neutral-400" /> 308 + <p class="text-sm text-neutral-600 dark:text-neutral-400"> 309 + No labels match your filter 310 + </p> 311 + </div> 312 + </Show> 310 313 311 - <Show when={labels().length === 0 && !loading()}> 312 - <div class="flex flex-col items-center justify-center py-8 text-center"> 313 - <span class="iconify lucide--tags mb-2 text-3xl text-neutral-400" /> 314 - <p class="text-sm text-neutral-600 dark:text-neutral-400">No labels found</p> 315 - </div> 314 + <Show when={labels().length === 0 && !loading()}> 315 + <div class="flex flex-col items-center justify-center py-8 text-center"> 316 + <span class="iconify lucide--tags mb-2 text-3xl text-neutral-400" /> 317 + <p class="text-sm text-neutral-600 dark:text-neutral-400">No labels found</p> 318 + </div> 319 + </Show> 316 320 </Show> 317 - </Show> 318 - </div> 319 - </Show> 320 - </div> 321 + </div> 322 + </Show> 323 + </div> 324 + </> 321 325 ); 322 326 };
+105 -98
src/views/pds.tsx
··· 2 2 import { Client, simpleFetchHandler } from "@atcute/client"; 3 3 import { InferXRPCBodyOutput } from "@atcute/lexicons"; 4 4 import * as TID from "@atcute/tid"; 5 + import { Title } from "@solidjs/meta"; 5 6 import { A, useLocation, useParams } from "@solidjs/router"; 6 7 import { createResource, createSignal, For, Show } from "solid-js"; 7 8 import { Button } from "../components/button"; ··· 155 156 ); 156 157 157 158 return ( 158 - <Show when={repos() || response()}> 159 - <div class="flex w-full flex-col px-2"> 160 - <div class="mb-3 flex gap-4 text-sm sm:text-base"> 161 - <Tab tab="repos" label="Repositories" /> 162 - <Tab tab="info" label="Info" /> 163 - <Tab tab="firehose" label="Firehose" /> 164 - </div> 165 - <Show when={!location.hash || location.hash === "#repos"}> 166 - <div class="flex flex-col divide-y-[0.5px] divide-neutral-300 pb-20 dark:divide-neutral-700"> 167 - <For each={repos()}>{(repo) => <RepoCard {...repo} />}</For> 159 + <> 160 + <Title>{params.pds} - PDSls</Title> 161 + <Show when={repos() || response()}> 162 + <div class="flex w-full flex-col px-2"> 163 + <div class="mb-3 flex gap-4 text-sm sm:text-base"> 164 + <Tab tab="repos" label="Repositories" /> 165 + <Tab tab="info" label="Info" /> 166 + <Tab tab="firehose" label="Firehose" /> 168 167 </div> 169 - </Show> 170 - <div class="flex flex-col gap-2"> 171 - <Show when={location.hash === "#info"}> 172 - <Show when={version()}> 173 - {(version) => ( 174 - <div class="flex flex-col"> 175 - <span class="font-semibold">Version</span> 176 - <span class="text-sm text-neutral-700 dark:text-neutral-300">{version()}</span> 177 - </div> 178 - )} 179 - </Show> 180 - <Show when={serverInfos()}> 181 - {(server) => ( 182 - <> 168 + <Show when={!location.hash || location.hash === "#repos"}> 169 + <div class="flex flex-col divide-y-[0.5px] divide-neutral-300 pb-20 dark:divide-neutral-700"> 170 + <For each={repos()}>{(repo) => <RepoCard {...repo} />}</For> 171 + </div> 172 + </Show> 173 + <div class="flex flex-col gap-2"> 174 + <Show when={location.hash === "#info"}> 175 + <Show when={version()}> 176 + {(version) => ( 183 177 <div class="flex flex-col"> 184 - <span class="font-semibold">DID</span> 185 - <span class="text-sm">{server().did}</span> 178 + <span class="font-semibold">Version</span> 179 + <span class="text-sm text-neutral-700 dark:text-neutral-300">{version()}</span> 186 180 </div> 187 - <div class="flex items-center gap-1"> 188 - <span class="font-semibold">Invite Code Required</span> 189 - <span 190 - classList={{ 191 - "iconify lucide--check text-green-500 dark:text-green-400": 192 - server().inviteCodeRequired === true, 193 - "iconify lucide--x text-red-500 dark:text-red-400": 194 - !server().inviteCodeRequired, 195 - }} 196 - ></span> 197 - </div> 198 - <Show when={server().phoneVerificationRequired}> 199 - <div class="flex items-center gap-1"> 200 - <span class="font-semibold">Phone Verification Required</span> 201 - <span class="iconify lucide--check text-green-500 dark:text-green-400"></span> 202 - </div> 203 - </Show> 204 - <Show when={server().availableUserDomains.length}> 205 - <div class="flex flex-col"> 206 - <span class="font-semibold">Available User Domains</span> 207 - <For each={server().availableUserDomains}> 208 - {(domain) => <span class="text-sm wrap-anywhere">{domain}</span>} 209 - </For> 210 - </div> 211 - </Show> 212 - <Show when={server().links?.privacyPolicy}> 181 + )} 182 + </Show> 183 + <Show when={serverInfos()}> 184 + {(server) => ( 185 + <> 213 186 <div class="flex flex-col"> 214 - <span class="font-semibold">Privacy Policy</span> 215 - <a 216 - href={server().links?.privacyPolicy} 217 - class="text-sm hover:underline" 218 - target="_blank" 219 - rel="noopener" 220 - > 221 - {server().links?.privacyPolicy} 222 - </a> 187 + <span class="font-semibold">DID</span> 188 + <span class="text-sm">{server().did}</span> 223 189 </div> 224 - </Show> 225 - <Show when={server().links?.termsOfService}> 226 - <div class="flex flex-col"> 227 - <span class="font-semibold">Terms of Service</span> 228 - <a 229 - href={server().links?.termsOfService} 230 - class="text-sm hover:underline" 231 - target="_blank" 232 - rel="noopener" 233 - > 234 - {server().links?.termsOfService} 235 - </a> 236 - </div> 237 - </Show> 238 - <Show when={server().contact?.email}> 239 - <div class="flex flex-col"> 240 - <span class="font-semibold">Contact</span> 241 - <a href={`mailto:${server().contact?.email}`} class="text-sm hover:underline"> 242 - {server().contact?.email} 243 - </a> 190 + <div class="flex items-center gap-1"> 191 + <span class="font-semibold">Invite Code Required</span> 192 + <span 193 + classList={{ 194 + "iconify lucide--check text-green-500 dark:text-green-400": 195 + server().inviteCodeRequired === true, 196 + "iconify lucide--x text-red-500 dark:text-red-400": 197 + !server().inviteCodeRequired, 198 + }} 199 + ></span> 244 200 </div> 245 - </Show> 246 - </> 247 - )} 248 - </Show> 249 - </Show> 250 - </div> 251 - </div> 252 - <Show when={!location.hash || location.hash === "#repos"}> 253 - <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 pt-2 pb-4"> 254 - <div class="flex flex-col items-center gap-1 pb-2"> 255 - <p>{repos()?.length} loaded</p> 256 - <Show when={!response.loading && cursor()}> 257 - <Button onClick={() => refetch()}>Load More</Button> 258 - </Show> 259 - <Show when={response.loading}> 260 - <span class="iconify lucide--loader-circle animate-spin py-3.5 text-xl"></span> 201 + <Show when={server().phoneVerificationRequired}> 202 + <div class="flex items-center gap-1"> 203 + <span class="font-semibold">Phone Verification Required</span> 204 + <span class="iconify lucide--check text-green-500 dark:text-green-400"></span> 205 + </div> 206 + </Show> 207 + <Show when={server().availableUserDomains.length}> 208 + <div class="flex flex-col"> 209 + <span class="font-semibold">Available User Domains</span> 210 + <For each={server().availableUserDomains}> 211 + {(domain) => <span class="text-sm wrap-anywhere">{domain}</span>} 212 + </For> 213 + </div> 214 + </Show> 215 + <Show when={server().links?.privacyPolicy}> 216 + <div class="flex flex-col"> 217 + <span class="font-semibold">Privacy Policy</span> 218 + <a 219 + href={server().links?.privacyPolicy} 220 + class="text-sm hover:underline" 221 + target="_blank" 222 + rel="noopener" 223 + > 224 + {server().links?.privacyPolicy} 225 + </a> 226 + </div> 227 + </Show> 228 + <Show when={server().links?.termsOfService}> 229 + <div class="flex flex-col"> 230 + <span class="font-semibold">Terms of Service</span> 231 + <a 232 + href={server().links?.termsOfService} 233 + class="text-sm hover:underline" 234 + target="_blank" 235 + rel="noopener" 236 + > 237 + {server().links?.termsOfService} 238 + </a> 239 + </div> 240 + </Show> 241 + <Show when={server().contact?.email}> 242 + <div class="flex flex-col"> 243 + <span class="font-semibold">Contact</span> 244 + <a 245 + href={`mailto:${server().contact?.email}`} 246 + class="text-sm hover:underline" 247 + > 248 + {server().contact?.email} 249 + </a> 250 + </div> 251 + </Show> 252 + </> 253 + )} 254 + </Show> 261 255 </Show> 262 256 </div> 263 257 </div> 258 + <Show when={!location.hash || location.hash === "#repos"}> 259 + <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 pt-2 pb-4"> 260 + <div class="flex flex-col items-center gap-1 pb-2"> 261 + <p>{repos()?.length} loaded</p> 262 + <Show when={!response.loading && cursor()}> 263 + <Button onClick={() => refetch()}>Load More</Button> 264 + </Show> 265 + <Show when={response.loading}> 266 + <span class="iconify lucide--loader-circle animate-spin py-3.5 text-xl"></span> 267 + </Show> 268 + </div> 269 + </div> 270 + </Show> 264 271 </Show> 265 - </Show> 272 + </> 266 273 ); 267 274 }; 268 275
+176 -170
src/views/record.tsx
··· 6 6 import { ActorIdentifier, is, Nsid } from "@atcute/lexicons"; 7 7 import { AtprotoDid, Did, isNsid } from "@atcute/lexicons/syntax"; 8 8 import { verifyRecord } from "@atcute/repo"; 9 + import { Title } from "@solidjs/meta"; 9 10 import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; 10 11 import { createResource, createSignal, ErrorBoundary, Show, Suspense } from "solid-js"; 11 12 import { hasUserScope } from "../auth/scope-utils"; ··· 379 380 }; 380 381 381 382 return ( 382 - <Show when={record()} keyed> 383 - <div class="flex w-full flex-col items-center"> 384 - <div class="mb-3 flex w-full justify-between px-2 text-sm sm:text-base"> 385 - <div class="flex items-center gap-4"> 386 - <RecordTab tab="record" label="Record" /> 387 - <RecordTab tab="schema" label="Schema" /> 388 - <RecordTab tab="backlinks" label="Backlinks" /> 389 - <RecordTab tab="info" label="Info" error /> 390 - </div> 391 - <div class="flex gap-0.5"> 392 - <Show when={agent() && agent()?.sub === record()?.uri.split("/")[2]}> 393 - <Show when={hasUserScope("update")}> 394 - <RecordEditor create={false} record={record()?.value} refetch={refetch} /> 395 - </Show> 396 - <Show when={hasUserScope("delete")}> 397 - <Tooltip text="Delete"> 398 - <button 399 - class="flex items-center rounded-sm p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 400 - onclick={() => setOpenDelete(true)} 401 - > 402 - <span class="iconify lucide--trash-2"></span> 403 - </button> 404 - </Tooltip> 405 - <Modal open={openDelete()} onClose={() => setOpenDelete(false)}> 406 - <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 407 - <h2 class="mb-2 font-semibold">Delete this record?</h2> 408 - <div class="flex justify-end gap-2"> 409 - <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 410 - <Button 411 - onClick={deleteRecord} 412 - class="dark:shadow-dark-700 rounded-lg bg-red-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-red-400 active:bg-red-400" 413 - > 414 - Delete 415 - </Button> 383 + <> 384 + <Title> 385 + {params.collection}/{params.rkey} - PDSls 386 + </Title> 387 + <Show when={record()} keyed> 388 + <div class="flex w-full flex-col items-center"> 389 + <div class="mb-3 flex w-full justify-between px-2 text-sm sm:text-base"> 390 + <div class="flex items-center gap-4"> 391 + <RecordTab tab="record" label="Record" /> 392 + <RecordTab tab="schema" label="Schema" /> 393 + <RecordTab tab="backlinks" label="Backlinks" /> 394 + <RecordTab tab="info" label="Info" error /> 395 + </div> 396 + <div class="flex gap-0.5"> 397 + <Show when={agent() && agent()?.sub === record()?.uri.split("/")[2]}> 398 + <Show when={hasUserScope("update")}> 399 + <RecordEditor create={false} record={record()?.value} refetch={refetch} /> 400 + </Show> 401 + <Show when={hasUserScope("delete")}> 402 + <Tooltip text="Delete"> 403 + <button 404 + class="flex items-center rounded-sm p-1.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 405 + onclick={() => setOpenDelete(true)} 406 + > 407 + <span class="iconify lucide--trash-2"></span> 408 + </button> 409 + </Tooltip> 410 + <Modal open={openDelete()} onClose={() => setOpenDelete(false)}> 411 + <div class="dark:bg-dark-300 dark:shadow-dark-700 absolute top-70 left-[50%] -translate-x-1/2 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md transition-opacity duration-200 dark:border-neutral-700 starting:opacity-0"> 412 + <h2 class="mb-2 font-semibold">Delete this record?</h2> 413 + <div class="flex justify-end gap-2"> 414 + <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 415 + <Button 416 + onClick={deleteRecord} 417 + class="dark:shadow-dark-700 rounded-lg bg-red-500 px-2 py-1.5 text-xs text-white shadow-xs select-none hover:bg-red-400 active:bg-red-400" 418 + > 419 + Delete 420 + </Button> 421 + </div> 416 422 </div> 417 - </div> 418 - </Modal> 423 + </Modal> 424 + </Show> 419 425 </Show> 420 - </Show> 421 - <MenuProvider> 422 - <DropdownMenu icon="lucide--ellipsis" buttonClass="rounded-sm p-1.5"> 423 - <CopyMenu 424 - content={JSON.stringify(record()?.value, null, 2)} 425 - label="Copy record" 426 - icon="lucide--copy" 427 - /> 428 - <CopyMenu 429 - content={`at://${params.repo}/${params.collection}/${params.rkey}`} 430 - label="Copy AT URI" 431 - icon="lucide--copy" 432 - /> 433 - <Show when={record()?.cid}> 434 - {(cid) => <CopyMenu content={cid()} label="Copy CID" icon="lucide--copy" />} 435 - </Show> 436 - <MenuSeparator /> 437 - <Show when={externalLink()}> 438 - {(externalLink) => ( 439 - <NavMenu 440 - href={externalLink()?.link} 441 - icon={`${externalLink().icon ?? "lucide--app-window"}`} 442 - label={`Open on ${externalLink().label}`} 443 - newTab 444 - /> 445 - )} 446 - </Show> 447 - <NavMenu 448 - href={`https://${pds()}/xrpc/com.atproto.repo.getRecord?repo=${params.repo}&collection=${params.collection}&rkey=${params.rkey}`} 449 - icon="lucide--external-link" 450 - label="Record on PDS" 451 - newTab 452 - /> 453 - </DropdownMenu> 454 - </MenuProvider> 455 - </div> 456 - </div> 457 - <Show when={!location.hash || location.hash === "#record"}> 458 - <div class="w-max max-w-screen min-w-full px-4 font-mono text-xs wrap-anywhere whitespace-pre-wrap sm:px-2 sm:text-sm md:max-w-3xl"> 459 - <JSONValue data={record()?.value as any} repo={record()!.uri.split("/")[2]} /> 426 + <MenuProvider> 427 + <DropdownMenu icon="lucide--ellipsis" buttonClass="rounded-sm p-1.5"> 428 + <CopyMenu 429 + content={JSON.stringify(record()?.value, null, 2)} 430 + label="Copy record" 431 + icon="lucide--copy" 432 + /> 433 + <CopyMenu 434 + content={`at://${params.repo}/${params.collection}/${params.rkey}`} 435 + label="Copy AT URI" 436 + icon="lucide--copy" 437 + /> 438 + <Show when={record()?.cid}> 439 + {(cid) => <CopyMenu content={cid()} label="Copy CID" icon="lucide--copy" />} 440 + </Show> 441 + <MenuSeparator /> 442 + <Show when={externalLink()}> 443 + {(externalLink) => ( 444 + <NavMenu 445 + href={externalLink()?.link} 446 + icon={`${externalLink().icon ?? "lucide--app-window"}`} 447 + label={`Open on ${externalLink().label}`} 448 + newTab 449 + /> 450 + )} 451 + </Show> 452 + <NavMenu 453 + href={`https://${pds()}/xrpc/com.atproto.repo.getRecord?repo=${params.repo}&collection=${params.collection}&rkey=${params.rkey}`} 454 + icon="lucide--external-link" 455 + label="Record on PDS" 456 + newTab 457 + /> 458 + </DropdownMenu> 459 + </MenuProvider> 460 + </div> 460 461 </div> 461 - </Show> 462 - <Show when={location.hash === "#schema" || location.hash.startsWith("#schema:")}> 463 - <Show when={lexiconNotFound() === true}> 464 - <span class="w-full px-2 text-sm">Lexicon schema could not be resolved.</span> 462 + <Show when={!location.hash || location.hash === "#record"}> 463 + <div class="w-max max-w-screen min-w-full px-4 font-mono text-xs wrap-anywhere whitespace-pre-wrap sm:px-2 sm:text-sm md:max-w-3xl"> 464 + <JSONValue data={record()?.value as any} repo={record()!.uri.split("/")[2]} /> 465 + </div> 465 466 </Show> 466 - <Show when={lexiconNotFound() === undefined}> 467 - <span class="w-full px-2 text-sm">Resolving lexicon schema...</span> 467 + <Show when={location.hash === "#schema" || location.hash.startsWith("#schema:")}> 468 + <Show when={lexiconNotFound() === true}> 469 + <span class="w-full px-2 text-sm">Lexicon schema could not be resolved.</span> 470 + </Show> 471 + <Show when={lexiconNotFound() === undefined}> 472 + <span class="w-full px-2 text-sm">Resolving lexicon schema...</span> 473 + </Show> 474 + <Show when={schema() || params.collection === "com.atproto.lexicon.schema"}> 475 + <ErrorBoundary fallback={(err) => <div>Error: {err.message}</div>}> 476 + <LexiconSchemaView schema={schema()?.rawSchema ?? (record()?.value as any)} /> 477 + </ErrorBoundary> 478 + </Show> 468 479 </Show> 469 - <Show when={schema() || params.collection === "com.atproto.lexicon.schema"}> 470 - <ErrorBoundary fallback={(err) => <div>Error: {err.message}</div>}> 471 - <LexiconSchemaView schema={schema()?.rawSchema ?? (record()?.value as any)} /> 480 + <Show when={location.hash === "#backlinks"}> 481 + <ErrorBoundary 482 + fallback={(err) => <div class="wrap-break-word">Error: {err.message}</div>} 483 + > 484 + <Suspense 485 + fallback={ 486 + <div class="iconify lucide--loader-circle animate-spin self-center text-xl" /> 487 + } 488 + > 489 + <div class="w-full px-2"> 490 + <Backlinks target={`at://${did}/${params.collection}/${params.rkey}`} /> 491 + </div> 492 + </Suspense> 472 493 </ErrorBoundary> 473 494 </Show> 474 - </Show> 475 - <Show when={location.hash === "#backlinks"}> 476 - <ErrorBoundary 477 - fallback={(err) => <div class="wrap-break-word">Error: {err.message}</div>} 478 - > 479 - <Suspense 480 - fallback={ 481 - <div class="iconify lucide--loader-circle animate-spin self-center text-xl" /> 482 - } 483 - > 484 - <div class="w-full px-2"> 485 - <Backlinks target={`at://${did}/${params.collection}/${params.rkey}`} /> 486 - </div> 487 - </Suspense> 488 - </ErrorBoundary> 489 - </Show> 490 - <Show when={location.hash === "#info"}> 491 - <div class="flex w-full flex-col gap-2 px-2 text-sm"> 492 - <div> 493 - <p class="font-semibold">AT URI</p> 494 - <div class="truncate text-xs">{record()?.uri}</div> 495 - </div> 496 - <Show when={record()?.cid}> 495 + <Show when={location.hash === "#info"}> 496 + <div class="flex w-full flex-col gap-2 px-2 text-sm"> 497 497 <div> 498 - <p class="font-semibold">CID</p> 499 - <div class="truncate text-left text-xs" dir="rtl"> 500 - {record()?.cid} 501 - </div> 498 + <p class="font-semibold">AT URI</p> 499 + <div class="truncate text-xs">{record()?.uri}</div> 502 500 </div> 503 - </Show> 504 - <div> 505 - <div class="flex items-center gap-1"> 506 - <p class="font-semibold">Record verification</p> 507 - <span 508 - classList={{ 509 - "iconify lucide--check text-green-500 dark:text-green-400": 510 - validRecord() === true, 511 - "iconify lucide--x text-red-500 dark:text-red-400": validRecord() === false, 512 - "iconify lucide--loader-circle animate-spin": validRecord() === undefined, 513 - }} 514 - ></span> 515 - </div> 516 - <Show when={validRecord() === false}> 517 - <div class="text-xs wrap-break-word">{verifyError()}</div> 501 + <Show when={record()?.cid}> 502 + <div> 503 + <p class="font-semibold">CID</p> 504 + <div class="truncate text-left text-xs" dir="rtl"> 505 + {record()?.cid} 506 + </div> 507 + </div> 518 508 </Show> 519 - </div> 520 - <div> 521 - <div class="flex items-center gap-1"> 522 - <p class="font-semibold">Schema validation</p> 523 - <span 524 - classList={{ 525 - "iconify lucide--check text-green-500 dark:text-green-400": 526 - validSchema() === true, 527 - "iconify lucide--x text-red-500 dark:text-red-400": validSchema() === false, 528 - "iconify lucide--loader-circle animate-spin": 529 - validSchema() === undefined && remoteValidation(), 530 - }} 531 - ></span> 509 + <div> 510 + <div class="flex items-center gap-1"> 511 + <p class="font-semibold">Record verification</p> 512 + <span 513 + classList={{ 514 + "iconify lucide--check text-green-500 dark:text-green-400": 515 + validRecord() === true, 516 + "iconify lucide--x text-red-500 dark:text-red-400": validRecord() === false, 517 + "iconify lucide--loader-circle animate-spin": validRecord() === undefined, 518 + }} 519 + ></span> 520 + </div> 521 + <Show when={validRecord() === false}> 522 + <div class="text-xs wrap-break-word">{verifyError()}</div> 523 + </Show> 532 524 </div> 533 - <Show when={validSchema() === false}> 534 - <div class="text-xs wrap-break-word">{validationError()}</div> 535 - </Show> 536 - <Show 537 - when={ 538 - !remoteValidation() && 539 - validSchema() === undefined && 540 - params.collection && 541 - !(params.collection in lexicons) 542 - } 543 - > 544 - <Button onClick={() => validateRemoteSchema(record()!.value)}> 545 - Validate via resolution 546 - </Button> 547 - </Show> 548 - </div> 549 - <Show when={lexiconUri()}> 550 525 <div> 551 - <p class="font-semibold">Lexicon schema</p> 552 - <div class="truncate text-xs"> 553 - <A 554 - href={`/${lexiconUri()}`} 555 - class="text-blue-400 hover:underline active:underline" 556 - > 557 - {lexiconUri()} 558 - </A> 526 + <div class="flex items-center gap-1"> 527 + <p class="font-semibold">Schema validation</p> 528 + <span 529 + classList={{ 530 + "iconify lucide--check text-green-500 dark:text-green-400": 531 + validSchema() === true, 532 + "iconify lucide--x text-red-500 dark:text-red-400": validSchema() === false, 533 + "iconify lucide--loader-circle animate-spin": 534 + validSchema() === undefined && remoteValidation(), 535 + }} 536 + ></span> 559 537 </div> 538 + <Show when={validSchema() === false}> 539 + <div class="text-xs wrap-break-word">{validationError()}</div> 540 + </Show> 541 + <Show 542 + when={ 543 + !remoteValidation() && 544 + validSchema() === undefined && 545 + params.collection && 546 + !(params.collection in lexicons) 547 + } 548 + > 549 + <Button onClick={() => validateRemoteSchema(record()!.value)}> 550 + Validate via resolution 551 + </Button> 552 + </Show> 560 553 </div> 561 - </Show> 562 - </div> 563 - </Show> 564 - </div> 565 - </Show> 554 + <Show when={lexiconUri()}> 555 + <div> 556 + <p class="font-semibold">Lexicon schema</p> 557 + <div class="truncate text-xs"> 558 + <A 559 + href={`/${lexiconUri()}`} 560 + class="text-blue-400 hover:underline active:underline" 561 + > 562 + {lexiconUri()} 563 + </A> 564 + </div> 565 + </div> 566 + </Show> 567 + </div> 568 + </Show> 569 + </div> 570 + </Show> 571 + </> 566 572 ); 567 573 };
+311 -292
src/views/repo.tsx
··· 1 1 import { Client, simpleFetchHandler } from "@atcute/client"; 2 2 import { DidDocument } from "@atcute/identity"; 3 3 import { ActorIdentifier, Did, Handle, Nsid } from "@atcute/lexicons"; 4 + import { Title } from "@solidjs/meta"; 4 5 import { A, useLocation, useNavigate, useParams } from "@solidjs/router"; 5 6 import { 6 7 createEffect, ··· 273 274 setDownloading(false); 274 275 }; 275 276 277 + const getTitle = () => { 278 + const doc = didDoc(); 279 + const handle = doc?.alsoKnownAs 280 + ?.find((alias) => alias.startsWith("at://")) 281 + ?.replace("at://", ""); 282 + return `${handle || params.repo} - PDSls`; 283 + }; 284 + 276 285 return ( 277 - <Show when={repo()}> 278 - <div class="flex w-full flex-col gap-3 wrap-break-word"> 279 - <div class="flex justify-between px-2 text-sm sm:text-base"> 280 - <div class="flex items-center gap-3 sm:gap-4"> 281 - <Show when={!error()}> 282 - <RepoTab tab="collections" label="Collections" /> 283 - </Show> 284 - <RepoTab tab="identity" label="Identity" /> 285 - <Show when={did.startsWith("did:plc")}> 286 - <RepoTab tab="logs" label="Logs" /> 287 - </Show> 288 - <Show when={!error()}> 289 - <RepoTab tab="blobs" label="Blobs" /> 290 - </Show> 291 - <RepoTab tab="backlinks" label="Backlinks" /> 292 - </div> 293 - <div class="flex gap-1"> 294 - <Show when={error() && error() !== "Missing PDS"}> 295 - <div class="flex items-center gap-1 text-red-500 dark:text-red-400"> 296 - <span class="iconify lucide--alert-triangle"></span> 297 - <span>{error()}</span> 298 - </div> 299 - </Show> 300 - <MenuProvider> 301 - <DropdownMenu icon="lucide--ellipsis" buttonClass="rounded-sm p-1.5"> 302 - <Show 303 - when={!error() && (!location.hash || location.hash.startsWith("#collections"))} 304 - > 305 - <ActionMenu 306 - label="Filter collections" 307 - icon="lucide--filter" 308 - onClick={() => setShowFilter(!showFilter())} 309 - /> 310 - </Show> 311 - <CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" /> 312 - <NavMenu 313 - href={`/jetstream?dids=${params.repo}`} 314 - label="Jetstream" 315 - icon="lucide--radio-tower" 316 - /> 317 - <Show when={params.repo && params.repo in labelerCache}> 286 + <> 287 + <Title>{getTitle()}</Title> 288 + <Show when={repo()}> 289 + <div class="flex w-full flex-col gap-3 wrap-break-word"> 290 + <div class="flex justify-between px-2 text-sm sm:text-base"> 291 + <div class="flex items-center gap-3 sm:gap-4"> 292 + <Show when={!error()}> 293 + <RepoTab tab="collections" label="Collections" /> 294 + </Show> 295 + <RepoTab tab="identity" label="Identity" /> 296 + <Show when={did.startsWith("did:plc")}> 297 + <RepoTab tab="logs" label="Logs" /> 298 + </Show> 299 + <Show when={!error()}> 300 + <RepoTab tab="blobs" label="Blobs" /> 301 + </Show> 302 + <RepoTab tab="backlinks" label="Backlinks" /> 303 + </div> 304 + <div class="flex gap-1"> 305 + <Show when={error() && error() !== "Missing PDS"}> 306 + <div class="flex items-center gap-1 text-red-500 dark:text-red-400"> 307 + <span class="iconify lucide--alert-triangle"></span> 308 + <span>{error()}</span> 309 + </div> 310 + </Show> 311 + <MenuProvider> 312 + <DropdownMenu icon="lucide--ellipsis" buttonClass="rounded-sm p-1.5"> 313 + <Show 314 + when={!error() && (!location.hash || location.hash.startsWith("#collections"))} 315 + > 316 + <ActionMenu 317 + label="Filter collections" 318 + icon="lucide--filter" 319 + onClick={() => setShowFilter(!showFilter())} 320 + /> 321 + </Show> 322 + <CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" /> 318 323 <NavMenu 319 - href={`/labels?did=${params.repo}&uriPatterns=*`} 320 - label="Labels" 321 - icon="lucide--tag" 324 + href={`/jetstream?dids=${params.repo}`} 325 + label="Jetstream" 326 + icon="lucide--radio-tower" 322 327 /> 323 - </Show> 324 - <Show when={error()?.length === 0 || error() === undefined}> 325 - <ActionMenu 326 - label="Export repo" 327 - icon={downloading() ? "lucide--loader-circle animate-spin" : "lucide--download"} 328 - onClick={() => downloadRepo()} 329 - /> 330 - </Show> 331 - <MenuSeparator /> 332 - <NavMenu 333 - href={ 334 - did.startsWith("did:plc") ? 335 - `${localStorage.plcDirectory ?? "https://plc.directory"}/${did}` 336 - : `https://${did.split("did:web:")[1]}/.well-known/did.json` 337 - } 338 - newTab 339 - label="DID document" 340 - icon="lucide--external-link" 341 - /> 342 - <Show when={did.startsWith("did:plc")}> 328 + <Show when={params.repo && params.repo in labelerCache}> 329 + <NavMenu 330 + href={`/labels?did=${params.repo}&uriPatterns=*`} 331 + label="Labels" 332 + icon="lucide--tag" 333 + /> 334 + </Show> 335 + <Show when={error()?.length === 0 || error() === undefined}> 336 + <ActionMenu 337 + label="Export repo" 338 + icon={ 339 + downloading() ? "lucide--loader-circle animate-spin" : "lucide--download" 340 + } 341 + onClick={() => downloadRepo()} 342 + /> 343 + </Show> 344 + <MenuSeparator /> 343 345 <NavMenu 344 - href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`} 346 + href={ 347 + did.startsWith("did:plc") ? 348 + `${localStorage.plcDirectory ?? "https://plc.directory"}/${did}` 349 + : `https://${did.split("did:web:")[1]}/.well-known/did.json` 350 + } 345 351 newTab 346 - label="Audit log" 352 + label="DID document" 347 353 icon="lucide--external-link" 348 354 /> 349 - </Show> 350 - </DropdownMenu> 351 - </MenuProvider> 355 + <Show when={did.startsWith("did:plc")}> 356 + <NavMenu 357 + href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`} 358 + newTab 359 + label="Audit log" 360 + icon="lucide--external-link" 361 + /> 362 + </Show> 363 + </DropdownMenu> 364 + </MenuProvider> 365 + </div> 352 366 </div> 353 - </div> 354 - <div class="flex w-full flex-col gap-1 px-2"> 355 - <Show when={location.hash === "#logs"}> 356 - <ErrorBoundary 357 - fallback={(err) => <div class="wrap-break-word">Error: {err.message}</div>} 358 - > 359 - <Suspense 360 - fallback={ 361 - <div class="iconify lucide--loader-circle mt-2 animate-spin self-center text-xl" /> 362 - } 367 + <div class="flex w-full flex-col gap-1 px-2"> 368 + <Show when={location.hash === "#logs"}> 369 + <ErrorBoundary 370 + fallback={(err) => <div class="wrap-break-word">Error: {err.message}</div>} 363 371 > 364 - <PlcLogView did={did} /> 365 - </Suspense> 366 - </ErrorBoundary> 367 - </Show> 368 - <Show when={location.hash === "#backlinks"}> 369 - <ErrorBoundary 370 - fallback={(err) => <div class="wrap-break-word">Error: {err.message}</div>} 371 - > 372 - <Suspense 373 - fallback={ 374 - <div class="iconify lucide--loader-circle mt-2 animate-spin self-center text-xl" /> 375 - } 372 + <Suspense 373 + fallback={ 374 + <div class="iconify lucide--loader-circle mt-2 animate-spin self-center text-xl" /> 375 + } 376 + > 377 + <PlcLogView did={did} /> 378 + </Suspense> 379 + </ErrorBoundary> 380 + </Show> 381 + <Show when={location.hash === "#backlinks"}> 382 + <ErrorBoundary 383 + fallback={(err) => <div class="wrap-break-word">Error: {err.message}</div>} 376 384 > 377 - <Backlinks target={did} /> 378 - </Suspense> 379 - </ErrorBoundary> 380 - </Show> 381 - <Show when={location.hash === "#blobs"}> 382 - <ErrorBoundary 383 - fallback={(err) => <div class="wrap-break-word">Error: {err.message}</div>} 384 - > 385 - <Suspense 386 - fallback={ 387 - <div class="iconify lucide--loader-circle mt-2 animate-spin self-center text-xl" /> 388 - } 385 + <Suspense 386 + fallback={ 387 + <div class="iconify lucide--loader-circle mt-2 animate-spin self-center text-xl" /> 388 + } 389 + > 390 + <Backlinks target={did} /> 391 + </Suspense> 392 + </ErrorBoundary> 393 + </Show> 394 + <Show when={location.hash === "#blobs"}> 395 + <ErrorBoundary 396 + fallback={(err) => <div class="wrap-break-word">Error: {err.message}</div>} 389 397 > 390 - <BlobView pds={pds!} repo={did} /> 391 - </Suspense> 392 - </ErrorBoundary> 393 - </Show> 394 - <Show when={nsids() && (!location.hash || location.hash.startsWith("#collections"))}> 395 - <Show when={showFilter()}> 396 - <TextInput 397 - name="filter" 398 - placeholder="Filter collections" 399 - onInput={(e) => setFilter(e.currentTarget.value.toLowerCase())} 400 - class="grow" 401 - ref={(node) => { 402 - onMount(() => node.focus()); 403 - }} 404 - /> 398 + <Suspense 399 + fallback={ 400 + <div class="iconify lucide--loader-circle mt-2 animate-spin self-center text-xl" /> 401 + } 402 + > 403 + <BlobView pds={pds!} repo={did} /> 404 + </Suspense> 405 + </ErrorBoundary> 405 406 </Show> 406 - <div class="flex flex-col text-sm wrap-anywhere" classList={{ "-mt-1": !showFilter() }}> 407 - <Show 408 - when={Object.keys(nsids() ?? {}).length != 0} 409 - fallback={<span class="mt-3 text-center text-base">No collections found.</span>} 407 + <Show when={nsids() && (!location.hash || location.hash.startsWith("#collections"))}> 408 + <Show when={showFilter()}> 409 + <TextInput 410 + name="filter" 411 + placeholder="Filter collections" 412 + onInput={(e) => setFilter(e.currentTarget.value.toLowerCase())} 413 + class="grow" 414 + ref={(node) => { 415 + onMount(() => node.focus()); 416 + }} 417 + /> 418 + </Show> 419 + <div 420 + class="flex flex-col text-sm wrap-anywhere" 421 + classList={{ "-mt-1": !showFilter() }} 410 422 > 411 - <For 412 - each={Object.keys(nsids() ?? {}).filter((authority) => 413 - filter() ? 414 - authority.includes(filter()!) || 415 - nsids()?.[authority].nsids.some((nsid) => 416 - `${authority}.${nsid}`.includes(filter()!), 417 - ) 418 - : true, 419 - )} 423 + <Show 424 + when={Object.keys(nsids() ?? {}).length != 0} 425 + fallback={<span class="mt-3 text-center text-base">No collections found.</span>} 420 426 > 421 - {(authority) => { 422 - const reversedDomain = authority.split(".").reverse().join("."); 423 - const [faviconLoaded, setFaviconLoaded] = createSignal(false); 427 + <For 428 + each={Object.keys(nsids() ?? {}).filter((authority) => 429 + filter() ? 430 + authority.includes(filter()!) || 431 + nsids()?.[authority].nsids.some((nsid) => 432 + `${authority}.${nsid}`.includes(filter()!), 433 + ) 434 + : true, 435 + )} 436 + > 437 + {(authority) => { 438 + const reversedDomain = authority.split(".").reverse().join("."); 439 + const [faviconLoaded, setFaviconLoaded] = createSignal(false); 424 440 425 - const isHighlighted = () => location.hash === `#collections:${authority}`; 441 + const isHighlighted = () => location.hash === `#collections:${authority}`; 426 442 427 - return ( 428 - <div 429 - id={`collection-${authority}`} 430 - class="group flex items-start gap-2 rounded-lg p-1 transition-colors" 431 - classList={{ 432 - "dark:hover:bg-dark-200 hover:bg-neutral-200": !isHighlighted(), 433 - "bg-blue-100 dark:bg-blue-500/25": isHighlighted(), 434 - }} 435 - > 436 - <a 437 - href={`#collections:${authority}`} 438 - class="relative flex h-5 w-4 shrink-0 items-center justify-center hover:opacity-70" 443 + return ( 444 + <div 445 + id={`collection-${authority}`} 446 + class="group flex items-start gap-2 rounded-lg p-1 transition-colors" 447 + classList={{ 448 + "dark:hover:bg-dark-200 hover:bg-neutral-200": !isHighlighted(), 449 + "bg-blue-100 dark:bg-blue-500/25": isHighlighted(), 450 + }} 439 451 > 440 - <span class="absolute top-1/2 -left-5 flex -translate-y-1/2 items-center text-base opacity-0 transition-opacity group-hover:opacity-100"> 441 - <span class="iconify lucide--link absolute -left-2 w-7"></span> 442 - </span> 443 - <Show when={!faviconLoaded()}> 444 - <span class="iconify lucide--globe size-4 text-neutral-400 dark:text-neutral-500" /> 445 - </Show> 446 - <img 447 - src={ 448 - ["bsky.app", "bsky.chat"].includes(reversedDomain) ? 449 - "https://web-cdn.bsky.app/static/apple-touch-icon.png" 450 - : `https://${reversedDomain}/favicon.ico` 451 - } 452 - alt={`${reversedDomain} favicon`} 453 - class="h-4 w-4" 454 - classList={{ hidden: !faviconLoaded() }} 455 - onLoad={() => setFaviconLoaded(true)} 456 - onError={() => setFaviconLoaded(false)} 457 - /> 458 - </a> 459 - <div class="flex flex-1 flex-col"> 460 - <For 461 - each={nsids()?.[authority].nsids.filter((nsid) => 462 - filter() ? `${authority}.${nsid}`.includes(filter()!) : true, 463 - )} 452 + <a 453 + href={`#collections:${authority}`} 454 + class="relative flex h-5 w-4 shrink-0 items-center justify-center hover:opacity-70" 464 455 > 465 - {(nsid) => ( 466 - <A 467 - href={`/at://${did}/${authority}.${nsid}`} 468 - class="hover:underline active:underline" 469 - > 470 - <span>{authority}</span> 471 - <span class="text-neutral-500 dark:text-neutral-400">.{nsid}</span> 472 - </A> 473 - )} 474 - </For> 456 + <span class="absolute top-1/2 -left-5 flex -translate-y-1/2 items-center text-base opacity-0 transition-opacity group-hover:opacity-100"> 457 + <span class="iconify lucide--link absolute -left-2 w-7"></span> 458 + </span> 459 + <Show when={!faviconLoaded()}> 460 + <span class="iconify lucide--globe size-4 text-neutral-400 dark:text-neutral-500" /> 461 + </Show> 462 + <img 463 + src={ 464 + ["bsky.app", "bsky.chat"].includes(reversedDomain) ? 465 + "https://web-cdn.bsky.app/static/apple-touch-icon.png" 466 + : `https://${reversedDomain}/favicon.ico` 467 + } 468 + alt={`${reversedDomain} favicon`} 469 + class="h-4 w-4" 470 + classList={{ hidden: !faviconLoaded() }} 471 + onLoad={() => setFaviconLoaded(true)} 472 + onError={() => setFaviconLoaded(false)} 473 + /> 474 + </a> 475 + <div class="flex flex-1 flex-col"> 476 + <For 477 + each={nsids()?.[authority].nsids.filter((nsid) => 478 + filter() ? `${authority}.${nsid}`.includes(filter()!) : true, 479 + )} 480 + > 481 + {(nsid) => ( 482 + <A 483 + href={`/at://${did}/${authority}.${nsid}`} 484 + class="hover:underline active:underline" 485 + > 486 + <span>{authority}</span> 487 + <span class="text-neutral-500 dark:text-neutral-400"> 488 + .{nsid} 489 + </span> 490 + </A> 491 + )} 492 + </For> 493 + </div> 475 494 </div> 495 + ); 496 + }} 497 + </For> 498 + </Show> 499 + </div> 500 + </Show> 501 + <Show when={location.hash === "#identity" || (error() && !location.hash)}> 502 + <Show when={didDoc()}> 503 + {(didDocument) => ( 504 + <div class="flex flex-col gap-3 wrap-anywhere"> 505 + {/* ID Section */} 506 + <div> 507 + <div class="font-semibold">DID</div> 508 + <div class="text-sm text-neutral-700 dark:text-neutral-300"> 509 + {didDocument().id} 476 510 </div> 477 - ); 478 - }} 479 - </For> 480 - </Show> 481 - </div> 482 - </Show> 483 - <Show when={location.hash === "#identity" || (error() && !location.hash)}> 484 - <Show when={didDoc()}> 485 - {(didDocument) => ( 486 - <div class="flex flex-col gap-3 wrap-anywhere"> 487 - {/* ID Section */} 488 - <div> 489 - <div class="font-semibold">DID</div> 490 - <div class="text-sm text-neutral-700 dark:text-neutral-300"> 491 - {didDocument().id} 492 511 </div> 493 - </div> 494 512 495 - {/* Aliases Section */} 496 - <div> 497 - <p class="font-semibold">Aliases</p> 498 - <For each={didDocument().alsoKnownAs}> 499 - {(alias) => ( 500 - <div class="flex items-center gap-1 text-sm text-neutral-700 dark:text-neutral-300"> 501 - <span>{alias}</span> 502 - <Show when={alias.startsWith("at://")}> 503 - <Tooltip 504 - text={ 505 - validHandles[alias] === true ? "Valid handle" 506 - : validHandles[alias] === undefined ? 507 - "Validating" 508 - : "Invalid handle" 509 - } 510 - > 511 - <span 512 - classList={{ 513 - "iconify lucide--check text-green-600 dark:text-green-400": 514 - validHandles[alias] === true, 515 - "iconify lucide--x text-red-500 dark:text-red-400": 516 - validHandles[alias] === false, 517 - "iconify lucide--loader-circle animate-spin": 518 - validHandles[alias] === undefined, 519 - }} 520 - ></span> 521 - </Tooltip> 522 - </Show> 523 - </div> 524 - )} 525 - </For> 526 - </div> 527 - 528 - {/* Services Section */} 529 - <div> 530 - <p class="font-semibold">Services</p> 531 - <div class="flex flex-col gap-1"> 532 - <For each={didDocument().service}> 533 - {(service) => ( 534 - <div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300"> 535 - <span class="iconify lucide--hash"></span> 536 - <span>{service.id.split("#")[1]}</span> 537 - <span></span> 538 - <a 539 - class="w-fit underline hover:text-blue-400" 540 - href={service.serviceEndpoint.toString()} 541 - target="_blank" 542 - rel="noopener" 543 - > 544 - {service.serviceEndpoint.toString()} 545 - </a> 513 + {/* Aliases Section */} 514 + <div> 515 + <p class="font-semibold">Aliases</p> 516 + <For each={didDocument().alsoKnownAs}> 517 + {(alias) => ( 518 + <div class="flex items-center gap-1 text-sm text-neutral-700 dark:text-neutral-300"> 519 + <span>{alias}</span> 520 + <Show when={alias.startsWith("at://")}> 521 + <Tooltip 522 + text={ 523 + validHandles[alias] === true ? "Valid handle" 524 + : validHandles[alias] === undefined ? 525 + "Validating" 526 + : "Invalid handle" 527 + } 528 + > 529 + <span 530 + classList={{ 531 + "iconify lucide--check text-green-600 dark:text-green-400": 532 + validHandles[alias] === true, 533 + "iconify lucide--x text-red-500 dark:text-red-400": 534 + validHandles[alias] === false, 535 + "iconify lucide--loader-circle animate-spin": 536 + validHandles[alias] === undefined, 537 + }} 538 + ></span> 539 + </Tooltip> 540 + </Show> 546 541 </div> 547 542 )} 548 543 </For> 549 544 </div> 550 - </div> 551 545 552 - {/* Verification Methods Section */} 553 - <div> 554 - <p class="font-semibold">Verification Methods</p> 555 - <div class="flex flex-col gap-1"> 556 - <For each={didDocument().verificationMethod}> 557 - {(verif) => ( 558 - <Show when={verif.publicKeyMultibase}> 559 - {(key) => ( 560 - <div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300"> 561 - <span class="iconify lucide--hash"></span> 562 - <div class="flex items-center gap-2"> 563 - <span>{verif.id.split("#")[1]}</span> 564 - <div class="flex items-center gap-1 text-neutral-500 dark:text-neutral-400"> 565 - <span class="iconify lucide--key-round"></span> 566 - <span>{detectKeyType(key())}</span> 567 - </div> 568 - </div> 569 - <span></span> 570 - <div class="font-mono break-all">{key()}</div> 571 - </div> 572 - )} 573 - </Show> 574 - )} 575 - </For> 576 - </div> 577 - </div> 578 - 579 - {/* Rotation Keys Section */} 580 - <Show when={rotationKeys().length > 0}> 546 + {/* Services Section */} 581 547 <div> 582 - <p class="font-semibold">Rotation Keys</p> 548 + <p class="font-semibold">Services</p> 583 549 <div class="flex flex-col gap-1"> 584 - <For each={rotationKeys()}> 585 - {(key) => ( 550 + <For each={didDocument().service}> 551 + {(service) => ( 586 552 <div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300"> 587 - <span class="iconify lucide--key-round text-neutral-500 dark:text-neutral-400"></span> 588 - <span class="text-neutral-500 dark:text-neutral-400"> 589 - {detectDidKeyType(key)} 590 - </span> 553 + <span class="iconify lucide--hash"></span> 554 + <span>{service.id.split("#")[1]}</span> 591 555 <span></span> 592 - <div class="font-mono break-all">{key.replace("did:key:", "")}</div> 556 + <a 557 + class="w-fit underline hover:text-blue-400" 558 + href={service.serviceEndpoint.toString()} 559 + target="_blank" 560 + rel="noopener" 561 + > 562 + {service.serviceEndpoint.toString()} 563 + </a> 593 564 </div> 594 565 )} 595 566 </For> 596 567 </div> 597 568 </div> 598 - </Show> 599 - </div> 600 - )} 569 + 570 + {/* Verification Methods Section */} 571 + <div> 572 + <p class="font-semibold">Verification Methods</p> 573 + <div class="flex flex-col gap-1"> 574 + <For each={didDocument().verificationMethod}> 575 + {(verif) => ( 576 + <Show when={verif.publicKeyMultibase}> 577 + {(key) => ( 578 + <div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300"> 579 + <span class="iconify lucide--hash"></span> 580 + <div class="flex items-center gap-2"> 581 + <span>{verif.id.split("#")[1]}</span> 582 + <div class="flex items-center gap-1 text-neutral-500 dark:text-neutral-400"> 583 + <span class="iconify lucide--key-round"></span> 584 + <span>{detectKeyType(key())}</span> 585 + </div> 586 + </div> 587 + <span></span> 588 + <div class="font-mono break-all">{key()}</div> 589 + </div> 590 + )} 591 + </Show> 592 + )} 593 + </For> 594 + </div> 595 + </div> 596 + 597 + {/* Rotation Keys Section */} 598 + <Show when={rotationKeys().length > 0}> 599 + <div> 600 + <p class="font-semibold">Rotation Keys</p> 601 + <div class="flex flex-col gap-1"> 602 + <For each={rotationKeys()}> 603 + {(key) => ( 604 + <div class="grid grid-cols-[auto_1fr] items-center gap-x-1 text-sm text-neutral-700 dark:text-neutral-300"> 605 + <span class="iconify lucide--key-round text-neutral-500 dark:text-neutral-400"></span> 606 + <span class="text-neutral-500 dark:text-neutral-400"> 607 + {detectDidKeyType(key)} 608 + </span> 609 + <span></span> 610 + <div class="font-mono break-all">{key.replace("did:key:", "")}</div> 611 + </div> 612 + )} 613 + </For> 614 + </div> 615 + </div> 616 + </Show> 617 + </div> 618 + )} 619 + </Show> 601 620 </Show> 602 - </Show> 621 + </div> 603 622 </div> 604 - </div> 605 - </Show> 623 + </Show> 624 + </> 606 625 ); 607 626 };
+2
src/views/settings.tsx
··· 1 + import { Title } from "@solidjs/meta"; 1 2 import { createSignal } from "solid-js"; 2 3 import { TextInput } from "../components/text-input.jsx"; 3 4 import { ThemeSelection } from "../components/theme.jsx"; ··· 7 8 const Settings = () => { 8 9 return ( 9 10 <div class="flex w-full flex-col gap-2 px-2"> 11 + <Title>Settings - PDSls</Title> 10 12 <div class="text-lg font-semibold">Settings</div> 11 13 <div class="flex flex-col gap-3"> 12 14 <div class="flex flex-col gap-1">
+123 -119
src/views/stream.tsx
··· 1 1 import { Firehose } from "@skyware/firehose"; 2 + import { Title } from "@solidjs/meta"; 2 3 import { A, useLocation, useSearchParams } from "@solidjs/router"; 3 4 import { createSignal, For, onCleanup, onMount, Show } from "solid-js"; 4 5 import { Button } from "../components/button"; ··· 169 170 }); 170 171 171 172 return ( 172 - <div class="flex w-full flex-col items-center"> 173 - <div class="mb-1 flex gap-4 font-medium"> 174 - <A 175 - class="flex items-center gap-1 border-b-2" 176 - inactiveClass="border-transparent text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-neutral-600" 177 - href="/jetstream" 178 - > 179 - Jetstream 180 - </A> 181 - <A 182 - class="flex items-center gap-1 border-b-2" 183 - inactiveClass="border-transparent text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-neutral-600" 184 - href="/firehose" 185 - > 186 - Firehose 187 - </A> 188 - </div> 189 - <StickyOverlay> 190 - <form ref={formRef} class="flex w-full flex-col gap-1.5 text-sm"> 191 - <Show when={!connected()}> 192 - <label class="flex items-center justify-end gap-x-1"> 193 - <span class="min-w-20">Instance</span> 194 - <TextInput 195 - name="instance" 196 - value={ 197 - searchParams.instance ?? 198 - (streamType === "jetstream" ? 199 - "wss://jetstream1.us-east.bsky.network/subscribe" 200 - : "wss://bsky.network") 201 - } 202 - class="grow" 203 - /> 204 - </label> 205 - <Show when={streamType === "jetstream"}> 173 + <> 174 + <Title>{streamType === "firehose" ? "Firehose" : "Jetstream"} - PDSls</Title> 175 + <div class="flex w-full flex-col items-center"> 176 + <div class="mb-1 flex gap-4 font-medium"> 177 + <A 178 + class="flex items-center gap-1 border-b-2" 179 + inactiveClass="border-transparent text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-neutral-600" 180 + href="/jetstream" 181 + > 182 + Jetstream 183 + </A> 184 + <A 185 + class="flex items-center gap-1 border-b-2" 186 + inactiveClass="border-transparent text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-neutral-600" 187 + href="/firehose" 188 + > 189 + Firehose 190 + </A> 191 + </div> 192 + <StickyOverlay> 193 + <form ref={formRef} class="flex w-full flex-col gap-1.5 text-sm"> 194 + <Show when={!connected()}> 206 195 <label class="flex items-center justify-end gap-x-1"> 207 - <span class="min-w-20">Collections</span> 208 - <textarea 209 - name="collections" 210 - spellcheck={false} 211 - placeholder="Comma-separated list of collections" 212 - value={searchParams.collections ?? ""} 213 - class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400" 196 + <span class="min-w-20">Instance</span> 197 + <TextInput 198 + name="instance" 199 + value={ 200 + searchParams.instance ?? 201 + (streamType === "jetstream" ? 202 + "wss://jetstream1.us-east.bsky.network/subscribe" 203 + : "wss://bsky.network") 204 + } 205 + class="grow" 214 206 /> 215 207 </label> 216 - </Show> 217 - <Show when={streamType === "jetstream"}> 208 + <Show when={streamType === "jetstream"}> 209 + <label class="flex items-center justify-end gap-x-1"> 210 + <span class="min-w-20">Collections</span> 211 + <textarea 212 + name="collections" 213 + spellcheck={false} 214 + placeholder="Comma-separated list of collections" 215 + value={searchParams.collections ?? ""} 216 + class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400" 217 + /> 218 + </label> 219 + </Show> 220 + <Show when={streamType === "jetstream"}> 221 + <label class="flex items-center justify-end gap-x-1"> 222 + <span class="min-w-20">DIDs</span> 223 + <textarea 224 + name="dids" 225 + spellcheck={false} 226 + placeholder="Comma-separated list of DIDs" 227 + value={searchParams.dids ?? ""} 228 + class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400" 229 + /> 230 + </label> 231 + </Show> 218 232 <label class="flex items-center justify-end gap-x-1"> 219 - <span class="min-w-20">DIDs</span> 220 - <textarea 221 - name="dids" 222 - spellcheck={false} 223 - placeholder="Comma-separated list of DIDs" 224 - value={searchParams.dids ?? ""} 225 - class="dark:bg-dark-100 grow rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400" 233 + <span class="min-w-20">Cursor</span> 234 + <TextInput 235 + name="cursor" 236 + placeholder="Leave empty for live-tail" 237 + value={searchParams.cursor ?? ""} 238 + class="grow" 226 239 /> 227 240 </label> 228 - </Show> 229 - <label class="flex items-center justify-end gap-x-1"> 230 - <span class="min-w-20">Cursor</span> 231 - <TextInput 232 - name="cursor" 233 - placeholder="Leave empty for live-tail" 234 - value={searchParams.cursor ?? ""} 235 - class="grow" 236 - /> 237 - </label> 238 - <Show when={streamType === "jetstream"}> 239 - <div class="flex items-center justify-end gap-x-1"> 240 - <input 241 - type="checkbox" 242 - name="allEvents" 243 - id="allEvents" 244 - checked={searchParams.allEvents === "on" ? true : false} 245 - /> 246 - <label for="allEvents" class="select-none"> 247 - Show account and identity events 248 - </label> 249 - </div> 241 + <Show when={streamType === "jetstream"}> 242 + <div class="flex items-center justify-end gap-x-1"> 243 + <input 244 + type="checkbox" 245 + name="allEvents" 246 + id="allEvents" 247 + checked={searchParams.allEvents === "on" ? true : false} 248 + /> 249 + <label for="allEvents" class="select-none"> 250 + Show account and identity events 251 + </label> 252 + </div> 253 + </Show> 250 254 </Show> 251 - </Show> 252 - <Show when={connected()}> 253 - <div class="flex flex-col gap-1 wrap-anywhere"> 254 - <For each={parameters()}> 255 - {(param) => ( 256 - <Show when={param.param}> 257 - <div class="flex"> 258 - <div class="min-w-24 font-semibold">{param.name}</div> 259 - {param.param} 260 - </div> 261 - </Show> 262 - )} 263 - </For> 264 - </div> 265 - </Show> 266 - <div class="flex justify-end"> 267 255 <Show when={connected()}> 268 - <button 269 - type="button" 270 - onmousedown={(e) => { 271 - e.preventDefault(); 272 - disconnect(); 273 - }} 274 - ontouchstart={(e) => { 275 - e.preventDefault(); 276 - disconnect(); 277 - }} 278 - class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 items-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 279 - > 280 - Disconnect 281 - </button> 282 - </Show> 283 - <Show when={!connected()}> 284 - <Button onClick={() => connectSocket(new FormData(formRef))}>Connect</Button> 256 + <div class="flex flex-col gap-1 wrap-anywhere"> 257 + <For each={parameters()}> 258 + {(param) => ( 259 + <Show when={param.param}> 260 + <div class="flex"> 261 + <div class="min-w-24 font-semibold">{param.name}</div> 262 + {param.param} 263 + </div> 264 + </Show> 265 + )} 266 + </For> 267 + </div> 285 268 </Show> 286 - </div> 287 - </form> 288 - </StickyOverlay> 289 - <Show when={notice().length}> 290 - <div class="text-red-500 dark:text-red-400">{notice()}</div> 291 - </Show> 292 - <div class="flex w-full flex-col gap-2 divide-y-[0.5px] divide-neutral-500 font-mono text-sm wrap-anywhere whitespace-pre-wrap md:w-3xl"> 293 - <For each={records().toReversed()}> 294 - {(rec) => ( 295 - <div class="pb-2"> 296 - <JSONValue data={rec} repo={rec.did ?? rec.repo} /> 269 + <div class="flex justify-end"> 270 + <Show when={connected()}> 271 + <button 272 + type="button" 273 + onmousedown={(e) => { 274 + e.preventDefault(); 275 + disconnect(); 276 + }} 277 + ontouchstart={(e) => { 278 + e.preventDefault(); 279 + disconnect(); 280 + }} 281 + class="dark:hover:bg-dark-200 dark:shadow-dark-700 dark:active:bg-dark-100 box-border flex h-7 items-center gap-1 rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 px-2 py-1.5 text-xs shadow-xs select-none hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800" 282 + > 283 + Disconnect 284 + </button> 285 + </Show> 286 + <Show when={!connected()}> 287 + <Button onClick={() => connectSocket(new FormData(formRef))}>Connect</Button> 288 + </Show> 297 289 </div> 298 - )} 299 - </For> 290 + </form> 291 + </StickyOverlay> 292 + <Show when={notice().length}> 293 + <div class="text-red-500 dark:text-red-400">{notice()}</div> 294 + </Show> 295 + <div class="flex w-full flex-col gap-2 divide-y-[0.5px] divide-neutral-500 font-mono text-sm wrap-anywhere whitespace-pre-wrap md:w-3xl"> 296 + <For each={records().toReversed()}> 297 + {(rec) => ( 298 + <div class="pb-2"> 299 + <JSONValue data={rec} repo={rec.did ?? rec.repo} /> 300 + </div> 301 + )} 302 + </For> 303 + </div> 300 304 </div> 301 - </div> 305 + </> 302 306 ); 303 307 }; 304 308