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