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;