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;