atmosphere explorer
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

new toolbar in collection view

Juliet 94f87dce d17be502

+247 -188
+12 -7
src/components/permission-button.tsx
··· 5 5 6 6 export interface PermissionButtonProps { 7 7 scope: "create" | "update" | "delete" | "blob"; 8 - tooltip: string; 8 + tooltip?: string; 9 9 class?: string; 10 10 disabledClass?: string; 11 11 onClick: () => void; ··· 28 28 "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"; 29 29 const disabledClass = props.disabledClass || "flex items-center rounded-sm p-1.5 opacity-40"; 30 30 31 - return ( 32 - <Tooltip text={hasPermission() ? props.tooltip : `${props.tooltip} (permission required)`}> 33 - <button class={hasPermission() ? baseClass : disabledClass} onclick={handleClick}> 34 - {props.children} 35 - </button> 36 - </Tooltip> 31 + const tooltip = () => 32 + hasPermission() ? props.tooltip : `${props.tooltip ?? ""} (permission required)`.trimStart(); 33 + 34 + const button = ( 35 + <button class={hasPermission() ? baseClass : disabledClass} onclick={handleClick}> 36 + {props.children} 37 + </button> 37 38 ); 39 + 40 + return props.tooltip ? 41 + <Tooltip text={tooltip()!}>{button}</Tooltip> 42 + : button; 38 43 };
+2 -2
src/views/blob.tsx
··· 45 45 </For> 46 46 </div> 47 47 </Show> 48 - <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 pt-2 pb-4"> 49 - <div class="flex min-w-50 items-center justify-around gap-3 pb-2"> 48 + <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center border-t border-neutral-200 bg-neutral-100 pt-3 pb-6 dark:border-neutral-700"> 49 + <div class="flex min-w-50 items-center justify-around gap-3"> 50 50 <p> 51 51 {blobs()?.length} blob{(blobs()?.length ?? 0 > 1) ? "s" : ""} 52 52 </p>
+229 -175
src/views/collection.tsx
··· 4 4 import * as TID from "@atcute/tid"; 5 5 import { Title } from "@solidjs/meta"; 6 6 import { A, useBeforeLeave, useParams, useSearchParams } from "@solidjs/router"; 7 - import { createMemo, createResource, createSignal, For, onMount, Show } from "solid-js"; 7 + import { createMemo, createResource, createSignal, For, onCleanup, onMount, Show } from "solid-js"; 8 8 import { createStore } from "solid-js/store"; 9 9 import { agent } from "../auth/state"; 10 10 import { Button } from "../components/button.jsx"; ··· 13 13 import { Modal } from "../components/modal.jsx"; 14 14 import { addNotification, removeNotification } from "../components/notification.jsx"; 15 15 import { PermissionButton } from "../components/permission-button.jsx"; 16 - import { StickyOverlay } from "../components/sticky.jsx"; 17 - import { TextInput } from "../components/text-input.jsx"; 18 16 import Tooltip from "../components/tooltip.jsx"; 17 + import { canHover } from "../layout.jsx"; 19 18 import { resolvePDS } from "../utils/api.js"; 20 19 import { localDateFromTimestamp } from "../utils/date.js"; 21 20 import { ··· 85 84 const did = params.repo; 86 85 let pds: string; 87 86 let rpc: Client; 87 + let filterInputRef: HTMLInputElement | undefined; 88 88 89 89 const cacheKey = () => `${params.pds}/${params.repo}/${params.collection}`; 90 90 ··· 103 103 window.scrollTo(0, cached.scrollY); 104 104 }); 105 105 } 106 + 107 + const handleKeyDown = (e: KeyboardEvent) => { 108 + if ( 109 + e.key === "/" && 110 + !["INPUT", "TEXTAREA"].includes((e.target as HTMLElement)?.tagName) && 111 + !document.querySelector("[data-modal]") 112 + ) { 113 + e.preventDefault(); 114 + filterInputRef?.focus(); 115 + } 116 + }; 117 + document.addEventListener("keydown", handleKeyDown); 118 + onCleanup(() => document.removeEventListener("keydown", handleKeyDown)); 106 119 }); 107 120 108 121 useBeforeLeave((e) => { ··· 241 254 <> 242 255 <Title>{params.collection} - PDSls</Title> 243 256 <Show when={records.length || response()}> 244 - <div class="-mt-2 flex w-full flex-col items-center"> 245 - <StickyOverlay> 246 - <div class="flex w-full flex-col gap-2"> 247 - <div class="flex items-center gap-1.5"> 248 - <Show when={agent() && agent()?.sub === did}> 249 - <div class="flex items-center"> 250 - <PermissionButton 251 - scope="delete" 252 - tooltip={batchDelete() ? "Cancel" : "Manage"} 257 + <div class="flex w-full flex-col items-center"> 258 + {/* Tab bar */} 259 + <div class="mb-2 flex min-h-7 w-full items-center justify-between px-2 text-sm sm:text-base"> 260 + <div class="flex gap-4"> 261 + <span class="border-b-2 font-medium">Records</span> 262 + <A 263 + href={`/jetstream?collections=${params.collection}&dids=${params.repo}`} 264 + class="border-b-2 border-transparent font-medium transition-colors not-hover:text-neutral-600 not-hover:dark:text-neutral-300/80" 265 + > 266 + Jetstream 267 + </A> 268 + </div> 269 + <Show when={agent() && agent()?.sub === did}> 270 + <div class="flex items-center text-sm"> 271 + <Show when={batchDelete()}> 272 + <Tooltip text="Select all"> 273 + <button 274 + onclick={() => selectAll()} 253 275 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" 254 - disabledClass="flex items-center rounded-md p-1.5 opacity-40" 255 - onClick={() => { 256 - setRecords({ from: 0, to: records.length - 1 }, "toDelete", false); 257 - setLastSelected(undefined); 258 - setBatchDelete(!batchDelete()); 259 - }} 260 276 > 261 - <span 262 - class={`iconify ${batchDelete() ? "lucide--x" : "lucide--trash-2"} `} 263 - ></span> 264 - </PermissionButton> 265 - <Show when={batchDelete()}> 266 - <Tooltip 267 - text="Select all" 268 - children={ 269 - <button 270 - onclick={() => selectAll()} 271 - 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" 272 - > 273 - <span class="iconify lucide--list-checks"></span> 274 - </button> 275 - } 276 - /> 277 - <PermissionButton 278 - scope="create" 279 - tooltip="Recreate" 280 - 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" 281 - disabledClass="flex items-center rounded-md p-1.5 opacity-40" 282 - onClick={() => { 283 - setRecreate(true); 284 - setOpenDelete(true); 285 - }} 286 - > 287 - <span class="iconify lucide--recycle text-green-500 dark:text-green-400"></span> 288 - </PermissionButton> 289 - <Tooltip 290 - text="Delete" 291 - children={ 292 - <button 293 - onclick={() => { 294 - setRecreate(false); 295 - setOpenDelete(true); 296 - }} 297 - 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" 298 - > 299 - <span class="iconify lucide--trash-2 text-red-500 dark:text-red-400"></span> 300 - </button> 301 - } 302 - /> 303 - </Show> 304 - </div> 305 - <Modal 306 - open={openDelete()} 307 - onClose={() => setOpenDelete(false)} 308 - contentClass="dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md dark:border-neutral-700" 309 - > 310 - <h2 class="mb-2 font-semibold"> 311 - {recreate() ? "Recreate" : "Delete"}{" "} 312 - {records.filter((r) => r.toDelete).length} records? 313 - </h2> 314 - <div class="flex justify-end gap-2"> 315 - <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 316 - <Button 317 - onClick={deleteRecords} 318 - classList={{ 319 - "bg-blue-500! text-white! hover:bg-blue-600! active:bg-blue-700! dark:bg-blue-600! dark:hover:bg-blue-500! dark:active:bg-blue-400! border-none!": 320 - recreate(), 321 - "text-white! border-none! bg-red-500! hover:bg-red-600! active:bg-red-700!": 322 - !recreate(), 323 - }} 324 - > 325 - {recreate() ? "Recreate" : "Delete"} 326 - </Button> 327 - </div> 328 - </Modal> 329 - </Show> 330 - <TextInput 331 - name="Filter" 332 - placeholder="Filter records" 333 - onInput={(e) => setFilter(e.currentTarget.value)} 334 - class="grow text-sm" 335 - /> 336 - <Tooltip text="Jetstream"> 337 - <A 338 - href={`/jetstream?collections=${params.collection}&dids=${params.repo}`} 277 + <span class="iconify lucide--list-checks"></span> 278 + </button> 279 + </Tooltip> 280 + <PermissionButton 281 + scope="create" 282 + tooltip="Recreate" 339 283 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" 340 - > 341 - <span class="iconify lucide--radio-tower"></span> 342 - </A> 343 - </Tooltip> 344 - </div> 345 - <Show when={records.length > 1}> 346 - <div class="flex items-center justify-between gap-x-2"> 347 - <Button 284 + disabledClass="flex items-center rounded-md p-1.5 opacity-40" 348 285 onClick={() => { 349 - const newReverse = !reverse(); 350 - setReverse(newReverse); 351 - setSearchParams({ reverse: newReverse ? "true" : undefined }); 352 - setCursor(undefined); 353 - setRestoredFromCache(false); 354 - clearCollectionCache(cacheKey()); 355 - refetch(); 356 - }} 357 - classList={{ 358 - "text-blue-500! dark:text-blue-400! border-blue-500! dark:border-blue-400!": 359 - reverse(), 286 + setRecreate(true); 287 + setOpenDelete(true); 360 288 }} 361 289 > 362 - <span 363 - class={`iconify ${reverse() ? "lucide--arrow-down-wide-narrow" : "lucide--arrow-up-narrow-wide"}`} 364 - ></span> 365 - Reverse 366 - </Button> 367 - <div> 368 - <Show when={batchDelete()}> 369 - <span>{records.filter((rec) => rec.toDelete).length}</span> 370 - <span>/</span> 371 - </Show> 372 - <span>{filter() ? filteredRecords().length : records.length} records</span> 373 - </div> 374 - <div class="flex w-20 items-center justify-end"> 375 - <Show when={cursor()}> 376 - <Show when={!response.loading}> 377 - <Button onClick={() => refetch()}>Load more</Button> 290 + <span class="iconify lucide--recycle text-green-500 dark:text-green-400"></span> 291 + </PermissionButton> 292 + <Tooltip text="Delete"> 293 + <button 294 + onclick={() => { 295 + setRecreate(false); 296 + setOpenDelete(true); 297 + }} 298 + 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 + > 300 + <span class="iconify lucide--trash-2 text-red-500 dark:text-red-400"></span> 301 + </button> 302 + </Tooltip> 303 + </Show> 304 + <PermissionButton 305 + scope="delete" 306 + class="flex items-center gap-1 rounded-md px-2 py-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 307 + disabledClass="flex items-center gap-1 rounded-md px-2 py-1 opacity-40" 308 + onClick={() => { 309 + setRecords({ from: 0, to: records.length - 1 }, "toDelete", false); 310 + setLastSelected(undefined); 311 + setBatchDelete(!batchDelete()); 312 + }} 313 + > 314 + <span 315 + class={`iconify ${batchDelete() ? "lucide--x" : "lucide--square-check-big"}`} 316 + ></span> 317 + {batchDelete() ? "Cancel" : "Manage"} 318 + </PermissionButton> 319 + </div> 320 + </Show> 321 + </div> 322 + 323 + {/* Record list */} 324 + <div class="flex max-w-full flex-col px-2 pb-20 font-mono"> 325 + <Show 326 + when={filteredRecords().length > 0} 327 + fallback={ 328 + <span class="font-sans text-neutral-500 dark:text-neutral-400"> 329 + {filter() ? "No records match filter" : "No records"} 330 + </span> 331 + } 332 + > 333 + <For each={filteredRecords()}> 334 + {(record, index) => { 335 + const rounding = () => { 336 + const recs = filteredRecords(); 337 + const prevSelected = recs[index() - 1]?.toDelete; 338 + const nextSelected = recs[index() + 1]?.toDelete; 339 + return `${!prevSelected ? "rounded-t" : ""} ${!nextSelected ? "rounded-b" : ""}`; 340 + }; 341 + return ( 342 + <> 343 + <Show when={batchDelete()}> 344 + <div 345 + class={`select-none ${ 346 + record.toDelete ? 347 + `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()}` 348 + : "rounded hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 349 + }`} 350 + onclick={(e) => { 351 + handleSelectionClick(e, index()); 352 + setRecords(index(), "toDelete", !record.toDelete); 353 + }} 354 + > 355 + <RecordLink record={record} /> 356 + </div> 378 357 </Show> 379 - <Show when={response.loading}> 380 - <div class="iconify lucide--loader-circle w-20 animate-spin text-lg" /> 358 + <Show when={!batchDelete()}> 359 + <A 360 + href={`/at://${did}/${params.collection}/${record.rkey}`} 361 + class="rounded select-none hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 362 + > 363 + <RecordLink record={record} /> 364 + </A> 381 365 </Show> 382 - </Show> 383 - </div> 384 - </div> 366 + </> 367 + ); 368 + }} 369 + </For> 370 + </Show> 371 + </div> 372 + </div> 373 + 374 + {/* Confirm delete/recreate modal */} 375 + <Modal 376 + open={openDelete()} 377 + onClose={() => setOpenDelete(false)} 378 + contentClass="dark:bg-dark-300 dark:shadow-dark-700 pointer-events-auto rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-4 shadow-md dark:border-neutral-700" 379 + > 380 + <h2 class="mb-2 font-semibold"> 381 + {recreate() ? "Recreate" : "Delete"} {records.filter((r) => r.toDelete).length} records? 382 + </h2> 383 + <div class="flex justify-end gap-2"> 384 + <Button onClick={() => setOpenDelete(false)}>Cancel</Button> 385 + <Button 386 + onClick={deleteRecords} 387 + classList={{ 388 + "bg-blue-500! text-white! hover:bg-blue-600! active:bg-blue-700! dark:bg-blue-600! dark:hover:bg-blue-500! dark:active:bg-blue-400! border-none!": 389 + recreate(), 390 + "text-white! border-none! bg-red-500! hover:bg-red-600! active:bg-red-700!": 391 + !recreate(), 392 + }} 393 + > 394 + {recreate() ? "Recreate" : "Delete"} 395 + </Button> 396 + </div> 397 + </Modal> 398 + 399 + {/* Fixed bottom panel */} 400 + <Show when={records.length > 1}> 401 + <div class="dark:bg-dark-500 fixed bottom-0 z-10 flex w-full flex-col items-center gap-2 border-t border-neutral-200 bg-neutral-100 px-3 pt-3 pb-6 dark:border-neutral-700"> 402 + {/* Filter */} 403 + <div 404 + class="dark:bg-dark-200 flex w-full max-w-lg cursor-text items-center gap-2 rounded-lg border border-neutral-200 bg-white px-3 dark:border-neutral-700" 405 + onClick={(e) => { 406 + const input = e.currentTarget.querySelector("input"); 407 + if (e.target !== input) input?.focus(); 408 + }} 409 + > 410 + <span class="iconify lucide--filter text-neutral-500 dark:text-neutral-400"></span> 411 + <input 412 + ref={filterInputRef} 413 + type="text" 414 + spellcheck={false} 415 + autocapitalize="off" 416 + autocomplete="off" 417 + class="grow py-2 select-none placeholder:text-sm focus:outline-none" 418 + placeholder="Filter records..." 419 + onInput={(e) => setFilter(e.currentTarget.value)} 420 + /> 421 + <Show when={canHover && !filter()}> 422 + <kbd class="rounded border border-neutral-200 bg-neutral-50 px-1.5 py-0.5 font-mono text-xs text-neutral-400 select-none dark:border-neutral-600 dark:bg-neutral-700"> 423 + / 424 + </kbd> 385 425 </Show> 386 426 </div> 387 - </StickyOverlay> 388 - <div class="flex max-w-full flex-col px-2 font-mono"> 389 - <For each={filteredRecords()}> 390 - {(record, index) => { 391 - const rounding = () => { 392 - const recs = filteredRecords(); 393 - const prevSelected = recs[index() - 1]?.toDelete; 394 - const nextSelected = recs[index() + 1]?.toDelete; 395 - return `${!prevSelected ? "rounded-t" : ""} ${!nextSelected ? "rounded-b" : ""}`; 396 - }; 397 - return ( 398 - <> 399 - <Show when={batchDelete()}> 400 - <div 401 - class={`select-none ${ 402 - record.toDelete ? 403 - `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()}` 404 - : "rounded hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 405 - }`} 406 - onclick={(e) => { 407 - handleSelectionClick(e, index()); 408 - setRecords(index(), "toDelete", !record.toDelete); 409 - }} 410 - > 411 - <RecordLink record={record} /> 412 - </div> 427 + 428 + {/* Pagination */} 429 + <div class="flex w-full max-w-lg items-center justify-between"> 430 + <Button 431 + onClick={() => { 432 + const newReverse = !reverse(); 433 + setReverse(newReverse); 434 + setSearchParams({ reverse: newReverse ? "true" : undefined }); 435 + setCursor(undefined); 436 + setRestoredFromCache(false); 437 + clearCollectionCache(cacheKey()); 438 + refetch(); 439 + }} 440 + classList={{ 441 + "text-blue-500! dark:text-blue-400! border-blue-500! dark:border-blue-400!": 442 + reverse(), 443 + }} 444 + > 445 + <span 446 + class={`iconify ${reverse() ? "lucide--arrow-down-wide-narrow" : "lucide--arrow-up-narrow-wide"}`} 447 + ></span> 448 + Reverse 449 + </Button> 450 + 451 + {/* Record count */} 452 + <div> 453 + <Show when={batchDelete()}> 454 + <span>{records.filter((rec) => rec.toDelete).length}</span> 455 + <span>/</span> 456 + </Show> 457 + <span>{filter() ? filteredRecords().length : records.length} records</span> 458 + </div> 459 + 460 + {/* Load more */} 461 + <div class="flex w-20 items-center justify-end"> 462 + <Show when={cursor()}> 463 + <Button 464 + onClick={() => refetch()} 465 + disabled={response.loading} 466 + classList={{ "w-20 h-7.5 justify-center": true }} 467 + > 468 + <Show 469 + when={!response.loading} 470 + fallback={ 471 + <span class="iconify lucide--loader-circle animate-spin text-base" /> 472 + } 473 + > 474 + Load more 413 475 </Show> 414 - <Show when={!batchDelete()}> 415 - <A 416 - href={`/at://${did}/${params.collection}/${record.rkey}`} 417 - class="rounded select-none hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 418 - > 419 - <RecordLink record={record} /> 420 - </A> 421 - </Show> 422 - </> 423 - ); 424 - }} 425 - </For> 476 + </Button> 477 + </Show> 478 + </div> 479 + </div> 426 480 </div> 427 - </div> 481 + </Show> 428 482 </Show> 429 483 </> 430 484 );
+3 -3
src/views/pds.tsx
··· 147 147 <Tab tab="firehose" label="Firehose" /> 148 148 </div> 149 149 <Show when={!location.hash || location.hash === "#repos"}> 150 - <div class="-mx-2 flex flex-col pb-20"> 150 + <div class="-mx-2 flex flex-col pb-12"> 151 151 <For each={repos()}>{(repo) => <RepoCard {...repo} />}</For> 152 152 </div> 153 153 </Show> ··· 243 243 </div> 244 244 </div> 245 245 <Show when={!location.hash || location.hash === "#repos"}> 246 - <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center bg-neutral-100 pt-2 pb-4"> 247 - <div class="flex items-center gap-3 pb-2"> 246 + <div class="dark:bg-dark-500 fixed bottom-0 z-5 flex w-screen justify-center border-t border-neutral-200 bg-neutral-100 pt-3 pb-6 dark:border-neutral-700"> 247 + <div class="flex items-center gap-3"> 248 248 <p> 249 249 {repos()?.length} loaded 250 250 <Show when={repos()?.some((r) => !r.active)}>
+1 -1
src/views/repo.tsx
··· 463 463 return ( 464 464 <div 465 465 id={`collection-${authority}`} 466 - class="group flex items-start gap-2 rounded-lg p-1 transition-colors scroll-mt-4" 466 + class="group flex scroll-mt-4 items-start gap-2 rounded-lg p-1 transition-colors" 467 467 classList={{ 468 468 "dark:hover:bg-dark-300 hover:bg-neutral-200": !isHighlighted(), 469 469 "bg-blue-100 dark:bg-blue-500/25": isHighlighted(),