atmosphere explorer pds.ls
tool typescript atproto
439
fork

Configure Feed

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

at v1.1.2 545 lines 20 kB view raw
1import { Nsid } from "@atcute/lexicons"; 2import { useLocation, useNavigate } from "@solidjs/router"; 3import { createEffect, For, Show } from "solid-js"; 4import { resolveLexiconAuthority } from "../utils/api.js"; 5 6interface LexiconSchema { 7 lexicon: number; 8 id: string; 9 description?: string; 10 defs: { 11 [key: string]: LexiconDef; 12 }; 13} 14 15interface LexiconDef { 16 type: string; 17 description?: string; 18 key?: string; 19 record?: LexiconObject; 20 parameters?: LexiconObject; 21 input?: { encoding: string; schema?: LexiconObject }; 22 output?: { encoding: string; schema?: LexiconObject }; 23 errors?: Array<{ name: string; description?: string }>; 24 properties?: { [key: string]: LexiconProperty }; 25 required?: string[]; 26 nullable?: string[]; 27 maxLength?: number; 28 minLength?: number; 29 maxGraphemes?: number; 30 minGraphemes?: number; 31 items?: LexiconProperty; 32 refs?: string[]; 33 closed?: boolean; 34 enum?: string[]; 35 const?: string; 36 default?: any; 37 minimum?: number; 38 maximum?: number; 39 accept?: string[]; 40 maxSize?: number; 41 knownValues?: string[]; 42 format?: string; 43} 44 45interface LexiconObject { 46 type: string; 47 description?: string; 48 ref?: string; 49 refs?: string[]; 50 closed?: boolean; 51 properties?: { [key: string]: LexiconProperty }; 52 required?: string[]; 53 nullable?: string[]; 54} 55 56interface LexiconProperty { 57 type: string; 58 description?: string; 59 ref?: string; 60 refs?: string[]; 61 closed?: boolean; 62 format?: string; 63 items?: LexiconProperty; 64 minLength?: number; 65 maxLength?: number; 66 maxGraphemes?: number; 67 minGraphemes?: number; 68 minimum?: number; 69 maximum?: number; 70 enum?: string[]; 71 const?: string | boolean | number; 72 default?: any; 73 knownValues?: string[]; 74 accept?: string[]; 75 maxSize?: number; 76} 77 78const TypeBadge = (props: { type: string; format?: string; refType?: string }) => { 79 const navigate = useNavigate(); 80 const displayType = 81 props.refType ? props.refType.replace(/^#/, "") 82 : props.format ? `${props.type}:${props.format}` 83 : props.type; 84 85 const isLocalRef = () => props.refType?.startsWith("#"); 86 const isExternalRef = () => props.refType && !props.refType.startsWith("#"); 87 88 const handleClick = async () => { 89 if (isLocalRef()) { 90 const defName = props.refType!.slice(1); 91 window.history.replaceState(null, "", `#schema:${defName}`); 92 const element = document.getElementById(`def-${defName}`); 93 if (element) { 94 element.scrollIntoView({ behavior: "instant", block: "start" }); 95 } 96 } else if (isExternalRef()) { 97 try { 98 const [nsid, anchor] = props.refType!.split("#"); 99 const authority = await resolveLexiconAuthority(nsid as Nsid); 100 101 const hash = anchor ? `#schema:${anchor}` : "#schema"; 102 navigate(`/at://${authority}/com.atproto.lexicon.schema/${nsid}${hash}`); 103 } catch (err) { 104 console.error("Failed to resolve lexicon authority:", err); 105 } 106 } 107 }; 108 109 return ( 110 <> 111 <Show when={props.refType}> 112 <button 113 type="button" 114 onClick={handleClick} 115 class="inline-block cursor-pointer truncate rounded bg-blue-100 px-1.5 py-0.5 font-mono text-xs text-blue-800 hover:bg-blue-200 hover:underline active:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:hover:bg-blue-900/50 dark:active:bg-blue-900/50" 116 > 117 {displayType} 118 </button> 119 </Show> 120 <Show when={!props.refType}> 121 <span class="inline-block rounded bg-blue-100 px-1.5 py-0.5 font-mono text-xs text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"> 122 {displayType} 123 </span> 124 </Show> 125 </> 126 ); 127}; 128 129const UnionBadges = (props: { refs: string[] }) => ( 130 <div class="flex flex-wrap gap-2"> 131 <For each={props.refs}>{(refType) => <TypeBadge type="union" refType={refType} />}</For> 132 </div> 133); 134 135const ConstraintsList = (props: { property: LexiconProperty }) => ( 136 <div class="flex flex-wrap gap-x-4 gap-y-1 text-xs text-neutral-500 dark:text-neutral-400"> 137 <Show when={props.property.minLength !== undefined}> 138 <span>minLength: {props.property.minLength}</span> 139 </Show> 140 <Show when={props.property.maxLength !== undefined}> 141 <span>maxLength: {props.property.maxLength}</span> 142 </Show> 143 <Show when={props.property.maxGraphemes !== undefined}> 144 <span>maxGraphemes: {props.property.maxGraphemes}</span> 145 </Show> 146 <Show when={props.property.minGraphemes !== undefined}> 147 <span>minGraphemes: {props.property.minGraphemes}</span> 148 </Show> 149 <Show when={props.property.minimum !== undefined}> 150 <span>min: {props.property.minimum}</span> 151 </Show> 152 <Show when={props.property.maximum !== undefined}> 153 <span>max: {props.property.maximum}</span> 154 </Show> 155 <Show when={props.property.maxSize !== undefined}> 156 <span>maxSize: {props.property.maxSize}</span> 157 </Show> 158 <Show when={props.property.accept}> 159 <span>accept: [{props.property.accept!.join(", ")}]</span> 160 </Show> 161 <Show when={props.property.enum}> 162 <span>enum: [{props.property.enum!.join(", ")}]</span> 163 </Show> 164 <Show when={props.property.const}> 165 <span>const: {props.property.const?.toString()}</span> 166 </Show> 167 <Show when={props.property.default !== undefined}> 168 <span>default: {JSON.stringify(props.property.default)}</span> 169 </Show> 170 <Show when={props.property.knownValues}> 171 <span>knownValues: [{props.property.knownValues!.join(", ")}]</span> 172 </Show> 173 <Show when={props.property.closed}> 174 <span>closed: true</span> 175 </Show> 176 </div> 177); 178 179const PropertyRow = (props: { 180 name: string; 181 property: LexiconProperty; 182 required?: boolean; 183 hideNameType?: boolean; 184}) => { 185 const hasConstraints = (property: LexiconProperty) => 186 property.minLength !== undefined || 187 property.maxLength !== undefined || 188 property.maxGraphemes !== undefined || 189 property.minGraphemes !== undefined || 190 property.minimum !== undefined || 191 property.maximum !== undefined || 192 property.maxSize !== undefined || 193 property.accept || 194 property.enum || 195 property.const || 196 property.default !== undefined || 197 property.knownValues || 198 property.closed; 199 200 return ( 201 <div class="flex flex-col gap-2 py-3"> 202 <Show when={!props.hideNameType}> 203 <div class="flex flex-wrap items-center gap-2"> 204 <span class="font-mono text-sm font-semibold">{props.name}</span> 205 <Show when={!props.property.refs}> 206 <TypeBadge 207 type={props.property.type} 208 format={props.property.format} 209 refType={props.property.ref} 210 /> 211 </Show> 212 <Show when={props.property.refs}> 213 <span class="inline-block rounded bg-blue-100 px-1.5 py-0.5 font-mono text-xs text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"> 214 union 215 </span> 216 </Show> 217 <Show when={props.required}> 218 <span class="text-xs font-semibold text-red-500 dark:text-red-400">required</span> 219 </Show> 220 </div> 221 </Show> 222 <Show when={props.property.refs}> 223 <UnionBadges refs={props.property.refs!} /> 224 </Show> 225 <Show when={hasConstraints(props.property)}> 226 <ConstraintsList property={props.property} /> 227 </Show> 228 <Show when={props.property.items}> 229 <div class="flex flex-col gap-2"> 230 <div class="flex items-center gap-2 text-xs text-neutral-500 dark:text-neutral-400"> 231 <span class="font-semibold">items:</span> 232 <Show when={!props.property.items!.refs}> 233 <TypeBadge 234 type={props.property.items!.type} 235 format={props.property.items!.format} 236 refType={props.property.items!.ref} 237 /> 238 </Show> 239 <Show when={props.property.items!.refs}> 240 <span class="inline-block rounded bg-blue-100 px-1.5 py-0.5 font-mono text-xs text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"> 241 union 242 </span> 243 </Show> 244 </div> 245 <Show when={props.property.items!.refs}> 246 <UnionBadges refs={props.property.items!.refs!} /> 247 </Show> 248 </div> 249 </Show> 250 <Show when={props.property.items && hasConstraints(props.property.items)}> 251 <ConstraintsList property={props.property.items!} /> 252 </Show> 253 <Show when={props.property.description && !props.hideNameType}> 254 <p class="text-sm text-neutral-700 dark:text-neutral-300">{props.property.description}</p> 255 </Show> 256 </div> 257 ); 258}; 259 260const DefSection = (props: { name: string; def: LexiconDef }) => { 261 const defTypeColor = () => { 262 switch (props.def.type) { 263 case "record": 264 return "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300"; 265 case "query": 266 return "bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300"; 267 case "procedure": 268 return "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300"; 269 case "subscription": 270 return "bg-pink-100 text-pink-800 dark:bg-pink-900/30 dark:text-pink-300"; 271 case "object": 272 return "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"; 273 case "token": 274 return "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300"; 275 default: 276 return "bg-neutral-200 text-neutral-800 dark:bg-neutral-700 dark:text-neutral-300"; 277 } 278 }; 279 280 const hasDefContent = () => 281 props.def.refs || 282 props.def.minLength !== undefined || 283 props.def.maxLength !== undefined || 284 props.def.maxGraphemes !== undefined || 285 props.def.minGraphemes !== undefined || 286 props.def.minimum !== undefined || 287 props.def.maximum !== undefined || 288 props.def.maxSize !== undefined || 289 props.def.accept || 290 props.def.enum || 291 props.def.const || 292 props.def.default !== undefined || 293 props.def.closed || 294 props.def.items || 295 props.def.knownValues; 296 297 return ( 298 <div class="flex flex-col gap-3" id={`def-${props.name}`}> 299 <div class="group flex items-center gap-2"> 300 <a href={`#schema:${props.name}`} class="relative text-lg font-semibold hover:underline"> 301 <span class="iconify lucide--link absolute top-1/2 -left-6 -translate-y-1/2 text-base opacity-0 transition-opacity group-hover:opacity-100" /> 302 {props.name === "main" ? "Main Definition" : props.name} 303 </a> 304 <span class={`rounded px-2 py-0.5 text-xs font-semibold uppercase ${defTypeColor()}`}> 305 {props.def.type} 306 </span> 307 </div> 308 309 <Show when={props.def.description}> 310 <p class="text-sm text-neutral-700 dark:text-neutral-300">{props.def.description}</p> 311 </Show> 312 313 {/* Record key */} 314 <Show when={props.def.key}> 315 <div> 316 <span class="text-sm font-semibold">Record Key: </span> 317 <span class="font-mono text-sm">{props.def.key}</span> 318 </div> 319 </Show> 320 321 {/* Properties (for record/object types) */} 322 <Show 323 when={Object.keys(props.def.properties || props.def.record?.properties || {}).length > 0} 324 > 325 <div class="flex flex-col gap-2"> 326 <h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400"> 327 Properties 328 </h4> 329 <div class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-neutral-50/50 px-3 dark:divide-neutral-700 dark:border-neutral-700 dark:bg-neutral-800/30"> 330 <For each={Object.entries(props.def.properties || props.def.record?.properties || {})}> 331 {([name, property]) => ( 332 <PropertyRow 333 name={name} 334 property={property} 335 required={(props.def.required || props.def.record?.required || []).includes(name)} 336 /> 337 )} 338 </For> 339 </div> 340 </div> 341 </Show> 342 343 {/* Parameters (for query/procedure) */} 344 <Show 345 when={ 346 props.def.parameters?.properties && 347 Object.keys(props.def.parameters.properties).length > 0 348 } 349 > 350 <div class="flex flex-col gap-2"> 351 <h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400"> 352 Parameters 353 </h4> 354 <div class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-neutral-50/50 px-3 dark:divide-neutral-700 dark:border-neutral-700 dark:bg-neutral-800/30"> 355 <For each={Object.entries(props.def.parameters!.properties!)}> 356 {([name, property]) => ( 357 <PropertyRow 358 name={name} 359 property={property} 360 required={(props.def.parameters?.required || []).includes(name)} 361 /> 362 )} 363 </For> 364 </div> 365 </div> 366 </Show> 367 368 {/* Input */} 369 <Show when={props.def.input}> 370 <div class="flex flex-col gap-2"> 371 <h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400"> 372 Input 373 </h4> 374 <div class="flex flex-col gap-2 rounded-lg border border-neutral-200 bg-neutral-50/50 p-3 dark:border-neutral-700 dark:bg-neutral-800/30"> 375 <div class="text-sm"> 376 <span class="font-semibold">Encoding: </span> 377 <span class="font-mono">{props.def.input!.encoding}</span> 378 </div> 379 <Show when={props.def.input!.schema?.ref}> 380 <div class="flex items-center gap-2"> 381 <span class="text-sm font-semibold">Schema:</span> 382 <TypeBadge type="ref" refType={props.def.input!.schema!.ref} /> 383 </div> 384 </Show> 385 <Show when={props.def.input!.schema?.refs}> 386 <div class="flex flex-col gap-2"> 387 <div class="flex items-center gap-2"> 388 <span class="text-sm font-semibold">Schema (union):</span> 389 </div> 390 <UnionBadges refs={props.def.input!.schema!.refs!} /> 391 </div> 392 </Show> 393 <Show 394 when={ 395 props.def.input!.schema?.properties && 396 Object.keys(props.def.input!.schema.properties).length > 0 397 } 398 > 399 <div class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-neutral-50/50 px-3 dark:divide-neutral-700 dark:border-neutral-700 dark:bg-neutral-800/30"> 400 <For each={Object.entries(props.def.input!.schema!.properties!)}> 401 {([name, property]) => ( 402 <PropertyRow 403 name={name} 404 property={property} 405 required={(props.def.input!.schema?.required || []).includes(name)} 406 /> 407 )} 408 </For> 409 </div> 410 </Show> 411 </div> 412 </div> 413 </Show> 414 415 {/* Output */} 416 <Show when={props.def.output}> 417 <div class="flex flex-col gap-2"> 418 <h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400"> 419 Output 420 </h4> 421 <div class="flex flex-col gap-2 rounded-lg border border-neutral-200 bg-neutral-50/50 p-3 dark:border-neutral-700 dark:bg-neutral-800/30"> 422 <div class="text-sm"> 423 <span class="font-semibold">Encoding: </span> 424 <span class="font-mono">{props.def.output!.encoding}</span> 425 </div> 426 <Show when={props.def.output!.schema?.ref}> 427 <div class="flex items-center gap-2"> 428 <span class="text-sm font-semibold">Schema:</span> 429 <TypeBadge type="ref" refType={props.def.output!.schema!.ref} /> 430 </div> 431 </Show> 432 <Show when={props.def.output!.schema?.refs}> 433 <div class="flex flex-col gap-2"> 434 <div class="flex items-center gap-2"> 435 <span class="text-sm font-semibold">Schema (union):</span> 436 </div> 437 <UnionBadges refs={props.def.output!.schema!.refs!} /> 438 </div> 439 </Show> 440 <Show 441 when={ 442 props.def.output!.schema?.properties && 443 Object.keys(props.def.output!.schema.properties).length > 0 444 } 445 > 446 <div class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-neutral-50/50 px-3 dark:divide-neutral-700 dark:border-neutral-700 dark:bg-neutral-800/30"> 447 <For each={Object.entries(props.def.output!.schema!.properties!)}> 448 {([name, property]) => ( 449 <PropertyRow 450 name={name} 451 property={property} 452 required={(props.def.output!.schema?.required || []).includes(name)} 453 /> 454 )} 455 </For> 456 </div> 457 </Show> 458 </div> 459 </div> 460 </Show> 461 462 {/* Errors */} 463 <Show when={props.def.errors && props.def.errors.length > 0}> 464 <div class="flex flex-col gap-2"> 465 <h4 class="text-sm font-semibold text-neutral-600 uppercase dark:text-neutral-400"> 466 Errors 467 </h4> 468 <div class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-neutral-50/50 px-3 dark:divide-neutral-700 dark:border-neutral-700 dark:bg-neutral-800/30"> 469 <For each={props.def.errors}> 470 {(error) => ( 471 <div class="flex flex-col gap-1 py-2"> 472 <div class="font-mono text-sm font-semibold">{error.name}</div> 473 <Show when={error.description}> 474 <p class="text-sm text-neutral-700 dark:text-neutral-300"> 475 {error.description} 476 </p> 477 </Show> 478 </div> 479 )} 480 </For> 481 </div> 482 </div> 483 </Show> 484 485 {/* Other Definitions */} 486 <Show 487 when={ 488 !( 489 props.def.properties || 490 props.def.parameters || 491 props.def.input || 492 props.def.output || 493 props.def.errors || 494 props.def.record 495 ) && hasDefContent() 496 } 497 > 498 <div class="divide-y divide-neutral-200 rounded-lg border border-neutral-200 bg-neutral-50/50 px-3 dark:divide-neutral-700 dark:border-neutral-700 dark:bg-neutral-800/30"> 499 <PropertyRow name={props.name} property={props.def} hideNameType /> 500 </div> 501 </Show> 502 </div> 503 ); 504}; 505 506export const LexiconSchemaView = (props: { schema: LexiconSchema }) => { 507 const location = useLocation(); 508 509 // Handle scrolling to a definition when hash is like #schema:definitionName 510 createEffect(() => { 511 const hash = location.hash; 512 if (hash.startsWith("#schema:")) { 513 const defName = hash.slice(8); 514 requestAnimationFrame(() => { 515 const element = document.getElementById(`def-${defName}`); 516 if (element) element.scrollIntoView({ behavior: "instant", block: "start" }); 517 }); 518 } 519 }); 520 521 return ( 522 <div class="w-full max-w-4xl px-2"> 523 {/* Header */} 524 <div class="flex flex-col gap-2 border-b border-neutral-300 pb-4 dark:border-neutral-700"> 525 <h2 class="text-lg font-semibold">{props.schema.id}</h2> 526 <div class="flex gap-4 text-sm text-neutral-600 dark:text-neutral-400"> 527 <span> 528 <span class="font-semibold">Lexicon version: </span> 529 <span class="font-mono">{props.schema.lexicon}</span> 530 </span> 531 </div> 532 <Show when={props.schema.description}> 533 <p class="text-sm text-neutral-700 dark:text-neutral-300">{props.schema.description}</p> 534 </Show> 535 </div> 536 537 {/* Definitions */} 538 <div class="flex flex-col gap-6 pt-4"> 539 <For each={Object.entries(props.schema.defs)}> 540 {([name, def]) => <DefSection name={name} def={def} />} 541 </For> 542 </div> 543 </div> 544 ); 545};