A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.

version bump, clean up demo a bit

Changed files
+24 -161
src
+1 -1
package.json
··· 1 1 { 2 2 "name": "atproto-ui", 3 - "version": "0.3.1-1", 3 + "version": "0.4.0", 4 4 "type": "module", 5 5 "description": "React components and hooks for rendering AT Protocol records.", 6 6 "main": "./lib-dist/index.js",
+23 -160
src/App.tsx
··· 6 6 useRef, 7 7 } from "react"; 8 8 import { AtProtoProvider } from "../lib/providers/AtProtoProvider"; 9 - import { AtProtoRecord } from "../lib/core/AtProtoRecord"; 9 + 10 10 import { TangledString } from "../lib/components/TangledString"; 11 11 import { LeafletDocument } from "../lib/components/LeafletDocument"; 12 12 import { BlueskyProfile } from "../lib/components/BlueskyProfile"; ··· 37 37 ); 38 38 }`; 39 39 40 - const customComponentSnippet = `import { useLatestRecord, useColorScheme, AtProtoRecord } from 'atproto-ui'; 40 + const prefetchedDataSnippet = `import { BlueskyPost, useLatestRecord } from 'atproto-ui'; 41 41 import type { FeedPostRecord } from 'atproto-ui'; 42 42 43 - const LatestPostSummary: React.FC<{ did: string }> = ({ did }) => { 44 - const scheme = useColorScheme('system'); 45 - const { rkey, loading, error } = useLatestRecord<FeedPostRecord>(did, 'app.bsky.feed.post'); 43 + const LatestPostWithPrefetch: React.FC<{ did: string }> = ({ did }) => { 44 + // Fetch once with the hook 45 + const { record, rkey, loading } = useLatestRecord<FeedPostRecord>( 46 + did, 47 + 'app.bsky.feed.post' 48 + ); 46 49 47 50 if (loading) return <span>Loading…</span>; 48 - if (error || !rkey) return <span>No post yet.</span>; 51 + if (!record || !rkey) return <span>No posts yet.</span>; 49 52 50 - return ( 51 - <AtProtoRecord<FeedPostRecord> 52 - did={did} 53 - collection="app.bsky.feed.post" 54 - rkey={rkey} 55 - renderer={({ record }) => ( 56 - <article data-color-scheme={scheme}> 57 - <strong>{record?.text ?? 'Empty post'}</strong> 58 - </article> 59 - )} 60 - /> 61 - ); 53 + // Pass prefetched record—BlueskyPost won't re-fetch it 54 + return <BlueskyPost did={did} rkey={rkey} record={record} />; 62 55 };`; 63 56 64 57 const codeBlockBase: React.CSSProperties = { ··· 219 212 const basicCodeRef = useRef<HTMLElement | null>(null); 220 213 const customCodeRef = useRef<HTMLElement | null>(null); 221 214 222 - // Latest Bluesky post 215 + // Latest Bluesky post - fetch with record for prefetch demo 223 216 const { 217 + record: latestPostRecord, 224 218 rkey: latestPostRkey, 225 219 loading: loadingLatestPost, 226 220 empty: noPosts, 227 221 error: latestPostError, 228 - } = useLatestRecord<unknown>(did, BLUESKY_POST_COLLECTION); 222 + } = useLatestRecord<FeedPostRecord>(did, BLUESKY_POST_COLLECTION); 229 223 230 224 const quoteSampleDid = "did:plc:ttdrpj45ibqunmfhdsb4zdwq"; 231 225 const quoteSampleRkey = "3m2prlq6xxc2v"; ··· 323 317 <div style={columnStackStyle}> 324 318 <section style={panelStyle}> 325 319 <h3 style={sectionHeaderStyle}> 326 - Latest Bluesky Post 320 + Latest Post (Prefetched Data) 327 321 </h3> 322 + <p style={{ fontSize: 12, color: mutedTextColor, margin: "0 0 8px" }}> 323 + Using <code style={{ background: scheme === "dark" ? "#1e293b" : "#e2e8f0", padding: "2px 4px", borderRadius: 3 }}>useLatestRecord</code> to fetch once, then passing <code style={{ background: scheme === "dark" ? "#1e293b" : "#e2e8f0", padding: "2px 4px", borderRadius: 3 }}>record</code> prop—no re-fetch! 324 + </p> 328 325 {loadingLatestPost && ( 329 326 <div style={loadingBox}> 330 327 Loading latest post… ··· 345 342 No posts found. 346 343 </div> 347 344 )} 348 - {!loadingLatestPost && latestPostRkey && ( 345 + {!loadingLatestPost && latestPostRkey && latestPostRecord && ( 349 346 <BlueskyPost 350 347 did={did} 351 348 rkey={latestPostRkey} 349 + record={latestPostRecord} 352 350 colorScheme={colorSchemePreference} 353 351 /> 354 352 )} ··· 392 390 </> 393 391 )} 394 392 <section style={{ ...panelStyle, marginTop: 32 }}> 395 - <h3 style={sectionHeaderStyle}>Build your own component</h3> 393 + <h3 style={sectionHeaderStyle}>Code Examples</h3> 396 394 <p style={{ color: mutedTextColor, margin: "4px 0 8px" }}> 397 395 Wrap your app with the provider once and drop the ready-made 398 396 components wherever you need them. ··· 407 405 </code> 408 406 </pre> 409 407 <p style={{ color: mutedTextColor, margin: "16px 0 8px" }}> 410 - Need to make your own component? Compose your own renderer 411 - with the hooks and utilities that ship with the library. 408 + Pass prefetched data to components to skip API calls—perfect for SSR or caching. 412 409 </p> 413 410 <pre style={codeBlockStyle}> 414 411 <code ··· 416 413 className="language-tsx" 417 414 style={codeTextStyle} 418 415 > 419 - {customComponentSnippet} 416 + {prefetchedDataSnippet} 420 417 </code> 421 418 </pre> 422 - {did && ( 423 - <div 424 - style={{ 425 - marginTop: 16, 426 - display: "flex", 427 - flexDirection: "column", 428 - gap: 12, 429 - }} 430 - > 431 - <p style={{ color: mutedTextColor, margin: 0 }}> 432 - Live example with your handle: 433 - </p> 434 - <LatestPostSummary 435 - did={did} 436 - handle={showHandle} 437 - colorScheme={colorSchemePreference} 438 - /> 439 - </div> 440 - )} 441 419 </section> 442 420 </div> 443 421 ); 444 422 }; 445 423 446 - const LatestPostSummary: React.FC<{ 447 - did: string; 448 - handle?: string; 449 - colorScheme: ColorSchemePreference; 450 - }> = ({ did, colorScheme }) => { 451 - const { record, rkey, loading, error } = useLatestRecord<FeedPostRecord>( 452 - did, 453 - BLUESKY_POST_COLLECTION, 454 - ); 455 - const scheme = useColorScheme(colorScheme); 456 - const palette = 457 - scheme === "dark" 458 - ? latestSummaryPalette.dark 459 - : latestSummaryPalette.light; 460 424 461 - if (loading) return <div style={palette.muted}>Loading summary…</div>; 462 - if (error) 463 - return <div style={palette.error}>Failed to load the latest post.</div>; 464 - if (!rkey) return <div style={palette.muted}>No posts published yet.</div>; 465 - 466 - const atProtoProps = record 467 - ? { record } 468 - : { did, collection: "app.bsky.feed.post", rkey }; 469 - 470 - return ( 471 - <AtProtoRecord<FeedPostRecord> 472 - {...atProtoProps} 473 - renderer={({ record: resolvedRecord }) => ( 474 - <article data-color-scheme={scheme}> 475 - <strong>{resolvedRecord?.text ?? "Empty post"}</strong> 476 - </article> 477 - )} 478 - /> 479 - ); 480 - }; 481 425 482 426 const sectionHeaderStyle: React.CSSProperties = { 483 427 margin: "4px 0", ··· 486 430 const loadingBox: React.CSSProperties = { padding: 8 }; 487 431 const errorBox: React.CSSProperties = { padding: 8, color: "crimson" }; 488 432 const infoBox: React.CSSProperties = { padding: 8, color: "#555" }; 489 - 490 - const latestSummaryPalette = { 491 - light: { 492 - card: { 493 - border: "1px solid #e2e8f0", 494 - background: "#ffffff", 495 - borderRadius: 12, 496 - padding: 12, 497 - display: "flex", 498 - flexDirection: "column", 499 - gap: 8, 500 - } satisfies React.CSSProperties, 501 - header: { 502 - display: "flex", 503 - alignItems: "baseline", 504 - justifyContent: "space-between", 505 - gap: 12, 506 - color: "#0f172a", 507 - } satisfies React.CSSProperties, 508 - time: { 509 - fontSize: 12, 510 - color: "#64748b", 511 - } satisfies React.CSSProperties, 512 - text: { 513 - margin: 0, 514 - color: "#1f2937", 515 - whiteSpace: "pre-wrap", 516 - } satisfies React.CSSProperties, 517 - link: { 518 - color: "#2563eb", 519 - fontWeight: 600, 520 - fontSize: 12, 521 - textDecoration: "none", 522 - } satisfies React.CSSProperties, 523 - muted: { 524 - color: "#64748b", 525 - } satisfies React.CSSProperties, 526 - error: { 527 - color: "crimson", 528 - } satisfies React.CSSProperties, 529 - }, 530 - dark: { 531 - card: { 532 - border: "1px solid #1e293b", 533 - background: "#0f172a", 534 - borderRadius: 12, 535 - padding: 12, 536 - display: "flex", 537 - flexDirection: "column", 538 - gap: 8, 539 - } satisfies React.CSSProperties, 540 - header: { 541 - display: "flex", 542 - alignItems: "baseline", 543 - justifyContent: "space-between", 544 - gap: 12, 545 - color: "#e2e8f0", 546 - } satisfies React.CSSProperties, 547 - time: { 548 - fontSize: 12, 549 - color: "#cbd5f5", 550 - } satisfies React.CSSProperties, 551 - text: { 552 - margin: 0, 553 - color: "#e2e8f0", 554 - whiteSpace: "pre-wrap", 555 - } satisfies React.CSSProperties, 556 - link: { 557 - color: "#38bdf8", 558 - fontWeight: 600, 559 - fontSize: 12, 560 - textDecoration: "none", 561 - } satisfies React.CSSProperties, 562 - muted: { 563 - color: "#94a3b8", 564 - } satisfies React.CSSProperties, 565 - error: { 566 - color: "#f472b6", 567 - } satisfies React.CSSProperties, 568 - }, 569 - } as const; 570 433 571 434 export const App: React.FC = () => { 572 435 return (