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