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 { useAtProto } from "../providers/AtProtoProvider";
4
5export type TangledStringRecord = ShTangledString.Main;
6
7export interface TangledStringRendererProps {
8 record: TangledStringRecord;
9 error?: Error;
10 loading: boolean;
11 did: string;
12 rkey: string;
13 canonicalUrl?: string;
14}
15
16export const TangledStringRenderer: React.FC<TangledStringRendererProps> = ({
17 record,
18 error,
19 loading,
20 did,
21 rkey,
22 canonicalUrl,
23}) => {
24 const { tangledBaseUrl } = useAtProto();
25
26 if (error)
27 return (
28 <div style={{ padding: 8, color: "crimson" }}>
29 Failed to load snippet.
30 </div>
31 );
32 if (loading && !record) return <div style={{ padding: 8 }}>Loading…</div>;
33
34 const viewUrl =
35 canonicalUrl ??
36 `${tangledBaseUrl}/strings/${did}/${encodeURIComponent(rkey)}`;
37 const timestamp = new Date(record.createdAt).toLocaleString(undefined, {
38 dateStyle: "medium",
39 timeStyle: "short",
40 });
41 return (
42 <div style={{ ...base.container, background: `var(--atproto-color-bg-elevated)`, borderWidth: "1px", borderStyle: "solid", borderColor: `var(--atproto-color-border)`, color: `var(--atproto-color-text)` }}>
43 <div style={{ ...base.header, background: `var(--atproto-color-bg-elevated)`, borderBottomWidth: "1px", borderBottomStyle: "solid", borderBottomColor: `var(--atproto-color-border)` }}>
44 <strong style={{ ...base.filename, color: `var(--atproto-color-text)` }}>
45 {record.filename}
46 </strong>
47 <div style={{ ...base.headerRight }}>
48 <time
49 style={{ ...base.timestamp, color: `var(--atproto-color-text-secondary)` }}
50 dateTime={record.createdAt}
51 >
52 {timestamp}
53 </time>
54 <a
55 href={viewUrl}
56 target="_blank"
57 rel="noopener noreferrer"
58 style={{ ...base.headerLink, color: `var(--atproto-color-link)` }}
59 >
60 View on Tangled
61 </a>
62 </div>
63 </div>
64 {record.description && (
65 <div style={{ ...base.description, background: `var(--atproto-color-bg)`, borderTopWidth: "1px", borderTopStyle: "solid", borderTopColor: `var(--atproto-color-border)`, borderBottomWidth: "1px", borderBottomStyle: "solid", borderBottomColor: `var(--atproto-color-border)`, color: `var(--atproto-color-text)` }}>
66 {record.description}
67 </div>
68 )}
69 <pre style={{ ...base.codeBlock, background: `var(--atproto-color-bg)`, color: `var(--atproto-color-text)`, borderTopWidth: "1px", borderTopStyle: "solid", borderTopColor: `var(--atproto-color-border)` }}>
70 <code>{record.contents}</code>
71 </pre>
72 </div>
73 );
74};
75
76const base: Record<string, React.CSSProperties> = {
77 container: {
78 fontFamily: "system-ui, sans-serif",
79 borderRadius: 6,
80 overflow: "hidden",
81 transition:
82 "background-color 180ms ease, border-color 180ms ease, color 180ms ease, box-shadow 180ms ease",
83 width: "100%",
84 },
85 header: {
86 padding: "10px 16px",
87 display: "flex",
88 justifyContent: "space-between",
89 alignItems: "center",
90 gap: 12,
91 },
92 headerRight: {
93 display: "flex",
94 alignItems: "center",
95 gap: 12,
96 flexWrap: "wrap",
97 justifyContent: "flex-end",
98 },
99 filename: {
100 fontFamily:
101 'SFMono-Regular, ui-monospace, Menlo, Monaco, "Courier New", monospace',
102 fontSize: 13,
103 wordBreak: "break-all",
104 },
105 timestamp: {
106 fontSize: 12,
107 },
108 headerLink: {
109 fontSize: 12,
110 fontWeight: 600,
111 textDecoration: "none",
112 },
113 description: {
114 padding: "10px 16px",
115 fontSize: 13,
116 borderTopWidth: "1px",
117 borderTopStyle: "solid",
118 borderTopColor: "transparent",
119 },
120 codeBlock: {
121 margin: 0,
122 padding: "16px",
123 fontSize: 13,
124 overflowX: "auto",
125 borderTopWidth: "1px",
126 borderTopStyle: "solid",
127 borderTopColor: "transparent",
128 fontFamily:
129 'SFMono-Regular, ui-monospace, Menlo, Monaco, "Courier New", monospace',
130 },
131};
132
133export default TangledStringRenderer;