Compare changes

Choose any two refs to compare.

+22
.tangled/workflows/deploy.yml
···
··· 1 + ## need this for commit idk what else to change 2 + 3 + when: 4 + - event: ["push"] 5 + branch: ["main"] 6 + 7 + engine: "nixery" 8 + 9 + clone: 10 + skip: true 11 + 12 + dependencies: 13 + nixpkgs: 14 + - curl 15 + 16 + steps: 17 + - name: "Trigger Deploy" 18 + command: | 19 + curl -X POST \ 20 + -H "Authorization: Bearer $SCANS_HOST_API_KEY" \ 21 + -H "Authorization: Bearer $SCANS_HOST_API_KEY" \ 22 + https://free.scan.blue/api/v1/sites/jy35AeguTwaqDy_3ufq09/deploy?wait=true
+25
0001-ok.patch
···
··· 1 + From baf405c82fb23f9274a35384286bac2b901d45af Mon Sep 17 00:00:00 2001 2 + From: scanash00 <scan@scanash.com> 3 + Date: Tue, 30 Dec 2025 22:12:13 -0900 4 + Subject: [PATCH] add ?wait=true 5 + 6 + --- 7 + .tangled/workflows/deploy.yml | 3 ++- 8 + 1 file changed, 2 insertions(+), 1 deletion(-) 9 + 10 + diff --git a/.tangled/workflows/deploy.yml b/.tangled/workflows/deploy.yml 11 + index a44c51b..b7edfea 100644 12 + --- a/.tangled/workflows/deploy.yml 13 + +++ b/.tangled/workflows/deploy.yml 14 + @@ -16,4 +16,5 @@ steps: 15 + command: | 16 + curl -X POST \ 17 + -H "Authorization: Bearer $SCANS_HOST_API_KEY" \ 18 + - https://free.scan.blue/api/v1/sites/jy35AeguTwaqDy_3ufq09/deploy 19 + \ No newline at end of file 20 + + -H "Authorization: Bearer $SCANS_HOST_API_KEY" \ 21 + + https://free.scan.blue/api/v1/sites/YOUR_SITE_ID/deploy?wait=true 22 + \ No newline at end of file 23 + -- 24 + 2.50.1 (Apple Git-155) 25 +
+1 -1
src/auth/account.tsx
··· 140 > 141 <span class="truncate">{sessions[did]?.handle || did}</span> 142 <Show when={did === agent()?.sub && sessions[did].signedIn}> 143 - <span class="iconify lucide--check shrink-0 text-green-500 dark:text-green-400"></span> 144 </Show> 145 <Show when={!sessions[did].signedIn}> 146 <span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span>
··· 140 > 141 <span class="truncate">{sessions[did]?.handle || did}</span> 142 <Show when={did === agent()?.sub && sessions[did].signedIn}> 143 + <span class="iconify lucide--circle-check shrink-0 text-blue-500 dark:text-blue-400"></span> 144 </Show> 145 <Show when={!sessions[did].signedIn}> 146 <span class="iconify lucide--circle-alert shrink-0 text-red-500 dark:text-red-400"></span>
+1 -1
src/auth/login.tsx
··· 49 <label for="username" class="hidden"> 50 Add account 51 </label> 52 - <div class="dark:bg-dark-100 dark:inset-shadow-dark-200 flex grow items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 inset-shadow-xs focus-within:outline-[1px] focus-within:outline-neutral-600 dark:border-neutral-600 dark:focus-within:outline-neutral-400"> 53 <label 54 for="username" 55 class="iconify lucide--user-round-plus shrink-0 text-neutral-500 dark:text-neutral-400"
··· 49 <label for="username" class="hidden"> 50 Add account 51 </label> 52 + <div class="dark:bg-dark-100 flex grow items-center gap-2 rounded-lg bg-white px-2 outline-1 outline-neutral-200 focus-within:outline-[1.5px] focus-within:outline-neutral-600 dark:outline-neutral-600 dark:focus-within:outline-neutral-400"> 53 <label 54 for="username" 55 class="iconify lucide--user-round-plus shrink-0 text-neutral-500 dark:text-neutral-400"
+1 -1
src/components/backlinks.tsx
··· 51 return ( 52 <a 53 href={`/at://${did}/${collection}/${rkey}`} 54 - class="grid grid-cols-[auto_1fr_auto] items-center gap-x-1 px-2 py-1.5 font-mono text-xs hover:bg-neutral-200/50 sm:gap-x-3 sm:px-3 dark:hover:bg-neutral-700/50" 55 > 56 <span class="text-blue-500 dark:text-blue-400">{rkey}</span> 57 <span class="truncate text-neutral-700 dark:text-neutral-300" title={did}>
··· 51 return ( 52 <a 53 href={`/at://${did}/${collection}/${rkey}`} 54 + class="grid grid-cols-[auto_1fr_auto] items-center gap-x-1 px-2 py-1.5 font-mono text-xs select-none hover:bg-neutral-200/50 active:bg-neutral-200/50 sm:gap-x-3 sm:px-3 dark:hover:bg-neutral-700/50 dark:active:bg-neutral-700/50" 55 > 56 <span class="text-blue-500 dark:text-blue-400">{rkey}</span> 57 <span class="truncate text-neutral-700 dark:text-neutral-300" title={did}>
+7 -2
src/components/dropdown.tsx
··· 75 export const ActionMenu = (props: { 76 label: string; 77 icon: string; 78 - onClick: JSX.EventHandlerUnion<HTMLButtonElement, MouseEvent>; 79 }) => { 80 return ( 81 <button 82 - onClick={props.onClick} 83 class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 84 > 85 <Show when={props.icon}>
··· 75 export const ActionMenu = (props: { 76 label: string; 77 icon: string; 78 + onClick: () => void; 79 }) => { 80 + const ctx = useContext(MenuContext); 81 + 82 return ( 83 <button 84 + onClick={() => { 85 + props.onClick(); 86 + ctx?.setShowMenu(false); 87 + }} 88 class="flex items-center gap-2 rounded-md p-1.5 whitespace-nowrap hover:bg-neutral-200/50 active:bg-neutral-200 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 89 > 90 <Show when={props.icon}>
+70 -56
src/components/json.tsx
··· 1 import { isCid, isDid, isNsid, isResourceUri, Nsid } from "@atcute/lexicons/syntax"; 2 import { A, useNavigate, useParams } from "@solidjs/router"; 3 - import { createEffect, createSignal, ErrorBoundary, For, on, Show } from "solid-js"; 4 import { resolveLexiconAuthority } from "../utils/api"; 5 import { hideMedia } from "../views/settings"; 6 import { pds } from "./navbar"; 7 import { addNotification, removeNotification } from "./notification"; 8 import VideoPlayer from "./video-player"; 9 10 interface AtBlob { 11 $type: string; 12 ref: { $link: string }; 13 mimeType: string; 14 } 15 16 - const JSONString = (props: { 17 - data: string; 18 - isType?: boolean; 19 - isLink?: boolean; 20 - parentIsBlob?: boolean; 21 - }) => { 22 const navigate = useNavigate(); 23 const params = useParams(); 24 25 - const isURL = 26 - URL.canParse ?? 27 - ((url, base) => { 28 - try { 29 - new URL(url, base); 30 - return true; 31 - } catch { 32 - return false; 33 - } 34 - }); 35 - 36 const handleClick = async (lex: string) => { 37 try { 38 const [nsid, anchor] = lex.split("#"); ··· 50 } 51 }; 52 53 return ( 54 <span> 55 " 56 - <For each={props.data.split(/(\s)/)}> 57 {(part) => ( 58 <> 59 {isResourceUri(part) ? ··· 72 > 73 {part} 74 </button> 75 - : isCid(part) && props.isLink && props.parentIsBlob && params.repo ? 76 <A 77 class="text-blue-400 hover:underline active:underline" 78 rel="noopener" ··· 93 </> 94 )} 95 </For> 96 " 97 </span> 98 ); 99 }; ··· 110 return <span>null</span>; 111 }; 112 113 - const JSONObject = (props: { 114 - data: { [x: string]: JSONType }; 115 - repo: string; 116 - parentIsBlob?: boolean; 117 - }) => { 118 const params = useParams(); 119 const [hide, setHide] = createSignal( 120 localStorage.hideMedia === "true" || params.rkey === undefined, ··· 136 ); 137 138 const isBlob = props.data.$type === "blob"; 139 - const isBlobContext = isBlob || props.parentIsBlob; 140 141 const Obj = ({ key, value }: { key: string; value: JSONType }) => { 142 const [show, setShow] = createSignal(true); ··· 169 "self-center": value !== Object(value), 170 "pl-[calc(2ch-0.5px)] border-l-[0.5px] border-neutral-500/50 dark:border-neutral-400/50 has-hover:group-hover/indent:border-neutral-700 transition-colors dark:has-hover:group-hover/indent:border-neutral-300": 171 value === Object(value), 172 - "invisible h-0": !show(), 173 }} 174 > 175 - <JSONValue 176 - data={value} 177 - repo={props.repo} 178 - isType={key === "$type"} 179 - isLink={key === "$link"} 180 - parentIsBlob={isBlobContext} 181 - /> 182 </span> 183 </span> 184 ); ··· 200 <Show when={blob.mimeType.startsWith("image/")}> 201 <img 202 class="h-auto max-h-48 max-w-48 object-contain sm:max-h-64 sm:max-w-64" 203 - src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${props.repo}&cid=${blob.ref.$link}`} 204 onLoad={() => setMediaLoaded(true)} 205 /> 206 </Show> 207 <Show when={blob.mimeType === "video/mp4"}> 208 <ErrorBoundary fallback={() => <span>Failed to load video</span>}> 209 <VideoPlayer 210 - did={props.repo} 211 cid={blob.ref.$link} 212 onLoad={() => setMediaLoaded(true)} 213 /> ··· 241 return rawObj; 242 }; 243 244 - const JSONArray = (props: { data: JSONType[]; repo: string; parentIsBlob?: boolean }) => { 245 return ( 246 <For each={props.data}> 247 {(value, index) => ( ··· 252 }} 253 > 254 <span class="ml-[1ch] w-full"> 255 - <JSONValue data={value} repo={props.repo} parentIsBlob={props.parentIsBlob} /> 256 </span> 257 </span> 258 )} ··· 260 ); 261 }; 262 263 - export const JSONValue = (props: { 264 - data: JSONType; 265 - repo: string; 266 - isType?: boolean; 267 - isLink?: boolean; 268 - parentIsBlob?: boolean; 269 - }) => { 270 const data = props.data; 271 if (typeof data === "string") 272 - return ( 273 - <JSONString 274 - data={data} 275 - isType={props.isType} 276 - isLink={props.isLink} 277 - parentIsBlob={props.parentIsBlob} 278 - /> 279 - ); 280 if (typeof data === "number") return <JSONNumber data={data} />; 281 if (typeof data === "boolean") return <JSONBoolean data={data} />; 282 if (data === null) return <JSONNull />; 283 - if (Array.isArray(data)) 284 - return <JSONArray data={data} repo={props.repo} parentIsBlob={props.parentIsBlob} />; 285 - return <JSONObject data={data} repo={props.repo} parentIsBlob={props.parentIsBlob} />; 286 }; 287 288 export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];
··· 1 import { isCid, isDid, isNsid, isResourceUri, Nsid } from "@atcute/lexicons/syntax"; 2 import { A, useNavigate, useParams } from "@solidjs/router"; 3 + import { 4 + createContext, 5 + createEffect, 6 + createSignal, 7 + ErrorBoundary, 8 + For, 9 + on, 10 + Show, 11 + useContext, 12 + } from "solid-js"; 13 import { resolveLexiconAuthority } from "../utils/api"; 14 import { hideMedia } from "../views/settings"; 15 import { pds } from "./navbar"; 16 import { addNotification, removeNotification } from "./notification"; 17 import VideoPlayer from "./video-player"; 18 19 + interface JSONContext { 20 + repo: string; 21 + truncate?: boolean; 22 + parentIsBlob?: boolean; 23 + } 24 + 25 + const JSONCtx = createContext<JSONContext>(); 26 + const useJSONCtx = () => useContext(JSONCtx)!; 27 + 28 interface AtBlob { 29 $type: string; 30 ref: { $link: string }; 31 mimeType: string; 32 } 33 34 + const isURL = 35 + URL.canParse ?? 36 + ((url, base) => { 37 + try { 38 + new URL(url, base); 39 + return true; 40 + } catch { 41 + return false; 42 + } 43 + }); 44 + 45 + const JSONString = (props: { data: string; isType?: boolean; isLink?: boolean }) => { 46 + const ctx = useJSONCtx(); 47 const navigate = useNavigate(); 48 const params = useParams(); 49 50 const handleClick = async (lex: string) => { 51 try { 52 const [nsid, anchor] = lex.split("#"); ··· 64 } 65 }; 66 67 + const MAX_LENGTH = 200; 68 + const isTruncated = () => ctx.truncate && props.data.length > MAX_LENGTH; 69 + const displayData = () => (isTruncated() ? props.data.slice(0, MAX_LENGTH) : props.data); 70 + const remainingChars = () => props.data.length - MAX_LENGTH; 71 + 72 return ( 73 <span> 74 " 75 + <For each={displayData().split(/(\s)/)}> 76 {(part) => ( 77 <> 78 {isResourceUri(part) ? ··· 91 > 92 {part} 93 </button> 94 + : isCid(part) && props.isLink && ctx.parentIsBlob && params.repo ? 95 <A 96 class="text-blue-400 hover:underline active:underline" 97 rel="noopener" ··· 112 </> 113 )} 114 </For> 115 + <Show when={isTruncated()}> 116 + <span>โ€ฆ</span> 117 + </Show> 118 " 119 + <Show when={isTruncated()}> 120 + <span class="ml-1 text-neutral-500 dark:text-neutral-400"> 121 + (+{remainingChars().toLocaleString()}) 122 + </span> 123 + </Show> 124 </span> 125 ); 126 }; ··· 137 return <span>null</span>; 138 }; 139 140 + const JSONObject = (props: { data: { [x: string]: JSONType } }) => { 141 + const ctx = useJSONCtx(); 142 const params = useParams(); 143 const [hide, setHide] = createSignal( 144 localStorage.hideMedia === "true" || params.rkey === undefined, ··· 160 ); 161 162 const isBlob = props.data.$type === "blob"; 163 + const isBlobContext = isBlob || ctx.parentIsBlob; 164 165 const Obj = ({ key, value }: { key: string; value: JSONType }) => { 166 const [show, setShow] = createSignal(true); ··· 193 "self-center": value !== Object(value), 194 "pl-[calc(2ch-0.5px)] border-l-[0.5px] border-neutral-500/50 dark:border-neutral-400/50 has-hover:group-hover/indent:border-neutral-700 transition-colors dark:has-hover:group-hover/indent:border-neutral-300": 195 value === Object(value), 196 + "invisible h-0 overflow-hidden": !show(), 197 }} 198 > 199 + <JSONCtx.Provider value={{ ...ctx, parentIsBlob: isBlobContext }}> 200 + <JSONValueInner data={value} isType={key === "$type"} isLink={key === "$link"} /> 201 + </JSONCtx.Provider> 202 </span> 203 </span> 204 ); ··· 220 <Show when={blob.mimeType.startsWith("image/")}> 221 <img 222 class="h-auto max-h-48 max-w-48 object-contain sm:max-h-64 sm:max-w-64" 223 + src={`https://${pds()}/xrpc/com.atproto.sync.getBlob?did=${ctx.repo}&cid=${blob.ref.$link}`} 224 onLoad={() => setMediaLoaded(true)} 225 /> 226 </Show> 227 <Show when={blob.mimeType === "video/mp4"}> 228 <ErrorBoundary fallback={() => <span>Failed to load video</span>}> 229 <VideoPlayer 230 + did={ctx.repo} 231 cid={blob.ref.$link} 232 onLoad={() => setMediaLoaded(true)} 233 /> ··· 261 return rawObj; 262 }; 263 264 + const JSONArray = (props: { data: JSONType[] }) => { 265 return ( 266 <For each={props.data}> 267 {(value, index) => ( ··· 272 }} 273 > 274 <span class="ml-[1ch] w-full"> 275 + <JSONValueInner data={value} /> 276 </span> 277 </span> 278 )} ··· 280 ); 281 }; 282 283 + const JSONValueInner = (props: { data: JSONType; isType?: boolean; isLink?: boolean }) => { 284 const data = props.data; 285 if (typeof data === "string") 286 + return <JSONString data={data} isType={props.isType} isLink={props.isLink} />; 287 if (typeof data === "number") return <JSONNumber data={data} />; 288 if (typeof data === "boolean") return <JSONBoolean data={data} />; 289 if (data === null) return <JSONNull />; 290 + if (Array.isArray(data)) return <JSONArray data={data} />; 291 + return <JSONObject data={data} />; 292 + }; 293 + 294 + export const JSONValue = (props: { data: JSONType; repo: string; truncate?: boolean }) => { 295 + return ( 296 + <JSONCtx.Provider value={{ repo: props.repo, truncate: props.truncate }}> 297 + <JSONValueInner data={props.data} /> 298 + </JSONCtx.Provider> 299 + ); 300 }; 301 302 export type JSONType = string | number | boolean | null | { [x: string]: JSONType } | JSONType[];
+4 -4
src/components/search.tsx
··· 188 <label for="input" class="hidden"> 189 PDS URL, AT URI, NSID, DID, or handle 190 </label> 191 - <div class="dark:bg-dark-100 dark:inset-shadow-dark-200 flex items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 inset-shadow-xs focus-within:outline-[1px] focus-within:outline-neutral-600 dark:border-neutral-600 dark:focus-within:outline-neutral-400"> 192 <label 193 for="input" 194 class="iconify lucide--search text-neutral-500 dark:text-neutral-400" ··· 312 src={actor.avatar?.replace("img/avatar/", "img/avatar_thumbnail/")} 313 class="size-9 rounded-full" 314 /> 315 - <div class="flex flex-col"> 316 <Show when={actor.displayName}> 317 - <span class="text-sm font-medium">{actor.displayName}</span> 318 </Show> 319 - <span class="text-xs text-neutral-600 dark:text-neutral-400"> 320 @{actor.handle} 321 </span> 322 </div>
··· 188 <label for="input" class="hidden"> 189 PDS URL, AT URI, NSID, DID, or handle 190 </label> 191 + <div class="dark:bg-dark-100 flex items-center gap-2 rounded-lg bg-white px-2 outline-1 outline-neutral-200 focus-within:outline-[1.5px] focus-within:outline-neutral-600 dark:outline-neutral-600 dark:focus-within:outline-neutral-400"> 192 <label 193 for="input" 194 class="iconify lucide--search text-neutral-500 dark:text-neutral-400" ··· 312 src={actor.avatar?.replace("img/avatar/", "img/avatar_thumbnail/")} 313 class="size-9 rounded-full" 314 /> 315 + <div class="flex min-w-0 flex-col"> 316 <Show when={actor.displayName}> 317 + <span class="truncate text-sm font-medium">{actor.displayName}</span> 318 </Show> 319 + <span class="truncate text-xs text-neutral-600 dark:text-neutral-400"> 320 @{actor.handle} 321 </span> 322 </div>
+1 -1
src/components/text-input.tsx
··· 25 disabled={props.disabled} 26 required={props.required} 27 class={ 28 - "dark:bg-dark-100 dark:inset-shadow-dark-200 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 inset-shadow-xs select-none placeholder:text-sm focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400 " + 29 props.class 30 } 31 onInput={props.onInput}
··· 25 disabled={props.disabled} 26 required={props.required} 27 class={ 28 + "dark:bg-dark-100 rounded-lg bg-white px-2 py-1 outline-1 outline-neutral-200 select-none placeholder:text-sm focus:outline-[1.5px] focus:outline-neutral-600 dark:outline-neutral-600 dark:focus:outline-neutral-400 " + 29 props.class 30 } 31 onInput={props.onInput}
+2 -2
src/layout.tsx
··· 118 }); 119 120 return ( 121 - <div id="main" class="mx-auto mb-8 flex max-w-lg flex-col items-center p-4"> 122 <MetaProvider> 123 <Show when={location.pathname !== "/"}> 124 <Meta name="robots" content="noindex, nofollow" /> ··· 151 <DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-lg p-1.5"> 152 <NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" /> 153 <NavMenu href="/firehose" label="Firehose" icon="lucide--droplet" /> 154 - <NavMenu href="/labels" label="Labels" icon="lucide--tags" /> 155 <NavMenu href="/settings" label="Settings" icon="lucide--settings" /> 156 <MenuSeparator /> 157 <NavMenu
··· 118 }); 119 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" /> ··· 151 <DropdownMenu icon="lucide--menu text-lg" buttonClass="rounded-lg p-1.5"> 152 <NavMenu href="/jetstream" label="Jetstream" icon="lucide--radio-tower" /> 153 <NavMenu href="/firehose" label="Firehose" icon="lucide--droplet" /> 154 + <NavMenu href="/labels" label="Labels" icon="lucide--tag" /> 155 <NavMenu href="/settings" label="Settings" icon="lucide--settings" /> 156 <MenuSeparator /> 157 <NavMenu
+24
src/utils/route-cache.ts
···
··· 1 + import { createStore } from "solid-js/store"; 2 + 3 + export interface CollectionCacheEntry { 4 + records: unknown[]; 5 + cursor: string | undefined; 6 + scrollY: number; 7 + reverse: boolean; 8 + } 9 + 10 + type RouteCache = Record<string, CollectionCacheEntry>; 11 + 12 + const [routeCache, setRouteCache] = createStore<RouteCache>({}); 13 + 14 + export const getCollectionCache = (key: string): CollectionCacheEntry | undefined => { 15 + return routeCache[key]; 16 + }; 17 + 18 + export const setCollectionCache = (key: string, entry: CollectionCacheEntry): void => { 19 + setRouteCache(key, entry); 20 + }; 21 + 22 + export const clearCollectionCache = (key: string): void => { 23 + setRouteCache(key, undefined!); 24 + };
+4 -3
src/views/blob.tsx
··· 30 return ( 31 <div class="flex flex-col items-center gap-2"> 32 <Show when={blobs() || response()}> 33 - <div class="flex w-full flex-col gap-0.5 font-mono text-xs wrap-anywhere"> 34 <For each={blobs()}> 35 {(cid) => ( 36 <a 37 href={`${props.pds}/xrpc/com.atproto.sync.getBlob?did=${props.repo}&cid=${cid}`} 38 target="_blank" 39 - class="w-fit rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 40 > 41 - <span class="text-blue-400">{cid}</span> 42 </a> 43 )} 44 </For>
··· 30 return ( 31 <div class="flex flex-col items-center gap-2"> 32 <Show when={blobs() || response()}> 33 + <div class="flex w-full flex-col gap-0.5 pb-20 font-mono text-xs sm:text-sm"> 34 <For each={blobs()}> 35 {(cid) => ( 36 <a 37 href={`${props.pds}/xrpc/com.atproto.sync.getBlob?did=${props.repo}&cid=${cid}`} 38 target="_blank" 39 + class="truncate rounded px-0.5 text-left text-blue-400 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 40 + dir="rtl" 41 > 42 + {cid} 43 </a> 44 )} 45 </For>
+75 -21
src/views/collection.tsx
··· 2 import { Client, simpleFetchHandler } from "@atcute/client"; 3 import { $type, ActorIdentifier, InferXRPCBodyOutput } from "@atcute/lexicons"; 4 import * as TID from "@atcute/tid"; 5 - import { A, useParams } from "@solidjs/router"; 6 - import { createEffect, createMemo, createResource, createSignal, For, Show } from "solid-js"; 7 import { createStore } from "solid-js/store"; 8 import { hasUserScope } from "../auth/scope-utils"; 9 import { agent } from "../auth/state"; ··· 17 import { isTouchDevice } from "../layout.jsx"; 18 import { resolvePDS } from "../utils/api.js"; 19 import { localDateFromTimestamp } from "../utils/date.js"; 20 21 interface AtprotoRecord { 22 rkey: string; ··· 43 44 return ( 45 <span 46 - class="relative flex w-full min-w-0 items-baseline rounded px-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 47 ref={rkeyRef} 48 onmouseover={() => !isTouchDevice && setHover(true)} 49 onmouseleave={() => !isTouchDevice && setHover(false)} 50 > 51 <span class="flex items-baseline truncate"> 52 - <span class="shrink-0 text-sm text-blue-400 sm:text-base">{props.record.rkey}</span> 53 <span class="ml-1 truncate text-xs text-neutral-500 dark:text-neutral-400" dir="rtl"> 54 {props.record.cid} 55 </span> ··· 67 <JSONValue 68 data={props.record.record.value as JSONType} 69 repo={props.record.record.uri.split("/")[2]} 70 /> 71 </span> 72 </Show> ··· 84 const [reverse, setReverse] = createSignal(false); 85 const [recreate, setRecreate] = createSignal(false); 86 const [openDelete, setOpenDelete] = createSignal(false); 87 const did = params.repo; 88 let pds: string; 89 let rpc: Client; 90 91 const fetchRecords = async () => { 92 if (!pds) pds = await resolvePDS(did!); 93 if (!rpc) rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 94 const res = await rpc.get("com.atproto.repo.listRecords", { ··· 167 setCursor(undefined); 168 setOpenDelete(false); 169 setRecreate(false); 170 refetch(); 171 }; 172 ··· 211 setLastSelected(undefined); 212 setBatchDelete(!batchDelete()); 213 }} 214 - class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 215 > 216 <span 217 - class={`iconify text-lg ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `} 218 ></span> 219 </button> 220 } ··· 225 children={ 226 <button 227 onclick={() => selectAll()} 228 - class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 229 > 230 - <span class="iconify lucide--copy-check text-lg"></span> 231 </button> 232 } 233 /> ··· 240 setRecreate(true); 241 setOpenDelete(true); 242 }} 243 - class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 244 > 245 - <span class="iconify lucide--recycle text-lg text-green-500 dark:text-green-400"></span> 246 </button> 247 } 248 /> ··· 255 setRecreate(false); 256 setOpenDelete(true); 257 }} 258 - class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 259 > 260 - <span class="iconify lucide--trash-2 text-lg text-red-500 dark:text-red-400"></span> 261 </button> 262 } 263 /> ··· 281 </div> 282 </Modal> 283 </Show> 284 - <Tooltip text="Jetstream"> 285 - <A 286 - href={`/jetstream?collections=${params.collection}&dids=${params.repo}`} 287 - class="flex items-center rounded-md p-1 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 288 - > 289 - <span class="iconify lucide--radio-tower text-lg"></span> 290 - </A> 291 - </Tooltip> 292 <TextInput 293 name="Filter" 294 placeholder="Filter by substring" 295 onInput={(e) => setFilter(e.currentTarget.value)} 296 class="grow" 297 /> 298 </div> 299 <Show when={records.length > 1}> 300 <div class="flex items-center justify-between gap-x-2"> ··· 303 setReverse(!reverse()); 304 setRecords([]); 305 setCursor(undefined); 306 refetch(); 307 }} 308 > ··· 350 </label> 351 </Show> 352 <Show when={!batchDelete()}> 353 - <A href={`/at://${did}/${params.collection}/${record.rkey}`}> 354 <RecordLink record={record} /> 355 </A> 356 </Show>
··· 2 import { Client, simpleFetchHandler } from "@atcute/client"; 3 import { $type, ActorIdentifier, InferXRPCBodyOutput } from "@atcute/lexicons"; 4 import * as TID from "@atcute/tid"; 5 + import { A, useBeforeLeave, useParams } from "@solidjs/router"; 6 + import { 7 + createEffect, 8 + createMemo, 9 + createResource, 10 + createSignal, 11 + For, 12 + onMount, 13 + Show, 14 + } from "solid-js"; 15 import { createStore } from "solid-js/store"; 16 import { hasUserScope } from "../auth/scope-utils"; 17 import { agent } from "../auth/state"; ··· 25 import { isTouchDevice } from "../layout.jsx"; 26 import { resolvePDS } from "../utils/api.js"; 27 import { localDateFromTimestamp } from "../utils/date.js"; 28 + import { 29 + clearCollectionCache, 30 + getCollectionCache, 31 + setCollectionCache, 32 + } from "../utils/route-cache.js"; 33 34 interface AtprotoRecord { 35 rkey: string; ··· 56 57 return ( 58 <span 59 + class="relative flex w-full min-w-0 items-baseline rounded p-0.5 hover:bg-neutral-200 active:bg-neutral-300 dark:hover:bg-neutral-700 dark:active:bg-neutral-600" 60 ref={rkeyRef} 61 onmouseover={() => !isTouchDevice && setHover(true)} 62 onmouseleave={() => !isTouchDevice && setHover(false)} 63 > 64 <span class="flex items-baseline truncate"> 65 + <span class="shrink-0 text-sm text-blue-400">{props.record.rkey}</span> 66 <span class="ml-1 truncate text-xs text-neutral-500 dark:text-neutral-400" dir="rtl"> 67 {props.record.cid} 68 </span> ··· 80 <JSONValue 81 data={props.record.record.value as JSONType} 82 repo={props.record.record.uri.split("/")[2]} 83 + truncate 84 /> 85 </span> 86 </Show> ··· 98 const [reverse, setReverse] = createSignal(false); 99 const [recreate, setRecreate] = createSignal(false); 100 const [openDelete, setOpenDelete] = createSignal(false); 101 + const [restoredFromCache, setRestoredFromCache] = createSignal(false); 102 const did = params.repo; 103 let pds: string; 104 let rpc: Client; 105 106 + const cacheKey = () => `${params.pds}/${params.repo}/${params.collection}`; 107 + 108 + onMount(() => { 109 + const cached = getCollectionCache(cacheKey()); 110 + if (cached) { 111 + setRecords(cached.records as AtprotoRecord[]); 112 + setCursor(cached.cursor); 113 + setReverse(cached.reverse); 114 + setRestoredFromCache(true); 115 + requestAnimationFrame(() => { 116 + window.scrollTo(0, cached.scrollY); 117 + }); 118 + } 119 + }); 120 + 121 + useBeforeLeave((e) => { 122 + const recordPathPrefix = `/at://${did}/${params.collection}/`; 123 + const isNavigatingToRecord = typeof e.to === "string" && e.to.startsWith(recordPathPrefix); 124 + 125 + if (isNavigatingToRecord && records.length > 0) { 126 + setCollectionCache(cacheKey(), { 127 + records: [...records], 128 + cursor: cursor(), 129 + scrollY: window.scrollY, 130 + reverse: reverse(), 131 + }); 132 + } else { 133 + clearCollectionCache(cacheKey()); 134 + } 135 + }); 136 + 137 const fetchRecords = async () => { 138 + if (restoredFromCache() && records.length > 0 && !cursor()) { 139 + setRestoredFromCache(false); 140 + return records; 141 + } 142 + if (restoredFromCache()) setRestoredFromCache(false); 143 + 144 if (!pds) pds = await resolvePDS(did!); 145 if (!rpc) rpc = new Client({ handler: simpleFetchHandler({ service: pds }) }); 146 const res = await rpc.get("com.atproto.repo.listRecords", { ··· 219 setCursor(undefined); 220 setOpenDelete(false); 221 setRecreate(false); 222 + clearCollectionCache(cacheKey()); 223 refetch(); 224 }; 225 ··· 264 setLastSelected(undefined); 265 setBatchDelete(!batchDelete()); 266 }} 267 + 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" 268 > 269 <span 270 + class={`iconify ${batchDelete() ? "lucide--circle-x" : "lucide--trash-2"} `} 271 ></span> 272 </button> 273 } ··· 278 children={ 279 <button 280 onclick={() => selectAll()} 281 + 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" 282 > 283 + <span class="iconify lucide--copy-check"></span> 284 </button> 285 } 286 /> ··· 293 setRecreate(true); 294 setOpenDelete(true); 295 }} 296 + 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" 297 > 298 + <span class="iconify lucide--recycle text-green-500 dark:text-green-400"></span> 299 </button> 300 } 301 /> ··· 308 setRecreate(false); 309 setOpenDelete(true); 310 }} 311 + 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" 312 > 313 + <span class="iconify lucide--trash-2 text-red-500 dark:text-red-400"></span> 314 </button> 315 } 316 /> ··· 334 </div> 335 </Modal> 336 </Show> 337 <TextInput 338 name="Filter" 339 placeholder="Filter by substring" 340 onInput={(e) => setFilter(e.currentTarget.value)} 341 class="grow" 342 /> 343 + <Tooltip text="Jetstream"> 344 + <A 345 + href={`/jetstream?collections=${params.collection}&dids=${params.repo}`} 346 + 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" 347 + > 348 + <span class="iconify lucide--radio-tower"></span> 349 + </A> 350 + </Tooltip> 351 </div> 352 <Show when={records.length > 1}> 353 <div class="flex items-center justify-between gap-x-2"> ··· 356 setReverse(!reverse()); 357 setRecords([]); 358 setCursor(undefined); 359 + clearCollectionCache(cacheKey()); 360 refetch(); 361 }} 362 > ··· 404 </label> 405 </Show> 406 <Show when={!batchDelete()}> 407 + <A href={`/at://${did}/${params.collection}/${record.rkey}`} class="select-none"> 408 <RecordLink record={record} /> 409 </A> 410 </Show>
+1 -1
src/views/labels.tsx
··· 228 rows={2} 229 value={searchParams.uriPatterns ?? "*"} 230 placeholder="at://did:web:example.com/app.bsky.feed.post/*" 231 - class="dark:bg-dark-100 dark:inset-shadow-dark-200 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1.5 text-sm inset-shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 232 /> 233 </label> 234 </div>
··· 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>
+11 -20
src/views/logs.tsx
··· 55 } 56 }); 57 58 - const FilterButton = (props: { icon: string; event: PlcEvent; label: string }) => { 59 const isActive = () => activePlcEvent() === props.event; 60 const toggleFilter = () => setActivePlcEvent(isActive() ? undefined : props.event); 61 62 return ( 63 <button 64 classList={{ 65 - "flex items-center gap-1 sm:gap-1.5 rounded-lg px-3 py-2 sm:px-2 sm:py-1.5 text-base sm:text-sm transition-colors": true, 66 - "bg-neutral-700 text-white dark:bg-neutral-200 dark:text-neutral-900": isActive(), 67 "bg-neutral-200 text-neutral-700 hover:bg-neutral-300 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600": 68 !isActive(), 69 }} 70 onclick={toggleFilter} 71 > 72 - <span class={props.icon}></span> 73 - <span class="hidden font-medium sm:inline">{props.label}</span> 74 </button> 75 ); 76 }; ··· 255 <div class="iconify lucide--filter" /> 256 <p class="font-medium">Filter by type</p> 257 </div> 258 - <div class="flex flex-wrap gap-1 sm:gap-2"> 259 - <FilterButton icon="iconify lucide--at-sign" event="handle" label="Alias" /> 260 - <FilterButton icon="iconify lucide--hard-drive" event="service" label="Service" /> 261 - <FilterButton 262 - icon="iconify lucide--shield-check" 263 - event="verification_method" 264 - label="Verification" 265 - /> 266 - <FilterButton 267 - icon="iconify lucide--key-round" 268 - event="rotation_key" 269 - label="Rotation Key" 270 - /> 271 </div> 272 </div> 273 <div class="flex items-center gap-1.5 text-sm font-medium"> 274 <Show when={validLog() === true}> 275 - <span class="iconify lucide--check-circle-2 text-green-600 dark:text-green-400"></span> 276 <span>Valid log</span> 277 </Show> 278 <Show when={validLog() === false}> 279 - <span class="iconify lucide--x-circle text-red-500 dark:text-red-400"></span> 280 <span>Log validation failed</span> 281 </Show> 282 <Show when={validLog() === undefined}>
··· 55 } 56 }); 57 58 + const FilterButton = (props: { event: PlcEvent; label: string }) => { 59 const isActive = () => activePlcEvent() === props.event; 60 const toggleFilter = () => setActivePlcEvent(isActive() ? undefined : props.event); 61 62 return ( 63 <button 64 classList={{ 65 + "font-medium rounded-lg px-2 py-1.5 text-xs sm:text-sm transition-colors": true, 66 + "bg-neutral-700 text-white dark:bg-neutral-300 dark:text-neutral-900": isActive(), 67 "bg-neutral-200 text-neutral-700 hover:bg-neutral-300 dark:bg-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-600": 68 !isActive(), 69 }} 70 onclick={toggleFilter} 71 > 72 + {props.label} 73 </button> 74 ); 75 }; ··· 254 <div class="iconify lucide--filter" /> 255 <p class="font-medium">Filter by type</p> 256 </div> 257 + <div class="flex flex-wrap gap-1"> 258 + <FilterButton event="handle" label="Alias" /> 259 + <FilterButton event="service" label="Service" /> 260 + <FilterButton event="verification_method" label="Verification" /> 261 + <FilterButton event="rotation_key" label="Rotation Key" /> 262 </div> 263 </div> 264 <div class="flex items-center gap-1.5 text-sm font-medium"> 265 <Show when={validLog() === true}> 266 + <span class="iconify lucide--check text-green-600 dark:text-green-400"></span> 267 <span>Valid log</span> 268 </Show> 269 <Show when={validLog() === false}> 270 + <span class="iconify lucide--x text-red-500 dark:text-red-400"></span> 271 <span>Log validation failed</span> 272 </Show> 273 <Show when={validLog() === undefined}>
+37 -34
src/views/pds.tsx
··· 5 import { A, useLocation, useParams } from "@solidjs/router"; 6 import { createResource, createSignal, For, Show } from "solid-js"; 7 import { Button } from "../components/button"; 8 - import { CopyMenu, DropdownMenu, MenuProvider, NavMenu } from "../components/dropdown"; 9 import { Modal } from "../components/modal"; 10 import { setPDS } from "../components/navbar"; 11 import Tooltip from "../components/tooltip"; ··· 137 ); 138 }; 139 140 - const Tab = (props: { tab: "repos" | "info"; label: string }) => ( 141 <A 142 classList={{ 143 - "border-b-2": true, 144 - "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 145 (!!location.hash && location.hash !== `#${props.tab}`) || 146 (!location.hash && props.tab !== "repos"), 147 }} 148 - href={`/${params.pds}#${props.tab}`} 149 > 150 {props.label} 151 </A> ··· 153 154 return ( 155 <Show when={repos() || response()}> 156 - <div class="flex w-full flex-col"> 157 - <div class="dark:shadow-dark-700 dark:bg-dark-300 mb-2 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700"> 158 - <div class="ml-1 flex items-center gap-3"> 159 - <Tab tab="repos" label="Repositories" /> 160 - <Tab tab="info" label="Info" /> 161 - </div> 162 - <MenuProvider> 163 - <DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> 164 - <CopyMenu content={params.pds!} label="Copy PDS" icon="lucide--copy" /> 165 - <NavMenu 166 - href={`/firehose?instance=wss://${params.pds}`} 167 - label="Firehose" 168 - icon="lucide--radio-tower" 169 - /> 170 - </DropdownMenu> 171 - </MenuProvider> 172 </div> 173 - <div class="flex flex-col gap-1 px-2"> 174 - <Show when={!location.hash || location.hash === "#repos"}> 175 - <div class="flex flex-col divide-y-[0.5px] divide-neutral-300 dark:divide-neutral-700"> 176 - <For each={repos()}>{(repo) => <RepoCard {...repo} />}</For> 177 - </div> 178 - </Show> 179 <Show when={location.hash === "#info"}> 180 <Show when={version()}> 181 {(version) => ( 182 - <div class="flex items-baseline gap-x-1"> 183 <span class="font-semibold">Version</span> 184 - <span class="truncate text-sm">{version()}</span> 185 </div> 186 )} 187 </Show> 188 <Show when={serverInfos()}> 189 {(server) => ( 190 <> 191 - <div class="flex items-baseline gap-x-1"> 192 <span class="font-semibold">DID</span> 193 - <span class="truncate text-sm">{server().did}</span> 194 </div> 195 - <Show when={server().inviteCodeRequired}> 196 <span class="font-semibold">Invite Code Required</span> 197 - </Show> 198 <Show when={server().phoneVerificationRequired}> 199 - <span class="font-semibold">Phone Verification Required</span> 200 </Show> 201 <Show when={server().availableUserDomains.length}> 202 <div class="flex flex-col">
··· 5 import { A, useLocation, useParams } from "@solidjs/router"; 6 import { createResource, createSignal, For, Show } from "solid-js"; 7 import { Button } from "../components/button"; 8 import { Modal } from "../components/modal"; 9 import { setPDS } from "../components/navbar"; 10 import Tooltip from "../components/tooltip"; ··· 136 ); 137 }; 138 139 + const Tab = (props: { tab: "repos" | "info" | "firehose"; label: string }) => ( 140 <A 141 classList={{ 142 + "border-b-2 font-medium": true, 143 + "border-transparent dark:text-neutral-300/80 text-neutral-600 hover:border-neutral-600 dark:hover:border-neutral-300/80": 144 (!!location.hash && location.hash !== `#${props.tab}`) || 145 (!location.hash && props.tab !== "repos"), 146 }} 147 + href={ 148 + props.tab === "firehose" ? 149 + `/firehose?instance=wss://${params.pds}` 150 + : `/${params.pds}#${props.tab}` 151 + } 152 > 153 {props.label} 154 </A> ··· 156 157 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> 168 + </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 <> 183 + <div class="flex flex-col"> 184 <span class="font-semibold">DID</span> 185 + <span class="text-sm">{server().did}</span> 186 </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">
+7 -18
src/views/record.tsx
··· 363 <div class="flex items-center gap-0.5"> 364 <A 365 classList={{ 366 - "border-b-2": true, 367 - "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": 368 !isActive(), 369 }} 370 href={`/at://${did}/${params.collection}/${params.rkey}#${props.tab}`} ··· 381 return ( 382 <Show when={record()} keyed> 383 <div class="flex w-full flex-col items-center"> 384 - <div class="dark:shadow-dark-700 dark:bg-dark-300 mb-3 flex w-full justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700"> 385 - <div class="ml-1 flex items-center gap-3"> 386 <RecordTab tab="record" label="Record" /> 387 <RecordTab tab="schema" label="Schema" /> 388 <RecordTab tab="backlinks" label="Backlinks" /> ··· 490 <Show when={location.hash === "#info"}> 491 <div class="flex w-full flex-col gap-2 px-2 text-sm"> 492 <div> 493 - <div class="flex items-center gap-1"> 494 - <span class="iconify lucide--at-sign"></span> 495 - <p class="font-semibold">AT URI</p> 496 - </div> 497 <div class="truncate text-xs">{record()?.uri}</div> 498 </div> 499 <Show when={record()?.cid}> 500 <div> 501 - <div class="flex items-center gap-1"> 502 - <span class="iconify lucide--box"></span> 503 - <p class="font-semibold">CID</p> 504 - </div> 505 <div class="truncate text-left text-xs" dir="rtl"> 506 {record()?.cid} 507 </div> ··· 509 </Show> 510 <div> 511 <div class="flex items-center gap-1"> 512 - <span class="iconify lucide--lock-keyhole"></span> 513 <p class="font-semibold">Record verification</p> 514 <span 515 classList={{ ··· 526 </div> 527 <div> 528 <div class="flex items-center gap-1"> 529 - <span class="iconify lucide--file-check"></span> 530 <p class="font-semibold">Schema validation</p> 531 <span 532 classList={{ ··· 556 </div> 557 <Show when={lexiconUri()}> 558 <div> 559 - <div class="flex items-center gap-1"> 560 - <span class="iconify lucide--scroll-text"></span> 561 - <p class="font-semibold">Lexicon schema</p> 562 - </div> 563 <div class="truncate text-xs"> 564 <A 565 href={`/${lexiconUri()}`}
··· 363 <div class="flex items-center gap-0.5"> 364 <A 365 classList={{ 366 + "border-b-2 font-medium": true, 367 + "border-transparent text-neutral-600 dark:text-neutral-300/80 hover:border-neutral-600 dark:hover:border-neutral-300/80": 368 !isActive(), 369 }} 370 href={`/at://${did}/${params.collection}/${params.rkey}#${props.tab}`} ··· 381 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" /> ··· 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}> 497 <div> 498 + <p class="font-semibold">CID</p> 499 <div class="truncate text-left text-xs" dir="rtl"> 500 {record()?.cid} 501 </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={{ ··· 519 </div> 520 <div> 521 <div class="flex items-center gap-1"> 522 <p class="font-semibold">Schema validation</p> 523 <span 524 classList={{ ··· 548 </div> 549 <Show when={lexiconUri()}> 550 <div> 551 + <p class="font-semibold">Lexicon schema</p> 552 <div class="truncate text-xs"> 553 <A 554 href={`/${lexiconUri()}`}
+74 -86
src/views/repo.tsx
··· 88 return ( 89 <A 90 classList={{ 91 - "border-b-2": true, 92 - "border-transparent hover:border-neutral-400 dark:hover:border-neutral-600": !isActive(), 93 }} 94 href={`/at://${params.repo}#${props.tab}`} 95 > ··· 275 return ( 276 <Show when={repo()}> 277 <div class="flex w-full flex-col gap-3 wrap-break-word"> 278 - <div class="dark:shadow-dark-700 dark:bg-dark-300 flex justify-between rounded-lg border-[0.5px] border-neutral-300 bg-neutral-50 p-2 text-sm shadow-xs dark:border-neutral-700"> 279 - <div class="ml-1 flex items-center gap-2 text-xs sm:gap-4 sm:text-sm"> 280 <Show when={!error()}> 281 <RepoTab tab="collections" label="Collections" /> 282 </Show> ··· 289 </Show> 290 <RepoTab tab="backlinks" label="Backlinks" /> 291 </div> 292 - <div class="flex gap-0.5"> 293 <Show when={error() && error() !== "Missing PDS"}> 294 <div class="flex items-center gap-1 text-red-500 dark:text-red-400"> 295 <span class="iconify lucide--alert-triangle"></span> 296 <span>{error()}</span> 297 </div> 298 </Show> 299 - <Show when={!error() && (!location.hash || location.hash.startsWith("#collections"))}> 300 - <Tooltip text="Filter collections"> 301 - <button 302 - 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" 303 - onClick={() => setShowFilter(!showFilter())} 304 - > 305 - <span class="iconify lucide--filter"></span> 306 - </button> 307 - </Tooltip> 308 - </Show> 309 <MenuProvider> 310 <DropdownMenu icon="lucide--ellipsis-vertical" buttonClass="rounded-sm p-1.5"> 311 <CopyMenu content={params.repo!} label="Copy DID" icon="lucide--copy" /> 312 <NavMenu 313 href={`/jetstream?dids=${params.repo}`} ··· 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 /> ··· 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")}> 343 <NavMenu 344 href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`} 345 newTab 346 - label="Audit Log" 347 icon="lucide--external-link" 348 /> 349 </Show> ··· 486 <div class="flex flex-col gap-3 wrap-anywhere"> 487 {/* ID Section */} 488 <div> 489 - <div class="flex items-center gap-1"> 490 - <div class="iconify lucide--id-card" /> 491 - <p class="font-semibold">ID</p> 492 </div> 493 - <div class="text-sm">{didDocument().id}</div> 494 </div> 495 496 {/* Aliases Section */} 497 <div> 498 - <div class="flex items-center gap-1"> 499 - <div class="iconify lucide--at-sign" /> 500 - <p class="font-semibold">Aliases</p> 501 - </div> 502 - <div class="flex flex-col gap-0.5"> 503 - <For each={didDocument().alsoKnownAs}> 504 - {(alias) => ( 505 - <div class="flex items-center gap-1 text-sm"> 506 - <span>{alias}</span> 507 - <Show when={alias.startsWith("at://")}> 508 - <Tooltip 509 - text={ 510 - validHandles[alias] === true ? "Valid handle" 511 - : validHandles[alias] === undefined ? 512 - "Validating" 513 - : "Invalid handle" 514 - } 515 - > 516 - <span 517 - classList={{ 518 - "iconify lucide--circle-check text-green-600 dark:text-green-400": 519 - validHandles[alias] === true, 520 - "iconify lucide--circle-x text-red-500 dark:text-red-400": 521 - validHandles[alias] === false, 522 - "iconify lucide--loader-circle animate-spin": 523 - validHandles[alias] === undefined, 524 - }} 525 - ></span> 526 - </Tooltip> 527 - </Show> 528 - </div> 529 - )} 530 - </For> 531 - </div> 532 </div> 533 534 {/* Services Section */} 535 <div> 536 - <div class="flex items-center gap-1"> 537 - <div class="iconify lucide--hard-drive" /> 538 - <p class="font-semibold">Services</p> 539 - </div> 540 - <div class="flex flex-col gap-0.5"> 541 <For each={didDocument().service}> 542 {(service) => ( 543 - <div class="text-sm"> 544 - <div class="font-medium text-neutral-700 dark:text-neutral-300"> 545 - #{service.id.split("#")[1]} 546 - </div> 547 <a 548 - class="underline hover:text-blue-400" 549 href={service.serviceEndpoint.toString()} 550 target="_blank" 551 rel="noopener" ··· 560 561 {/* Verification Methods Section */} 562 <div> 563 - <div class="flex items-center gap-1"> 564 - <div class="iconify lucide--shield-check" /> 565 - <p class="font-semibold">Verification Methods</p> 566 - </div> 567 - <div class="flex flex-col gap-0.5"> 568 <For each={didDocument().verificationMethod}> 569 {(verif) => ( 570 <Show when={verif.publicKeyMultibase}> 571 {(key) => ( 572 - <div class="text-sm"> 573 - <div class="flex items-baseline gap-1"> 574 - <span class="font-medium text-neutral-700 dark:text-neutral-300"> 575 - #{verif.id.split("#")[1]} 576 - </span> 577 - <span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300"> 578 - {detectKeyType(key())} 579 - </span> 580 </div> 581 <div class="font-mono break-all">{key()}</div> 582 </div> 583 )} ··· 590 {/* Rotation Keys Section */} 591 <Show when={rotationKeys().length > 0}> 592 <div> 593 - <div class="flex items-center gap-1"> 594 - <div class="iconify lucide--key-round" /> 595 - <p class="font-semibold">Rotation Keys</p> 596 - </div> 597 - <div class="flex flex-col gap-0.5"> 598 <For each={rotationKeys()}> 599 {(key) => ( 600 - <div class="text-sm"> 601 - <span class="rounded bg-neutral-200 px-1 py-0.5 text-xs text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300"> 602 {detectDidKeyType(key)} 603 </span> 604 <div class="font-mono break-all">{key.replace("did:key:", "")}</div> 605 </div> 606 )}
··· 88 return ( 89 <A 90 classList={{ 91 + "border-b-2 font-medium": true, 92 + "border-transparent text-neutral-600 dark:text-neutral-300/80 hover:border-neutral-600 dark:hover:border-neutral-300/80": 93 + !isActive(), 94 }} 95 href={`/at://${params.repo}#${props.tab}`} 96 > ··· 276 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> ··· 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-vertical" 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}`} ··· 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 /> ··· 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")}> 343 <NavMenu 344 href={`${localStorage.plcDirectory ?? "https://plc.directory"}/${did}/log/audit`} 345 newTab 346 + label="Audit log" 347 icon="lucide--external-link" 348 /> 349 </Show> ··· 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 </div> 493 </div> 494 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" ··· 551 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 )} ··· 579 {/* Rotation Keys Section */} 580 <Show when={rotationKeys().length > 0}> 581 <div> 582 + <p class="font-semibold">Rotation Keys</p> 583 + <div class="flex flex-col gap-1"> 584 <For each={rotationKeys()}> 585 {(key) => ( 586 + <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> 591 + <span></span> 592 <div class="font-mono break-all">{key.replace("did:key:", "")}</div> 593 </div> 594 )}
+8 -8
src/views/stream.tsx
··· 143 144 return ( 145 <div class="flex w-full flex-col items-center"> 146 - <div class="flex gap-2 text-sm"> 147 <A 148 - class="flex items-center gap-1 border-b-2 p-1" 149 - inactiveClass="border-transparent hover:border-neutral-400 dark:hover:border-neutral-600" 150 href="/jetstream" 151 > 152 Jetstream 153 </A> 154 <A 155 - class="flex items-center gap-1 border-b-2 p-1" 156 - inactiveClass="border-transparent hover:border-neutral-400 dark:hover:border-neutral-600" 157 href="/firehose" 158 > 159 Firehose 160 </A> 161 </div> 162 <StickyOverlay> 163 - <form ref={formRef} class="flex w-full flex-col gap-1 text-sm"> 164 <Show when={!connected()}> 165 <label class="flex items-center justify-end gap-x-1"> 166 <span class="min-w-20">Instance</span> ··· 183 spellcheck={false} 184 placeholder="Comma-separated list of collections" 185 value={searchParams.collections ?? ""} 186 - class="dark:bg-dark-100 dark:inset-shadow-dark-200 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 inset-shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 187 /> 188 </label> 189 </Show> ··· 195 spellcheck={false} 196 placeholder="Comma-separated list of DIDs" 197 value={searchParams.dids ?? ""} 198 - class="dark:bg-dark-100 dark:inset-shadow-dark-200 grow rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 py-1 inset-shadow-xs focus:outline-[1px] focus:outline-neutral-600 dark:border-neutral-600 dark:focus:outline-neutral-400" 199 /> 200 </label> 201 </Show>
··· 143 144 return ( 145 <div class="flex w-full flex-col items-center"> 146 + <div class="mb-1 flex gap-4 font-medium"> 147 <A 148 + class="flex items-center gap-1 border-b-2" 149 + inactiveClass="border-transparent text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-neutral-600" 150 href="/jetstream" 151 > 152 Jetstream 153 </A> 154 <A 155 + class="flex items-center gap-1 border-b-2" 156 + inactiveClass="border-transparent text-neutral-600 dark:text-neutral-400 hover:border-neutral-400 dark:hover:border-neutral-600" 157 href="/firehose" 158 > 159 Firehose 160 </A> 161 </div> 162 <StickyOverlay> 163 + <form ref={formRef} class="flex w-full flex-col gap-1.5 text-sm"> 164 <Show when={!connected()}> 165 <label class="flex items-center justify-end gap-x-1"> 166 <span class="min-w-20">Instance</span> ··· 183 spellcheck={false} 184 placeholder="Comma-separated list of collections" 185 value={searchParams.collections ?? ""} 186 + 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" 187 /> 188 </label> 189 </Show> ··· 195 spellcheck={false} 196 placeholder="Comma-separated list of DIDs" 197 value={searchParams.dids ?? ""} 198 + 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" 199 /> 200 </label> 201 </Show>