import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'; import { AtProtoProvider } from '../lib/providers/AtProtoProvider'; import { AtProtoRecord } from '../lib/core/AtProtoRecord'; import { TangledString } from '../lib/components/TangledString'; import { LeafletDocument } from '../lib/components/LeafletDocument'; import { BlueskyProfile } from '../lib/components/BlueskyProfile'; import { BlueskyPost, BLUESKY_POST_COLLECTION } from '../lib/components/BlueskyPost'; import { BlueskyPostList } from '../lib/components/BlueskyPostList'; import { BlueskyQuotePost } from '../lib/components/BlueskyQuotePost'; import { useDidResolution } from '../lib/hooks/useDidResolution'; import { useLatestRecord } from '../lib/hooks/useLatestRecord'; import { ColorSchemeToggle } from '../lib/components/ColorSchemeToggle.tsx'; import { useColorScheme, type ColorSchemePreference } from '../lib/hooks/useColorScheme'; import type { FeedPostRecord } from '../lib/types/bluesky'; const COLOR_SCHEME_STORAGE_KEY = 'atproto-ui-color-scheme'; const basicUsageSnippet = `import { AtProtoProvider, BlueskyPost } from 'atproto-ui'; export function App() { return ( ); }`; const customComponentSnippet = `import { useLatestRecord, useColorScheme, AtProtoRecord } from 'atproto-ui'; import type { FeedPostRecord } from 'atproto-ui'; const LatestPostSummary: React.FC<{ did: string }> = ({ did }) => { const scheme = useColorScheme('system'); const { rkey, loading, error } = useLatestRecord(did, 'app.bsky.feed.post'); if (loading) return Loading…; if (error || !rkey) return No post yet.; return ( did={did} collection="app.bsky.feed.post" rkey={rkey} renderer={({ record }) => (
{record?.text ?? 'Empty post'}
)} /> ); };`; const codeBlockBase: React.CSSProperties = { fontFamily: 'Menlo, Consolas, "SFMono-Regular", ui-monospace, monospace', fontSize: 12, whiteSpace: 'pre', overflowX: 'auto', borderRadius: 10, padding: '12px 14px', lineHeight: 1.6 }; const FullDemo: React.FC = () => { const handleInputRef = useRef(null); const [submitted, setSubmitted] = useState(null); const [colorSchemePreference, setColorSchemePreference] = useState(() => { if (typeof window === 'undefined') return 'system'; try { const stored = window.localStorage.getItem(COLOR_SCHEME_STORAGE_KEY); if (stored === 'light' || stored === 'dark' || stored === 'system') return stored; } catch { /* ignore */ } return 'system'; }); const scheme = useColorScheme(colorSchemePreference); const { did, loading: resolvingDid } = useDidResolution(submitted ?? undefined); const onSubmit = useCallback((e) => { e.preventDefault(); const rawValue = handleInputRef.current?.value; const nextValue = rawValue?.trim(); if (!nextValue) return; if (handleInputRef.current) { handleInputRef.current.value = nextValue; } setSubmitted(nextValue); }, []); useEffect(() => { if (typeof window === 'undefined') return; try { window.localStorage.setItem(COLOR_SCHEME_STORAGE_KEY, colorSchemePreference); } catch { /* ignore */ } }, [colorSchemePreference]); useEffect(() => { if (typeof document === 'undefined') return; const root = document.documentElement; const body = document.body; const prevScheme = root.dataset.colorScheme; const prevBg = body.style.backgroundColor; const prevColor = body.style.color; root.dataset.colorScheme = scheme; body.style.backgroundColor = scheme === 'dark' ? '#020617' : '#f8fafc'; body.style.color = scheme === 'dark' ? '#e2e8f0' : '#0f172a'; return () => { root.dataset.colorScheme = prevScheme ?? ''; body.style.backgroundColor = prevBg; body.style.color = prevColor; }; }, [scheme]); const showHandle = submitted && !submitted.startsWith('did:') ? submitted : undefined; const mutedTextColor = useMemo(() => (scheme === 'dark' ? '#94a3b8' : '#555'), [scheme]); const panelStyle = useMemo(() => ({ display: 'flex', flexDirection: 'column', gap: 8, padding: 10, borderRadius: 12, borderColor: scheme === 'dark' ? '#1e293b' : '#e2e8f0', }), [scheme]); const baseTextColor = useMemo(() => (scheme === 'dark' ? '#e2e8f0' : '#0f172a'), [scheme]); const gistPanelStyle = useMemo(() => ({ ...panelStyle, padding: 0, border: 'none', background: 'transparent', backdropFilter: 'none', marginTop: 32 }), [panelStyle]); const leafletPanelStyle = useMemo(() => ({ ...panelStyle, padding: 0, border: 'none', background: 'transparent', backdropFilter: 'none', marginTop: 32, alignItems: 'center' }), [panelStyle]); const primaryGridStyle = useMemo(() => ({ display: 'grid', gap: 32, gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))' }), []); const columnStackStyle = useMemo(() => ({ display: 'flex', flexDirection: 'column', gap: 32 }), []); const codeBlockStyle = useMemo(() => ({ ...codeBlockBase, background: scheme === 'dark' ? '#0b1120' : '#f1f5f9', border: `1px solid ${scheme === 'dark' ? '#1e293b' : '#e2e8f0'}` }), [scheme]); const codeTextStyle = useMemo(() => ({ margin: 0, display: 'block', fontFamily: codeBlockBase.fontFamily, fontSize: 12, lineHeight: 1.6, whiteSpace: 'pre' }), []); const basicCodeRef = useRef(null); const customCodeRef = useRef(null); // Latest Bluesky post const { rkey: latestPostRkey, loading: loadingLatestPost, empty: noPosts, error: latestPostError } = useLatestRecord(did, BLUESKY_POST_COLLECTION); const quoteSampleDid = 'did:plc:ttdrpj45ibqunmfhdsb4zdwq'; const quoteSampleRkey = '3m2prlq6xxc2v'; return (
{!submitted &&

Enter a handle to fetch your profile, latest Bluesky post, a Tangled string, and a Leaflet document.

} {submitted && resolvingDid &&

Resolving DID…

} {did && ( <>

Profile

Recent Posts

Latest Bluesky Post

{loadingLatestPost &&
Loading latest post…
} {latestPostError &&
Failed to load latest post.
} {noPosts &&
No posts found.
} {!loadingLatestPost && latestPostRkey && ( )}

Quote Post Demo

A Tangled String

A Leaflet Document.

)}

Build your own component

Wrap your app with the provider once and drop the ready-made components wherever you need them.

                            {basicUsageSnippet}
                

Need to make your own component? Compose your own renderer with the hooks and utilities that ship with the library.

                            {customComponentSnippet}
                
{did && (

Live example with your handle:

)}
); }; const LatestPostSummary: React.FC<{ did: string; handle?: string; colorScheme: ColorSchemePreference }> = ({ did, colorScheme }) => { const { rkey, loading, error } = useLatestRecord(did, BLUESKY_POST_COLLECTION); const scheme = useColorScheme(colorScheme); const palette = scheme === 'dark' ? latestSummaryPalette.dark : latestSummaryPalette.light; if (loading) return
Loading summary…
; if (error) return
Failed to load the latest post.
; if (!rkey) return
No posts published yet.
; return ( did={did} collection="app.bsky.feed.post" rkey={rkey} renderer={({ record }) => (
{record?.text ?? 'Empty post'}
)} /> ); }; const sectionHeaderStyle: React.CSSProperties = { margin: '4px 0', fontSize: 16 }; const loadingBox: React.CSSProperties = { padding: 8 }; const errorBox: React.CSSProperties = { padding: 8, color: 'crimson' }; const infoBox: React.CSSProperties = { padding: 8, color: '#555' }; const latestSummaryPalette = { light: { card: { border: '1px solid #e2e8f0', background: '#ffffff', borderRadius: 12, padding: 12, display: 'flex', flexDirection: 'column', gap: 8 } satisfies React.CSSProperties, header: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, color: '#0f172a' } satisfies React.CSSProperties, time: { fontSize: 12, color: '#64748b' } satisfies React.CSSProperties, text: { margin: 0, color: '#1f2937', whiteSpace: 'pre-wrap' } satisfies React.CSSProperties, link: { color: '#2563eb', fontWeight: 600, fontSize: 12, textDecoration: 'none' } satisfies React.CSSProperties, muted: { color: '#64748b' } satisfies React.CSSProperties, error: { color: 'crimson' } satisfies React.CSSProperties }, dark: { card: { border: '1px solid #1e293b', background: '#0f172a', borderRadius: 12, padding: 12, display: 'flex', flexDirection: 'column', gap: 8 } satisfies React.CSSProperties, header: { display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12, color: '#e2e8f0' } satisfies React.CSSProperties, time: { fontSize: 12, color: '#cbd5f5' } satisfies React.CSSProperties, text: { margin: 0, color: '#e2e8f0', whiteSpace: 'pre-wrap' } satisfies React.CSSProperties, link: { color: '#38bdf8', fontWeight: 600, fontSize: 12, textDecoration: 'none' } satisfies React.CSSProperties, muted: { color: '#94a3b8' } satisfies React.CSSProperties, error: { color: '#f472b6' } satisfies React.CSSProperties } } as const; export const App: React.FC = () => { return (

atproto-ui Demo

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


); }; export default App;