A React component library for rendering common AT Protocol records for applications such as Bluesky and Leaflet.
1import React from 'react'; 2import type { ShTangledString } from '@atcute/tangled'; 3import { useColorScheme, type ColorSchemePreference } from '../hooks/useColorScheme'; 4 5export type TangledStringRecord = ShTangledString.Main; 6 7export interface TangledStringRendererProps { 8 record: TangledStringRecord; 9 error?: Error; 10 loading: boolean; 11 colorScheme?: ColorSchemePreference; 12 did: string; 13 rkey: string; 14 canonicalUrl?: string; 15} 16 17export const TangledStringRenderer: React.FC<TangledStringRendererProps> = ({ record, error, loading, colorScheme = 'system', did, rkey, canonicalUrl }) => { 18 const scheme = useColorScheme(colorScheme); 19 20 if (error) return <div style={{ padding: 8, color: 'crimson' }}>Failed to load snippet.</div>; 21 if (loading && !record) return <div style={{ padding: 8 }}>Loading</div>; 22 23 const palette = scheme === 'dark' ? theme.dark : theme.light; 24 const viewUrl = canonicalUrl ?? `https://tangled.org/strings/${did}/${encodeURIComponent(rkey)}`; 25 const timestamp = new Date(record.createdAt).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' }); 26 return ( 27 <div style={{ ...base.container, ...palette.container }}> 28 <div style={{ ...base.header, ...palette.header }}> 29 <strong style={{ ...base.filename, ...palette.filename }}>{record.filename}</strong> 30 <div style={{ ...base.headerRight, ...palette.headerRight }}> 31 <time style={{ ...base.timestamp, ...palette.timestamp }} dateTime={record.createdAt}>{timestamp}</time> 32 <a href={viewUrl} target="_blank" rel="noopener noreferrer" style={{ ...base.headerLink, ...palette.headerLink }}> 33 View on Tangled 34 </a> 35 </div> 36 </div> 37 {record.description && ( 38 <div style={{ ...base.description, ...palette.description }}>{record.description}</div> 39 )} 40 <pre style={{ ...base.codeBlock, ...palette.codeBlock }}> 41 <code>{record.contents}</code> 42 </pre> 43 </div> 44 ); 45}; 46 47const base: Record<string, React.CSSProperties> = { 48 container: { 49 fontFamily: 'system-ui, sans-serif', 50 borderRadius: 6, 51 overflow: 'hidden', 52 transition: 'background-color 180ms ease, border-color 180ms ease, color 180ms ease, box-shadow 180ms ease', 53 width: '100%' 54 }, 55 header: { 56 padding: '10px 16px', 57 display: 'flex', 58 justifyContent: 'space-between', 59 alignItems: 'center', 60 gap: 12 61 }, 62 headerRight: { 63 display: 'flex', 64 alignItems: 'center', 65 gap: 12, 66 flexWrap: 'wrap', 67 justifyContent: 'flex-end' 68 }, 69 filename: { 70 fontFamily: 'SFMono-Regular, ui-monospace, Menlo, Monaco, "Courier New", monospace', 71 fontSize: 13, 72 wordBreak: 'break-all' 73 }, 74 timestamp: { 75 fontSize: 12 76 }, 77 headerLink: { 78 fontSize: 12, 79 fontWeight: 600, 80 textDecoration: 'none' 81 }, 82 description: { 83 padding: '10px 16px', 84 fontSize: 13, 85 borderTop: '1px solid transparent' 86 }, 87 codeBlock: { 88 margin: 0, 89 padding: '16px', 90 fontSize: 13, 91 overflowX: 'auto', 92 borderTop: '1px solid transparent', 93 fontFamily: 'SFMono-Regular, ui-monospace, Menlo, Monaco, "Courier New", monospace' 94 } 95}; 96 97const theme = { 98 light: { 99 container: { 100 border: '1px solid #d0d7de', 101 background: '#f6f8fa', 102 color: '#1f2328', 103 boxShadow: '0 1px 2px rgba(31,35,40,0.05)' 104 }, 105 header: { 106 background: '#f6f8fa', 107 borderBottom: '1px solid #d0d7de' 108 }, 109 headerRight: {}, 110 filename: { 111 color: '#1f2328' 112 }, 113 timestamp: { 114 color: '#57606a' 115 }, 116 headerLink: { 117 color: '#2563eb' 118 }, 119 description: { 120 background: '#ffffff', 121 borderBottom: '1px solid #d0d7de', 122 borderTopColor: '#d0d7de', 123 color: '#1f2328' 124 }, 125 codeBlock: { 126 background: '#ffffff', 127 color: '#1f2328', 128 borderTopColor: '#d0d7de' 129 } 130 }, 131 dark: { 132 container: { 133 border: '1px solid #30363d', 134 background: '#0d1117', 135 color: '#c9d1d9', 136 boxShadow: '0 0 0 1px rgba(1,4,9,0.3) inset' 137 }, 138 header: { 139 background: '#161b22', 140 borderBottom: '1px solid #30363d' 141 }, 142 headerRight: {}, 143 filename: { 144 color: '#c9d1d9' 145 }, 146 timestamp: { 147 color: '#8b949e' 148 }, 149 headerLink: { 150 color: '#58a6ff' 151 }, 152 description: { 153 background: '#161b22', 154 borderBottom: '1px solid #30363d', 155 borderTopColor: '#30363d', 156 color: '#c9d1d9' 157 }, 158 codeBlock: { 159 background: '#0d1117', 160 color: '#c9d1d9', 161 borderTopColor: '#30363d' 162 } 163 } 164} satisfies Record<'light' | 'dark', Record<string, React.CSSProperties>>; 165 166export default TangledStringRenderer;