+53
-51
README.md
+53
-51
README.md
···3. Use the hooks to prefetch handles, blobs, or latest records when you want to control the render flow yourself.-| `AtProtoProvider` | Configures PLC directory (defaults to `https://plc.directory`) and shares protocol clients via React context. |-| `BlueskyProfile` | Renders a profile card for a DID/handle. Accepts `fallback`, `loadingIndicator`, `renderer`, and `colorScheme`. |-| `BlueskyPost` / `BlueskyQuotePost` | Shows a single Bluesky post, with quotation support, custom renderer overrides, and the same loading/fallback knobs. |-| `BlueskyPostList` | Lists the latest posts with built-in pagination (defaults: 5 per page, pagination controls on). |-| `TangledString` | Renders a Tangled string (gist-like record) with optional renderer overrides. |-| `LeafletDocument` | Displays long-form Leaflet documents with blocks, theme support, and renderer overrides. |-| `useDidResolution`, `useLatestRecord`, `usePaginatedRecords`, … | Hook-level access to records if you want to own the markup or prefill components. |All components accept a `colorScheme` of `'light' | 'dark' | 'system'` so they can blend into your design. They also accept `fallback` and `loadingIndicator` props to control what renders before or during network work, and most expose a `renderer` override when you need total control of the final markup.···`useLatestRecord` gives you the most recent record for any collection along with its `rkey`. You can use that key to pre-populate components like `BlueskyPost`, `LeafletDocument`, or `TangledString`.-const { rkey, loading, error, empty } = useLatestRecord<FeedPostRecord>(did, 'app.bsky.feed.post');······The helpers let you stitch together custom experiences without reimplementing protocol plumbing. The example below pulls a creator’s latest post and renders a minimal summary:···
···3. Use the hooks to prefetch handles, blobs, or latest records when you want to control the render flow yourself.+| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |+| `AtProtoProvider` | Configures PLC directory (defaults to `https://plc.directory`) and shares protocol clients via React context. |+| `BlueskyProfile` | Renders a profile card for a DID/handle. Accepts `fallback`, `loadingIndicator`, `renderer`, and `colorScheme`. |+| `BlueskyPost` / `BlueskyQuotePost` | Shows a single Bluesky post, with quotation support, custom renderer overrides, and the same loading/fallback knobs. |+| `BlueskyPostList` | Lists the latest posts with built-in pagination (defaults: 5 per page, pagination controls on). |+| `TangledString` | Renders a Tangled string (gist-like record) with optional renderer overrides. |+| `LeafletDocument` | Displays long-form Leaflet documents with blocks, theme support, and renderer overrides. |+| `useDidResolution`, `useLatestRecord`, `usePaginatedRecords`, … | Hook-level access to records if you want to own the markup or prefill components. |All components accept a `colorScheme` of `'light' | 'dark' | 'system'` so they can blend into your design. They also accept `fallback` and `loadingIndicator` props to control what renders before or during network work, and most expose a `renderer` override when you need total control of the final markup.···`useLatestRecord` gives you the most recent record for any collection along with its `rkey`. You can use that key to pre-populate components like `BlueskyPost`, `LeafletDocument`, or `TangledString`.······The helpers let you stitch together custom experiences without reimplementing protocol plumbing. The example below pulls a creator’s latest post and renders a minimal summary:···
+36
-32
lib/components/BlueskyIcon.tsx
+36
-32
lib/components/BlueskyIcon.tsx
······-export const BlueskyIcon: React.FC<BlueskyIconProps> = ({ size = 16, color = '#1185fe', title = 'Bluesky' }) => (-d="M3.468 1.948C5.303 3.325 7.276 6.118 8 7.616c.725-1.498 2.698-4.29 4.532-5.668C13.855.955 16 .186 16 2.632c0 .489-.28 4.105-.444 4.692-.572 2.04-2.653 2.561-4.504 2.246 3.236.551 4.06 2.375 2.281 4.2-3.376 3.464-4.852-.87-5.23-1.98-.07-.204-.103-.3-.103-.218 0-.081-.033.014-.102.218-.379 1.11-1.855 5.444-5.231 1.98-1.778-1.825-.955-3.65 2.28-4.2-1.85.315-3.932-.205-4.503-2.246C.28 6.737 0 3.12 0 2.632 0 .186 2.145.955 3.468 1.948"
······+d="M3.468 1.948C5.303 3.325 7.276 6.118 8 7.616c.725-1.498 2.698-4.29 4.532-5.668C13.855.955 16 .186 16 2.632c0 .489-.28 4.105-.444 4.692-.572 2.04-2.653 2.561-4.504 2.246 3.236.551 4.06 2.375 2.281 4.2-3.376 3.464-4.852-.87-5.23-1.98-.07-.204-.103-.3-.103-.218 0-.081-.033.014-.102.218-.379 1.11-1.855 5.444-5.231 1.98-1.778-1.825-.955-3.65 2.28-4.2-1.85.315-3.932-.205-4.503-2.246C.28 6.737 0 3.12 0 2.632 0 .186 2.145.955 3.468 1.948"
+176
-134
lib/components/BlueskyPost.tsx
+176
-134
lib/components/BlueskyPost.tsx
······* @param iconPlacement - Determines where the icon is positioned in the rendered post. Defaults to `'timestamp'`.-export const BlueskyPost: React.FC<BlueskyPostProps> = ({ did: handleOrDid, rkey, renderer, fallback, loadingIndicator, colorScheme, showIcon = true, iconPlacement = 'timestamp' }) => {-const { did: resolvedDid, handle, loading: resolvingIdentity, error: resolutionError } = useDidResolution(handleOrDid);-const { record: profile } = useAtProtoRecord<ProfileRecord>({ did: repoIdentifier, collection: BLUESKY_PROFILE_COLLECTION, rkey: 'self' });-const Comp: React.ComponentType<BlueskyPostRendererInjectedProps> = renderer ?? ((props) => <BlueskyPostRenderer {...props} />);-const atUri = resolvedDid ? `at://${resolvedDid}/${BLUESKY_POST_COLLECTION}/${rkey}` : undefined;-const WrappedComponent: React.FC<{ record: FeedPostRecord; loading: boolean; error?: Error }> = (props) => {-}, [Comp, repoIdentifier, avatarCid, authorHandle, colorScheme, iconPlacement, showIcon, atUri]);
······* @param iconPlacement - Determines where the icon is positioned in the rendered post. Defaults to `'timestamp'`.
+558
-427
lib/components/BlueskyPostList.tsx
+558
-427
lib/components/BlueskyPostList.tsx
···-import { usePaginatedRecords, type AuthorFeedReason, type ReplyParentInfo } from '../hooks/usePaginatedRecords';···-export const BlueskyPostList: React.FC<BlueskyPostListProps> = ({ did, limit = 5, enablePagination = true, colorScheme = 'system' }) => {-const { records, loading, error, hasNext, hasPrev, loadNext, loadPrev, pageIndex, pagesCount } = usePaginatedRecords<FeedPostRecord>({-{pageLabel && <span style={{ ...listStyles.pageMeta, ...palette.pageMeta }}>{pageLabel}</span>}-{loading && records.length === 0 && <div style={{ ...listStyles.empty, ...palette.empty }}>Loading posts…</div>}-{!loading && records.length === 0 && <div style={{ ...listStyles.empty, ...palette.empty }}>No posts found.</div>}-<span style={{ ...listStyles.pageChipActive, ...palette.pageChipActive }}>{pageIndex + 1}</span>-{loading && records.length > 0 && <div style={{ ...listStyles.loadingBar, ...palette.loadingBar }}>Updating…</div>}-const ListRow: React.FC<ListRowProps> = ({ record, rkey, did, reason, replyParent, palette, hasDivider }) => {-const parentDid = replyParent?.author?.did ?? (parentUri ? parseAtUri(parentUri)?.did : undefined);-<a href={href} target="_blank" rel="noopener noreferrer" style={{ ...listStyles.row, ...palette.row, borderBottom: hasDivider ? `1px solid ${palette.divider}` : 'none' }}>-{repostLabel && <span style={{ ...listStyles.rowMeta, ...palette.rowMeta }}>{repostLabel}</span>}-{replyLabel && <span style={{ ...listStyles.rowMeta, ...palette.rowMeta }}>{replyLabel}</span>}-{!text && <p style={{ ...listStyles.rowBody, ...palette.rowBody, fontStyle: 'italic' }}>No text content.</p>}-const thresholds: Array<{ limit: number; unit: Intl.RelativeTimeFormatUnit; divisor: number }> = [-const threshold = thresholds.find(t => absSeconds < t.limit) ?? thresholds[thresholds.length - 1];-function formatReplyTarget(parentUri?: string, feedParent?: ReplyParentInfo, resolvedHandle?: string) {
······
+112
-86
lib/components/BlueskyProfile.tsx
+112
-86
lib/components/BlueskyProfile.tsx
······-export const BlueskyProfile: React.FC<BlueskyProfileProps> = ({ did: handleOrDid, rkey = 'self', renderer, fallback, loadingIndicator, handle, colorScheme }) => {-const Component: React.ComponentType<BlueskyProfileRendererInjectedProps> = renderer ?? ((props) => <BlueskyProfileRenderer {...props} />);-const effectiveHandle = handle ?? resolvedHandle ?? (handleOrDid.startsWith('did:') ? formatDidForLabel(repoIdentifier) : handleOrDid);-const Wrapped: React.FC<{ record: ProfileRecord; loading: boolean; error?: Error }> = (props) => {-return <Component {...props} did={repoIdentifier} handle={effectiveHandle} avatarUrl={avatarUrl} colorScheme={colorScheme} />;
······
+108
-82
lib/components/BlueskyQuotePost.tsx
+108
-82
lib/components/BlueskyQuotePost.tsx
···-import { BlueskyPost, type BlueskyPostRendererInjectedProps, BLUESKY_POST_COLLECTION } from './BlueskyPost';···-const BlueskyQuotePostComponent: React.FC<BlueskyQuotePostProps> = ({ did, rkey, colorScheme, renderer, fallback, loadingIndicator, showIcon = true, iconPlacement = 'timestamp' }) => {-export const BlueskyQuotePost: NamedExoticComponent<BlueskyQuotePostProps> = memo(BlueskyQuotePostComponent);···-function createQuoteEmbed(embed: QuoteRecordEmbed | undefined, colorScheme?: 'light' | 'dark' | 'system') {-<BlueskyPost did={parsed.did} rkey={parsed.rkey} colorScheme={colorScheme} showIcon={false} />
·········
+96
-83
lib/components/ColorSchemeToggle.tsx
+96
-83
lib/components/ColorSchemeToggle.tsx
······-export const ColorSchemeToggle: React.FC<ColorSchemeToggleProps> = ({ value, onChange, scheme = 'light' }) => {-<div aria-label="Color scheme" role="radiogroup" style={{ ...containerStyle, ...palette.container }}>
······
+115
-75
lib/components/LeafletDocument.tsx
+115
-75
lib/components/LeafletDocument.tsx
···-import { LeafletDocumentRenderer, type LeafletDocumentRendererProps } from '../renderers/LeafletDocumentRenderer';-import { parseAtUri, toBlueskyPostUrl, leafletRkeyUrl, normalizeLeafletBasePath } from '../utils/at-uri';······-export const LeafletDocument: React.FC<LeafletDocumentProps> = ({ did, rkey, renderer, fallback, loadingIndicator, colorScheme }) => {-const Comp: React.ComponentType<LeafletDocumentRendererInjectedProps> = renderer ?? ((props) => <LeafletDocumentRenderer {...props} />);-const Wrapped: React.FC<{ record: LeafletDocumentRecord; loading: boolean; error?: Error }> = (props) => {-const publicationUri = useMemo(() => parseAtUri(props.record.publication), [props.record.publication]);-const canonicalUrl = resolveCanonicalUrl(props.record, did, rkey, publicationRecord?.base_path);···-function resolveCanonicalUrl(record: LeafletDocumentRecord, did: string, rkey: string, publicationBasePath?: string): string {-return `at://${encodeURIComponent(did)}/${LEAFLET_DOCUMENT_COLLECTION}/${encodeURIComponent(rkey)}`;
············+return `at://${encodeURIComponent(did)}/${LEAFLET_DOCUMENT_COLLECTION}/${encodeURIComponent(rkey)}`;
+22
-10
lib/components/TangledString.tsx
+22
-10
lib/components/TangledString.tsx
············* Resolves a Tangled String record and renders it with optional overrides while computing a canonical link.···-export const TangledString: React.FC<TangledStringProps> = ({ did, rkey, renderer, fallback, loadingIndicator, colorScheme }) => {-const Comp: React.ComponentType<TangledStringRendererInjectedProps> = renderer ?? ((props) => <TangledStringRenderer {...props} />);-const Wrapped: React.FC<{ record: TangledStringRecord; loading: boolean; error?: Error }> = (props) => (
············* Resolves a Tangled String record and renders it with optional overrides while computing a canonical link.···
+35
-9
lib/core/AtProtoRecord.tsx
+35
-9
lib/core/AtProtoRecord.tsx
······-export type AtProtoRecordProps<T = unknown> = AtProtoRecordFetchProps<T> | AtProtoRecordProvidedRecordProps<T>;···-return <pre style={{ fontSize: 12, padding: 8, background: '#f5f5f5', overflow: 'auto' }}>{JSON.stringify(record, null, 2)}</pre>;
·········
+113
-67
lib/hooks/useAtProtoRecord.ts
+113
-67
lib/hooks/useAtProtoRecord.ts
······* @returns {AtProtoRecordState<T>} Object containing the resolved record, any error, and a loading flag.-export function useAtProtoRecord<T = unknown>({ did: handleOrDid, collection, rkey }: AtProtoRecordKey): AtProtoRecordState<T> {-const [state, setState] = useState<AtProtoRecordState<T>>({ loading: !!(handleOrDid && collection && rkey) });-}, [handleOrDid, did, endpoint, collection, rkey, resolvingDid, resolvingEndpoint, didError, endpointError]);
······* @returns {AtProtoRecordState<T>} Object containing the resolved record, any error, and a loading flag.
+148
-109
lib/hooks/useBlob.ts
+148
-109
lib/hooks/useBlob.ts
······-export function useBlob(handleOrDid: string | undefined, cid: string | undefined): UseBlobState {-`${endpoint}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`,-const aborted = (controller && controller.signal.aborted) || (e instanceof DOMException && e.name === 'AbortError');-}, [handleOrDid, cid, did, endpoint, didLoading, endpointLoading, didError, endpointError, blobCache]);
······+`${endpoint}/xrpc/com.atproto.sync.getBlob?did=${encodeURIComponent(did)}&cid=${encodeURIComponent(cid)}`,
+53
-44
lib/hooks/useBlueskyProfile.ts
+53
-44
lib/hooks/useBlueskyProfile.ts
······* @returns {{ data: BlueskyProfileData | undefined; loading: boolean; error: Error | undefined }} Object exposing the profile payload, loading flag, and any error.-get: (nsid: string, options: { params: { actor: string } }) => Promise<{ ok: boolean; data: unknown }>;
······* @returns {{ data: BlueskyProfileData | undefined; loading: boolean; error: Error | undefined }} Object exposing the profile payload, loading flag, and any error.
+27
-17
lib/hooks/useColorScheme.ts
+27
-17
lib/hooks/useColorScheme.ts
·········-export function useColorScheme(preference: ColorSchemePreference = 'system'): 'light' | 'dark' {
·········
+32
-13
lib/hooks/useDidResolution.ts
+32
-13
lib/hooks/useDidResolution.ts
············
············
+138
-73
lib/hooks/useLatestRecord.ts
+138
-73
lib/hooks/useLatestRecord.ts
······* @returns {LatestRecordState<T>} Object reporting the latest record value, derived rkey, loading status, emptiness, and any error.-export function useLatestRecord<T = unknown>(handleOrDid: string | undefined, collection: string): LatestRecordState<T> {-const [state, setState] = useState<LatestRecordState<T>>({ loading: !!handleOrDid, empty: false });-assign({ loading: false, record: undefined, rkey: undefined, error: undefined, empty: false });-) => Promise<{ ok: boolean; data: { records: Array<{ uri: string; rkey?: string; value: T }> } }>;-}, [handleOrDid, did, endpoint, collection, resolvingDid, resolvingEndpoint, didError, endpointError]);
······* @returns {LatestRecordState<T>} Object reporting the latest record value, derived rkey, loading status, emptiness, and any error.
+412
-318
lib/hooks/usePaginatedRecords.ts
+412
-318
lib/hooks/usePaginatedRecords.ts
······* @returns {UsePaginatedRecordsResult<T>} Object containing the current page, pagination metadata, and navigation callbacks.-const { did, handle, error: didError, loading: resolvingDid } = useDidResolution(handleOrDid);-const fetchPage = useCallback(async (identityKey: string, cursor: string | undefined, targetIndex: number, mode: 'active' | 'prefetch') => {-const shouldUseAuthorFeed = preferAuthorFeed && collection === 'app.bsky.feed.post' && !feedDisabledRef.current && !!actorIdentifier;-const { rpc } = await createAtprotoClient({ service: authorFeedService ?? DEFAULT_APPVIEW_SERVICE });-) => Promise<{ ok: boolean; data: { records: Array<{ uri: string; rkey?: string; value: T }>; cursor?: string } }>;-if (mode === 'active' && token === requestSeq.current && identityKey === identityRef.current) {-if (mode === 'active' && token === requestSeq.current && identityKey === identityRef.current) {-}, [handleOrDid, identity, fetchPage, resetState, resolvingDid, resolvingEndpoint, didError, endpointError]);-const effectiveError = error ?? (endpointError as Error | undefined) ?? (didError as Error | undefined);
······* @returns {UsePaginatedRecordsResult<T>} Object containing the current page, pagination metadata, and navigation callbacks.
+15
-8
lib/hooks/usePdsEndpoint.ts
+15
-8
lib/hooks/usePdsEndpoint.ts
·········
·········
+27
-27
lib/index.ts
+27
-27
lib/index.ts
···
···
+46
-17
lib/providers/AtProtoProvider.tsx
+46
-17
lib/providers/AtProtoProvider.tsx
······-const normalizedPlc = useMemo(() => normalizeBaseUrl(plcDirectory && plcDirectory.trim() ? plcDirectory : 'https://plc.directory'), [plcDirectory]);-const resolver = useMemo(() => new ServiceResolver({ plcDirectory: normalizedPlc }), [normalizedPlc]);
······
+564
-428
lib/renderers/BlueskyPostRenderer.tsx
+564
-428
lib/renderers/BlueskyPostRenderer.tsx
···-import { parseAtUri, toBlueskyPostUrl, formatDidForLabel, type ParsedAtUri } from '../utils/at-uri';-export const BlueskyPostRenderer: React.FC<BlueskyPostRendererProps> = ({ record, loading, error, authorDisplayName, authorHandle, avatarUrl, colorScheme = 'system', authorDid, embed, iconPlacement = 'timestamp', showIcon = true, atUri }) => {-const { handle: parentHandle, loading: parentHandleLoading } = useDidResolution(replyTarget?.did);-const replyLabel = replyTarget ? formatReplyLabel(replyTarget, parentHandle, parentHandleLoading) : undefined;-const cardPadding = typeof baseStyles.card.padding === 'number' ? baseStyles.card.padding : 12;-...(iconPlacement === 'cardBottomRight' && showIcon ? { paddingBottom: cardPadding + 16 } : {})-{authorDisplayName && authorHandle && <span style={{ ...baseStyles.handle, ...palette.handle }}>@{authorHandle}</span>}-<a href={replyHref} target="_blank" rel="noopener noreferrer" style={{ ...baseStyles.replyLink, ...palette.replyLink }}>-<time style={{ ...baseStyles.time, ...palette.time }} dateTime={record.createdAt}>{created}</time>-<a href={postUrl} target="_blank" rel="noopener noreferrer" style={{ ...baseStyles.postLink, ...palette.postLink }}>-function formatReplyLabel(target: ParsedAtUri, resolvedHandle?: string, loading?: boolean): string {-function createAutoEmbed(record: FeedPostRecord, authorDid: string | undefined, scheme: 'light' | 'dark'): React.ReactNode {
···
+232
-176
lib/renderers/BlueskyProfileRenderer.tsx
+232
-176
lib/renderers/BlueskyProfileRenderer.tsx
···-export const BlueskyProfileRenderer: React.FC<BlueskyProfileRendererProps> = ({ record, loading, error, did, handle, avatarUrl, colorScheme = 'system' }) => {-const websiteHref = rawWebsite ? (rawWebsite.match(/^https?:\/\//i) ? rawWebsite : `https://${rawWebsite}`) : undefined;-{avatarUrl ? <img src={avatarUrl} alt="avatar" style={base.avatarImg} /> : <div style={{ ...base.avatar, ...palette.avatar }} aria-label="avatar" />}-<div style={{ ...base.display, ...palette.display }}>{record.displayName ?? handle ?? did}</div>-{record.pronouns && <div style={{ ...base.pronouns, ...palette.pronouns }}>{record.pronouns}</div>}-{record.createdAt && <div style={{ ...base.meta, ...palette.meta }}>Joined {new Date(record.createdAt).toLocaleDateString()}</div>}-<a href={websiteHref} target="_blank" rel="noopener noreferrer" style={{ ...base.link, ...palette.link }}>-<a href={profileUrl} target="_blank" rel="noopener noreferrer" style={{ ...base.link, ...palette.link }}>
···
+1320
-856
lib/renderers/LeafletDocumentRenderer.tsx
+1320
-856
lib/renderers/LeafletDocumentRenderer.tsx
···-import { parseAtUri, formatDidForLabel, toBlueskyPostUrl, leafletRkeyUrl, normalizeLeafletBasePath } from '../utils/at-uri';-export const LeafletDocumentRenderer: React.FC<LeafletDocumentRendererProps> = ({ record, loading, error, colorScheme = 'system', did, rkey, canonicalUrl, publicationBaseUrl, publicationRecord }) => {-?? (publicationHandle ? `@${publicationHandle}` : publicationUri ? formatDidForLabel(publicationUri.did) : undefined);-const authorHref = publicationUri ? `https://bsky.app/profile/${publicationUri.did}` : undefined;-if (error) return <div style={{ padding: 12, color: 'crimson' }}>Failed to load leaflet.</div>;-if (!record) return <div style={{ padding: 12, color: 'crimson' }}>Leaflet record missing.</div>;-const publishedLabel = publishedAt ? publishedAt.toLocaleString(undefined, { dateStyle: 'long', timeStyle: 'short' }) : undefined;-const fallbackLeafletUrl = `https://bsky.app/leaflet/${encodeURIComponent(did)}/${encodeURIComponent(rkey)}`;-const resolvedPublicationRoot = publicationRoot ? normalizeLeafletBasePath(publicationRoot) : undefined;-const viewUrl = canonicalUrl ?? publicationLeafletUrl ?? postUrl ?? (publicationUri ? `https://bsky.app/profile/${publicationUri.did}` : undefined) ?? fallbackLeafletUrl;-if (publishedLabel) metaItems.push(<time dateTime={record.publishedAt}>{publishedLabel}</time>);-<a href={resolvedPublicationRoot} target="_blank" rel="noopener noreferrer" style={palette.metaLink}>-const LeafletPageRenderer: React.FC<{ page: LeafletLinearDocumentPage; documentDid: string; colorScheme: 'light' | 'dark' }> = ({ page, documentDid, colorScheme }) => {-const LeafletBlockRenderer: React.FC<LeafletBlockRendererProps> = ({ wrapper, documentDid, colorScheme, isFirst }) => {-return <LeafletHeaderBlockView block={block} alignment={alignment} colorScheme={colorScheme} isFirst={isFirst} />;-return <LeafletBlockquoteBlockView block={block} alignment={alignment} colorScheme={colorScheme} isFirst={isFirst} />;-return <LeafletImageBlockView block={block} alignment={alignment} documentDid={documentDid} colorScheme={colorScheme} />;-return <LeafletListBlockView block={block} alignment={alignment} documentDid={documentDid} colorScheme={colorScheme} />;-return <LeafletWebsiteBlockView block={block} alignment={alignment} documentDid={documentDid} colorScheme={colorScheme} />;-return <LeafletMathBlockView block={block} alignment={alignment} colorScheme={colorScheme} />;-return <LeafletCodeBlockView block={block} alignment={alignment} colorScheme={colorScheme} />;-return <LeafletTextBlockView block={block as LeafletTextBlock} alignment={alignment} colorScheme={colorScheme} isFirst={isFirst} />;-const LeafletTextBlockView: React.FC<{ block: LeafletTextBlock; alignment?: React.CSSProperties['textAlign']; colorScheme: 'light' | 'dark'; isFirst?: boolean }> = ({ block, alignment, colorScheme, isFirst }) => {-const segments = useMemo(() => createFacetedSegments(block.plaintext, block.facets), [block.plaintext, block.facets]);-const LeafletHeaderBlockView: React.FC<{ block: LeafletHeaderBlock; alignment?: React.CSSProperties['textAlign']; colorScheme: 'light' | 'dark'; isFirst?: boolean }> = ({ block, alignment, colorScheme, isFirst }) => {-const segments = useMemo(() => createFacetedSegments(block.plaintext, block.facets), [block.plaintext, block.facets]);-const LeafletBlockquoteBlockView: React.FC<{ block: LeafletBlockquoteBlock; alignment?: React.CSSProperties['textAlign']; colorScheme: 'light' | 'dark'; isFirst?: boolean }> = ({ block, alignment, colorScheme, isFirst }) => {-const segments = useMemo(() => createFacetedSegments(block.plaintext, block.facets), [block.plaintext, block.facets]);-<blockquote style={{ ...base.blockquote, ...palette.blockquote, ...(alignment ? { textAlign: alignment } : undefined), ...(isFirst ? { marginTop: 0 } : undefined) }}>-const LeafletImageBlockView: React.FC<{ block: LeafletImageBlock; alignment?: React.CSSProperties['textAlign']; documentDid: string; colorScheme: 'light' | 'dark' }> = ({ block, alignment, documentDid, colorScheme }) => {-<figure style={{ ...base.figure, ...palette.figure, ...(alignment ? { textAlign: alignment } : undefined) }}>-<div style={{ ...base.imageWrapper, ...palette.imageWrapper, ...(aspectRatio ? { aspectRatio } : {}) }}>-const LeafletListBlockView: React.FC<{ block: LeafletUnorderedListBlock; alignment?: React.CSSProperties['textAlign']; documentDid: string; colorScheme: 'light' | 'dark' }> = ({ block, alignment, documentDid, colorScheme }) => {-<ul style={{ ...base.list, ...palette.list, ...(alignment ? { textAlign: alignment } : undefined) }}>-const LeafletListItemRenderer: React.FC<{ item: LeafletListItem; documentDid: string; colorScheme: 'light' | 'dark'; alignment?: React.CSSProperties['textAlign'] }> = ({ item, documentDid, colorScheme, alignment }) => {-<LeafletInlineBlock block={item.content} colorScheme={colorScheme} documentDid={documentDid} alignment={alignment} />-<LeafletListItemRenderer key={`nested-${idx}`} item={child} documentDid={documentDid} colorScheme={colorScheme} alignment={alignment} />-const LeafletInlineBlock: React.FC<{ block: LeafletBlock; colorScheme: 'light' | 'dark'; documentDid: string; alignment?: React.CSSProperties['textAlign'] }> = ({ block, colorScheme, documentDid, alignment }) => {-return <LeafletHeaderBlockView block={block as LeafletHeaderBlock} colorScheme={colorScheme} alignment={alignment} />;-return <LeafletBlockquoteBlockView block={block as LeafletBlockquoteBlock} colorScheme={colorScheme} alignment={alignment} />;-return <LeafletImageBlockView block={block as LeafletImageBlock} documentDid={documentDid} colorScheme={colorScheme} alignment={alignment} />;-return <LeafletTextBlockView block={block as LeafletTextBlock} colorScheme={colorScheme} alignment={alignment} />;-const LeafletWebsiteBlockView: React.FC<{ block: LeafletWebsiteBlock; alignment?: React.CSSProperties['textAlign']; documentDid: string; colorScheme: 'light' | 'dark' }> = ({ block, alignment, documentDid, colorScheme }) => {-<a href={block.src} target="_blank" rel="noopener noreferrer" style={{ ...base.linkCard, ...palette.linkCard, ...(alignment ? { textAlign: alignment } : undefined) }}>-<img src={url} alt={block.title ?? 'Website preview'} style={{ ...base.linkPreview, ...palette.linkPreview }} />-const LeafletIframeBlockView: React.FC<{ block: LeafletIFrameBlock; alignment?: React.CSSProperties['textAlign'] }> = ({ block, alignment }) => {-style={{ ...base.iframe, ...(block.height ? { height: Math.min(Math.max(block.height, 120), 800) } : {}) }}-const LeafletMathBlockView: React.FC<{ block: LeafletMathBlock; alignment?: React.CSSProperties['textAlign']; colorScheme: 'light' | 'dark' }> = ({ block, alignment, colorScheme }) => {-<pre style={{ ...base.math, ...palette.math, ...(alignment ? { textAlign: alignment } : undefined) }}>{block.tex}</pre>-const LeafletCodeBlockView: React.FC<{ block: LeafletCodeBlock; alignment?: React.CSSProperties['textAlign']; colorScheme: 'light' | 'dark' }> = ({ block, alignment, colorScheme }) => {-<pre style={{ ...base.code, ...palette.code, ...(alignment ? { textAlign: alignment } : undefined) }}>-const LeafletHorizontalRuleBlockView: React.FC<{ alignment?: React.CSSProperties['textAlign']; colorScheme: 'light' | 'dark' }> = ({ alignment, colorScheme }) => {-return <hr style={{ ...base.hr, ...palette.hr, marginLeft: alignment ? 'auto' : undefined, marginRight: alignment ? 'auto' : undefined }} />;-const LeafletBskyPostBlockView: React.FC<{ block: LeafletBskyPostBlock; colorScheme: 'light' | 'dark' }> = ({ block, colorScheme }) => {-return <BlueskyPost did={parsed.did} rkey={parsed.rkey} colorScheme={colorScheme} iconPlacement="linkInline" />;-function alignmentValue(value?: LeafletAlignmentValue): React.CSSProperties['textAlign'] | undefined {-function useAuthorLabel(author: string | undefined, authorDid: string | undefined): string | undefined {-function createFacetedSegments(plaintext: string, facets?: LeafletRichTextFacet[]): Segment[] {-if (typeof byteStart !== 'number' || typeof byteEnd !== 'number' || byteStart >= byteEnd) continue;-const wrapped = applyFeatures(part.length ? part : '\u00a0', segment.features, key, colorScheme);-function applyFeatures(content: React.ReactNode, features: LeafletRichTextFeature[], key: string, colorScheme: 'light' | 'dark'): React.ReactNode {-{features.reduce<React.ReactNode>((child, feature, idx) => wrapFeature(child, feature, `${key}-feature-${idx}`, colorScheme), content)}-function wrapFeature(child: React.ReactNode, feature: LeafletRichTextFeature, key: string, colorScheme: 'light' | 'dark'): React.ReactNode {-return <a key={key} href={feature.uri} target="_blank" rel="noopener noreferrer" style={linkStyles[colorScheme]}>{child}</a>;
···+const fallbackLeafletUrl = `https://bsky.app/leaflet/${encodeURIComponent(did)}/${encodeURIComponent(rkey)}`;
+110
-72
lib/renderers/TangledStringRenderer.tsx
+110
-72
lib/renderers/TangledStringRenderer.tsx
······-export const TangledStringRenderer: React.FC<TangledStringRendererProps> = ({ record, error, loading, colorScheme = 'system', did, rkey, canonicalUrl }) => {-const viewUrl = canonicalUrl ?? `https://tangled.org/strings/${did}/${encodeURIComponent(rkey)}`;-const timestamp = new Date(record.createdAt).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });-<time style={{ ...base.timestamp, ...palette.timestamp }} dateTime={record.createdAt}>{timestamp}</time>-<a href={viewUrl} target="_blank" rel="noopener noreferrer" style={{ ...base.headerLink, ...palette.headerLink }}>···-transition: 'background-color 180ms ease, border-color 180ms ease, color 180ms ease, box-shadow 180ms ease',
·········+"background-color 180ms ease, border-color 180ms ease, color 180ms ease, box-shadow 180ms ease",
+1
-1
lib/types/bluesky.ts
+1
-1
lib/types/bluesky.ts
+133
-127
lib/types/leaflet.ts
+133
-127
lib/types/leaflet.ts
···-export type LeafletInlineRenderable = LeafletTextBlock | LeafletHeaderBlock | LeafletBlockquoteBlock;
···
+34
-27
lib/utils/at-uri.ts
+34
-27
lib/utils/at-uri.ts
···-export function leafletRkeyUrl(basePath: string | undefined, rkey: string): string | undefined {
···
+185
-132
lib/utils/atproto-client.ts
+185
-132
lib/utils/atproto-client.ts
···-import { CompositeDidDocumentResolver, PlcDidDocumentResolver, WebDidDocumentResolver, XrpcHandleResolver } from '@atcute/identity-resolver';-const withScheme = ABSOLUTE_URL_RE.test(trimmed) ? trimmed : `https://${trimmed.replace(/^\/+/, '')}`;-const plcSource = opts.plcDirectory && opts.plcDirectory.trim() ? opts.plcDirectory : DEFAULT_PLC;-const identitySource = opts.identityService && opts.identityService.trim() ? opts.identityService : DEFAULT_IDENTITY_SERVICE;-this.didResolver = new CompositeDidDocumentResolver({ methods: { plc: plcResolver, web: webResolver } });-this.handleResolver = new XrpcHandleResolver({ serviceUrl: identityBase, fetch: this.fetchImpl });-function createSlingshotAwareHandler(service: string, fetchImpl: typeof fetch): FetchHandler {-const matched = SLINGSHOT_RETRY_PATHS.find(candidate => pathname === candidate || pathname.startsWith(`${candidate}?`));
···
+60
-21
lib/utils/cache.ts
+60
-21
lib/utils/cache.ts
·········-memoize(entry: { did: string; handle?: string; doc?: DidDocument; pdsEndpoint?: string }): DidCacheSnapshot {-const existing = this.byDid.get(did) ?? (normalizedHandle ? this.byHandle.get(normalizedHandle) : undefined);···············-ensure(did: string, cid: string, loader: () => { promise: Promise<Blob>; abort: () => void }): EnsureResult {···
···························
+5
-3
lib/utils/profile.ts
+5
-3
lib/utils/profile.ts
···
···
+531
-324
src/App.tsx
+531
-324
src/App.tsx
······-const [colorSchemePreference, setColorSchemePreference] = useState<ColorSchemePreference>(() => {-<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12, alignItems: 'center', justifyContent: 'space-between' }}>-<form onSubmit={onSubmit} style={{ display: 'flex', gap: 8, flexWrap: 'wrap', flex: '1 1 320px' }}>-style={{ flex: '1 1 260px', padding: '6px 8px', borderRadius: 8, border: '1px solid', borderColor: scheme === 'dark' ? '#1e293b' : '#cbd5f5', background: scheme === 'dark' ? '#0b1120' : '#fff', color: scheme === 'dark' ? '#e2e8f0' : '#0f172a' }}-<button type="submit" style={{ padding: '6px 16px', borderRadius: 8, border: 'none', background: '#2563eb', color: '#fff', cursor: 'pointer' }}>Load</button>-<ColorSchemeToggle value={colorSchemePreference} onChange={setColorSchemePreference} scheme={scheme} />-{!submitted && <p style={{ color: mutedTextColor }}>Enter a handle to fetch your profile, latest Bluesky post, a Tangled string, and a Leaflet document.</p>}-<BlueskyQuotePost did={quoteSampleDid} rkey={quoteSampleRkey} colorScheme={colorSchemePreference} />-<LeafletDocument did={"did:plc:ttdrpj45ibqunmfhdsb4zdwq"} rkey={"3m2seagm2222c"} colorScheme={colorSchemePreference} />-Wrap your app with the provider once and drop the ready-made components wherever you need them.-<code ref={basicCodeRef} className="language-tsx" style={codeTextStyle}>{basicUsageSnippet}</code>-Need to make your own component? Compose your own renderer with the hooks and utilities that ship with the library.-<code ref={customCodeRef} className="language-tsx" style={codeTextStyle}>{customComponentSnippet}</code>-const LatestPostSummary: React.FC<{ did: string; handle?: string; colorScheme: ColorSchemePreference }> = ({ did, colorScheme }) => {-const { record, rkey, loading, error } = useLatestRecord<FeedPostRecord>(did, BLUESKY_POST_COLLECTION);-<div style={{ maxWidth: 860, margin: '40px auto', padding: '0 20px', fontFamily: 'system-ui, sans-serif' }}>-<p style={{ lineHeight: 1.4 }}>A component library for rendering common AT Protocol records for applications such as Bluesky and Tangled.</p>
······
+5
-5
src/main.tsx
+5
-5
src/main.tsx