import React, { useMemo, useRef } from "react";
import {
useColorScheme,
type ColorSchemePreference,
} from "../hooks/useColorScheme";
import { useDidResolution } from "../hooks/useDidResolution";
import { useBlob } from "../hooks/useBlob";
import {
parseAtUri,
formatDidForLabel,
toBlueskyPostUrl,
leafletRkeyUrl,
normalizeLeafletBasePath,
} from "../utils/at-uri";
import { BlueskyPost } from "../components/BlueskyPost";
import type {
LeafletDocumentRecord,
LeafletLinearDocumentPage,
LeafletLinearDocumentBlock,
LeafletBlock,
LeafletTextBlock,
LeafletHeaderBlock,
LeafletBlockquoteBlock,
LeafletImageBlock,
LeafletUnorderedListBlock,
LeafletListItem,
LeafletWebsiteBlock,
LeafletIFrameBlock,
LeafletMathBlock,
LeafletCodeBlock,
LeafletBskyPostBlock,
LeafletAlignmentValue,
LeafletRichTextFacet,
LeafletRichTextFeature,
LeafletPublicationRecord,
} from "../types/leaflet";
export interface LeafletDocumentRendererProps {
record: LeafletDocumentRecord;
loading: boolean;
error?: Error;
colorScheme?: ColorSchemePreference;
did: string;
rkey: string;
canonicalUrl?: string;
publicationBaseUrl?: string;
publicationRecord?: LeafletPublicationRecord;
}
export const LeafletDocumentRenderer: React.FC<
LeafletDocumentRendererProps
> = ({
record,
loading,
error,
colorScheme = "system",
did,
rkey,
canonicalUrl,
publicationBaseUrl,
publicationRecord,
}) => {
const scheme = useColorScheme(colorScheme);
const palette = scheme === "dark" ? theme.dark : theme.light;
const authorDid = record.author?.startsWith("did:")
? record.author
: undefined;
const publicationUri = useMemo(
() => parseAtUri(record.publication),
[record.publication],
);
const postUrl = useMemo(() => {
const postRefUri = record.postRef?.uri;
if (!postRefUri) return undefined;
const parsed = parseAtUri(postRefUri);
return parsed ? toBlueskyPostUrl(parsed) : undefined;
}, [record.postRef?.uri]);
const { handle: publicationHandle } = useDidResolution(publicationUri?.did);
const fallbackAuthorLabel = useAuthorLabel(record.author, authorDid);
const resolvedPublicationLabel =
publicationRecord?.name?.trim() ??
(publicationHandle
? `@${publicationHandle}`
: publicationUri
? formatDidForLabel(publicationUri.did)
: undefined);
const authorLabel = resolvedPublicationLabel ?? fallbackAuthorLabel;
const authorHref = publicationUri
? `https://bsky.app/profile/${publicationUri.did}`
: undefined;
if (error)
return (
Failed to load leaflet.
);
if (loading && !record)
return Loading leaflet…
;
if (!record)
return (
Leaflet record missing.
);
const publishedAt = record.publishedAt
? new Date(record.publishedAt)
: undefined;
const publishedLabel = publishedAt
? publishedAt.toLocaleString(undefined, {
dateStyle: "long",
timeStyle: "short",
})
: undefined;
const fallbackLeafletUrl = `https://bsky.app/leaflet/${encodeURIComponent(did)}/${encodeURIComponent(rkey)}`;
const publicationRoot =
publicationBaseUrl ?? publicationRecord?.base_path ?? undefined;
const resolvedPublicationRoot = publicationRoot
? normalizeLeafletBasePath(publicationRoot)
: undefined;
const publicationLeafletUrl = leafletRkeyUrl(publicationRoot, rkey);
const viewUrl =
canonicalUrl ??
publicationLeafletUrl ??
postUrl ??
(publicationUri
? `https://bsky.app/profile/${publicationUri.did}`
: undefined) ??
fallbackLeafletUrl;
const metaItems: React.ReactNode[] = [];
if (authorLabel) {
const authorNode = authorHref ? (
{authorLabel}
) : (
authorLabel
);
metaItems.push(By {authorNode});
}
if (publishedLabel)
metaItems.push(
,
);
if (resolvedPublicationRoot) {
metaItems.push(
{resolvedPublicationRoot.replace(/^https?:\/\//, "")}
,
);
}
if (viewUrl) {
metaItems.push(
View source
,
);
}
return (
{record.pages?.map((page, pageIndex) => (
))}
);
};
const LeafletPageRenderer: React.FC<{
page: LeafletLinearDocumentPage;
documentDid: string;
colorScheme: "light" | "dark";
}> = ({ page, documentDid, colorScheme }) => {
if (!page.blocks?.length) return null;
return (
{page.blocks.map((blockWrapper, idx) => (
))}
);
};
interface LeafletBlockRendererProps {
wrapper: LeafletLinearDocumentBlock;
documentDid: string;
colorScheme: "light" | "dark";
isFirst?: boolean;
}
const LeafletBlockRenderer: React.FC = ({
wrapper,
documentDid,
colorScheme,
isFirst,
}) => {
const block = wrapper.block;
if (!block || !("$type" in block) || !block.$type) {
return null;
}
const alignment = alignmentValue(wrapper.alignment);
switch (block.$type) {
case "pub.leaflet.blocks.header":
return (
);
case "pub.leaflet.blocks.blockquote":
return (
);
case "pub.leaflet.blocks.image":
return (
);
case "pub.leaflet.blocks.unorderedList":
return (
);
case "pub.leaflet.blocks.website":
return (
);
case "pub.leaflet.blocks.iframe":
return (
);
case "pub.leaflet.blocks.math":
return (
);
case "pub.leaflet.blocks.code":
return (
);
case "pub.leaflet.blocks.horizontalRule":
return (
);
case "pub.leaflet.blocks.bskyPost":
return (
);
case "pub.leaflet.blocks.text":
default:
return (
);
}
};
const LeafletTextBlockView: React.FC<{
block: LeafletTextBlock;
alignment?: React.CSSProperties["textAlign"];
colorScheme: "light" | "dark";
isFirst?: boolean;
}> = ({ block, alignment, colorScheme, isFirst }) => {
const segments = useMemo(
() => createFacetedSegments(block.plaintext, block.facets),
[block.plaintext, block.facets],
);
const textContent = block.plaintext ?? "";
if (!textContent.trim() && segments.length === 0) {
return null;
}
const palette = colorScheme === "dark" ? theme.dark : theme.light;
const style: React.CSSProperties = {
...base.paragraph,
...palette.paragraph,
...(alignment ? { textAlign: alignment } : undefined),
...(isFirst ? { marginTop: 0 } : undefined),
};
return (
{segments.map((segment, idx) => (
{renderSegment(segment, colorScheme)}
))}
);
};
const LeafletHeaderBlockView: React.FC<{
block: LeafletHeaderBlock;
alignment?: React.CSSProperties["textAlign"];
colorScheme: "light" | "dark";
isFirst?: boolean;
}> = ({ block, alignment, colorScheme, isFirst }) => {
const palette = colorScheme === "dark" ? theme.dark : theme.light;
const level =
block.level && block.level >= 1 && block.level <= 6 ? block.level : 2;
const segments = useMemo(
() => createFacetedSegments(block.plaintext, block.facets),
[block.plaintext, block.facets],
);
const normalizedLevel = Math.min(Math.max(level, 1), 6) as
| 1
| 2
| 3
| 4
| 5
| 6;
const headingTag = (["h1", "h2", "h3", "h4", "h5", "h6"] as const)[
normalizedLevel - 1
];
const headingStyles = palette.heading[normalizedLevel];
const style: React.CSSProperties = {
...base.heading,
...headingStyles,
...(alignment ? { textAlign: alignment } : undefined),
...(isFirst ? { marginTop: 0 } : undefined),
};
return React.createElement(
headingTag,
{ style },
segments.map((segment, idx) => (
{renderSegment(segment, colorScheme)}
)),
);
};
const LeafletBlockquoteBlockView: React.FC<{
block: LeafletBlockquoteBlock;
alignment?: React.CSSProperties["textAlign"];
colorScheme: "light" | "dark";
isFirst?: boolean;
}> = ({ block, alignment, colorScheme, isFirst }) => {
const segments = useMemo(
() => createFacetedSegments(block.plaintext, block.facets),
[block.plaintext, block.facets],
);
const textContent = block.plaintext ?? "";
if (!textContent.trim() && segments.length === 0) {
return null;
}
const palette = colorScheme === "dark" ? theme.dark : theme.light;
return (
{segments.map((segment, idx) => (
{renderSegment(segment, colorScheme)}
))}
);
};
const LeafletImageBlockView: React.FC<{
block: LeafletImageBlock;
alignment?: React.CSSProperties["textAlign"];
documentDid: string;
colorScheme: "light" | "dark";
}> = ({ block, alignment, documentDid, colorScheme }) => {
const palette = colorScheme === "dark" ? theme.dark : theme.light;
const cid = block.image?.ref?.$link ?? block.image?.cid;
const { url, loading, error } = useBlob(documentDid, cid);
const aspectRatio =
block.aspectRatio?.height && block.aspectRatio?.width
? `${block.aspectRatio.width} / ${block.aspectRatio.height}`
: undefined;
return (
{url && !error ? (

) : (
{loading
? "Loading image…"
: error
? "Image unavailable"
: "No image"}
)}
{block.alt && block.alt.trim().length > 0 && (
{block.alt}
)}
);
};
const LeafletListBlockView: React.FC<{
block: LeafletUnorderedListBlock;
alignment?: React.CSSProperties["textAlign"];
documentDid: string;
colorScheme: "light" | "dark";
}> = ({ block, alignment, documentDid, colorScheme }) => {
const palette = colorScheme === "dark" ? theme.dark : theme.light;
return (
{block.children?.map((child, idx) => (
))}
);
};
const LeafletListItemRenderer: React.FC<{
item: LeafletListItem;
documentDid: string;
colorScheme: "light" | "dark";
alignment?: React.CSSProperties["textAlign"];
}> = ({ item, documentDid, colorScheme, alignment }) => {
return (
{item.children && item.children.length > 0 && (
{item.children.map((child, idx) => (
))}
)}
);
};
const LeafletInlineBlock: React.FC<{
block: LeafletBlock;
colorScheme: "light" | "dark";
documentDid: string;
alignment?: React.CSSProperties["textAlign"];
}> = ({ block, colorScheme, documentDid, alignment }) => {
switch (block.$type) {
case "pub.leaflet.blocks.header":
return (
);
case "pub.leaflet.blocks.blockquote":
return (
);
case "pub.leaflet.blocks.image":
return (
);
default:
return (
);
}
};
const LeafletWebsiteBlockView: React.FC<{
block: LeafletWebsiteBlock;
alignment?: React.CSSProperties["textAlign"];
documentDid: string;
colorScheme: "light" | "dark";
}> = ({ block, alignment, documentDid, colorScheme }) => {
const palette = colorScheme === "dark" ? theme.dark : theme.light;
const previewCid =
block.previewImage?.ref?.$link ?? block.previewImage?.cid;
const { url, loading, error } = useBlob(documentDid, previewCid);
return (
{url && !error ? (
) : (
{loading ? "Loading preview…" : "Open link"}
)}
{block.title && (
{block.title}
)}
{block.description && (
{block.description}
)}
{block.src}
);
};
const LeafletIframeBlockView: React.FC<{
block: LeafletIFrameBlock;
alignment?: React.CSSProperties["textAlign"];
}> = ({ block, alignment }) => {
return (
);
};
const LeafletMathBlockView: React.FC<{
block: LeafletMathBlock;
alignment?: React.CSSProperties["textAlign"];
colorScheme: "light" | "dark";
}> = ({ block, alignment, colorScheme }) => {
const palette = colorScheme === "dark" ? theme.dark : theme.light;
return (
{block.tex}
);
};
const LeafletCodeBlockView: React.FC<{
block: LeafletCodeBlock;
alignment?: React.CSSProperties["textAlign"];
colorScheme: "light" | "dark";
}> = ({ block, alignment, colorScheme }) => {
const palette = colorScheme === "dark" ? theme.dark : theme.light;
const codeRef = useRef(null);
const langClass = block.language
? `language-${block.language.toLowerCase()}`
: undefined;
return (
{block.plaintext}
);
};
const LeafletHorizontalRuleBlockView: React.FC<{
alignment?: React.CSSProperties["textAlign"];
colorScheme: "light" | "dark";
}> = ({ alignment, colorScheme }) => {
const palette = colorScheme === "dark" ? theme.dark : theme.light;
return (
);
};
const LeafletBskyPostBlockView: React.FC<{
block: LeafletBskyPostBlock;
colorScheme: "light" | "dark";
}> = ({ block, colorScheme }) => {
const parsed = parseAtUri(block.postRef?.uri);
if (!parsed) {
return (
Referenced post unavailable.
);
}
return (
);
};
function alignmentValue(
value?: LeafletAlignmentValue,
): React.CSSProperties["textAlign"] | undefined {
if (!value) return undefined;
let normalized = value.startsWith("#") ? value.slice(1) : value;
if (normalized.includes("#")) {
normalized = normalized.split("#").pop() ?? normalized;
}
if (normalized.startsWith("lex:")) {
normalized = normalized.split(":").pop() ?? normalized;
}
switch (normalized) {
case "textAlignLeft":
return "left";
case "textAlignCenter":
return "center";
case "textAlignRight":
return "right";
case "textAlignJustify":
return "justify";
default:
return undefined;
}
}
function useAuthorLabel(
author: string | undefined,
authorDid: string | undefined,
): string | undefined {
const { handle } = useDidResolution(authorDid);
if (!author) return undefined;
if (handle) return `@${handle}`;
if (authorDid) return formatDidForLabel(authorDid);
return author;
}
interface Segment {
text: string;
features: LeafletRichTextFeature[];
}
function createFacetedSegments(
plaintext: string,
facets?: LeafletRichTextFacet[],
): Segment[] {
if (!facets?.length) {
return [{ text: plaintext, features: [] }];
}
const prefix = buildBytePrefix(plaintext);
const startEvents = new Map();
const endEvents = new Map();
const boundaries = new Set([0, prefix.length - 1]);
for (const facet of facets) {
const { byteStart, byteEnd } = facet.index ?? {};
if (
typeof byteStart !== "number" ||
typeof byteEnd !== "number" ||
byteStart >= byteEnd
)
continue;
const start = byteOffsetToCharIndex(prefix, byteStart);
const end = byteOffsetToCharIndex(prefix, byteEnd);
if (start >= end) continue;
boundaries.add(start);
boundaries.add(end);
if (facet.features?.length) {
startEvents.set(start, [
...(startEvents.get(start) ?? []),
...facet.features,
]);
endEvents.set(end, [
...(endEvents.get(end) ?? []),
...facet.features,
]);
}
}
const sortedBounds = [...boundaries].sort((a, b) => a - b);
const segments: Segment[] = [];
let active: LeafletRichTextFeature[] = [];
for (let i = 0; i < sortedBounds.length - 1; i++) {
const boundary = sortedBounds[i];
const next = sortedBounds[i + 1];
const endFeatures = endEvents.get(boundary);
if (endFeatures?.length) {
active = active.filter((feature) => !endFeatures.includes(feature));
}
const startFeatures = startEvents.get(boundary);
if (startFeatures?.length) {
active = [...active, ...startFeatures];
}
if (boundary === next) continue;
const text = sliceByCharRange(plaintext, boundary, next);
segments.push({ text, features: active.slice() });
}
return segments;
}
function buildBytePrefix(text: string): number[] {
const encoder = new TextEncoder();
const prefix: number[] = [0];
let byteCount = 0;
for (let i = 0; i < text.length; ) {
const codePoint = text.codePointAt(i)!;
const char = String.fromCodePoint(codePoint);
const encoded = encoder.encode(char);
byteCount += encoded.length;
prefix.push(byteCount);
i += codePoint > 0xffff ? 2 : 1;
}
return prefix;
}
function byteOffsetToCharIndex(prefix: number[], byteOffset: number): number {
for (let i = 0; i < prefix.length; i++) {
if (prefix[i] === byteOffset) return i;
if (prefix[i] > byteOffset) return Math.max(0, i - 1);
}
return prefix.length - 1;
}
function sliceByCharRange(text: string, start: number, end: number): string {
if (start <= 0 && end >= text.length) return text;
let result = "";
let charIndex = 0;
for (let i = 0; i < text.length && charIndex < end; ) {
const codePoint = text.codePointAt(i)!;
const char = String.fromCodePoint(codePoint);
if (charIndex >= start && charIndex < end) result += char;
i += codePoint > 0xffff ? 2 : 1;
charIndex++;
}
return result;
}
function renderSegment(
segment: Segment,
colorScheme: "light" | "dark",
): React.ReactNode {
const parts = segment.text.split("\n");
return parts.flatMap((part, idx) => {
const key = `${segment.text}-${idx}-${part.length}`;
const wrapped = applyFeatures(
part.length ? part : "\u00a0",
segment.features,
key,
colorScheme,
);
if (idx === parts.length - 1) return wrapped;
return [wrapped,
];
});
}
function applyFeatures(
content: React.ReactNode,
features: LeafletRichTextFeature[],
key: string,
colorScheme: "light" | "dark",
): React.ReactNode {
if (!features?.length)
return {content};
return (
{features.reduce(
(child, feature, idx) =>
wrapFeature(
child,
feature,
`${key}-feature-${idx}`,
colorScheme,
),
content,
)}
);
}
function wrapFeature(
child: React.ReactNode,
feature: LeafletRichTextFeature,
key: string,
colorScheme: "light" | "dark",
): React.ReactNode {
switch (feature.$type) {
case "pub.leaflet.richtext.facet#link":
return (
{child}
);
case "pub.leaflet.richtext.facet#code":
return (
{child}
);
case "pub.leaflet.richtext.facet#highlight":
return (
{child}
);
case "pub.leaflet.richtext.facet#underline":
return (
{child}
);
case "pub.leaflet.richtext.facet#strikethrough":
return (
{child}
);
case "pub.leaflet.richtext.facet#bold":
return {child};
case "pub.leaflet.richtext.facet#italic":
return {child};
case "pub.leaflet.richtext.facet#id":
return (
{child}
);
default:
return {child};
}
}
const base: Record = {
container: {
display: "flex",
flexDirection: "column",
gap: 24,
padding: "24px 28px",
borderRadius: 20,
border: "1px solid transparent",
maxWidth: 720,
width: "100%",
fontFamily:
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
},
header: {
display: "flex",
flexDirection: "column",
gap: 16,
},
headerContent: {
display: "flex",
flexDirection: "column",
gap: 8,
},
title: {
fontSize: 32,
margin: 0,
lineHeight: 1.15,
},
subtitle: {
margin: 0,
fontSize: 16,
lineHeight: 1.5,
},
meta: {
display: "flex",
flexWrap: "wrap",
gap: 8,
alignItems: "center",
fontSize: 14,
},
body: {
display: "flex",
flexDirection: "column",
gap: 18,
},
page: {
display: "flex",
flexDirection: "column",
gap: 18,
},
paragraph: {
margin: "1em 0 0",
lineHeight: 1.65,
fontSize: 16,
},
heading: {
margin: "0.5em 0 0",
fontWeight: 700,
},
blockquote: {
margin: "1em 0 0",
padding: "0.6em 1em",
borderLeft: "4px solid",
},
figure: {
margin: "1.2em 0 0",
display: "flex",
flexDirection: "column",
gap: 12,
},
imageWrapper: {
borderRadius: 16,
overflow: "hidden",
width: "100%",
position: "relative",
background: "#e2e8f0",
},
image: {
width: "100%",
height: "100%",
objectFit: "cover",
display: "block",
},
imagePlaceholder: {
width: "100%",
padding: "24px 16px",
textAlign: "center",
},
caption: {
fontSize: 13,
lineHeight: 1.4,
},
list: {
paddingLeft: 28,
margin: "1em 0 0",
listStyleType: "disc",
listStylePosition: "outside",
},
nestedList: {
paddingLeft: 20,
marginTop: 8,
listStyleType: "circle",
listStylePosition: "outside",
},
listItem: {
marginTop: 8,
display: "list-item",
},
linkCard: {
borderRadius: 16,
border: "1px solid",
display: "flex",
flexDirection: "column",
overflow: "hidden",
textDecoration: "none",
},
linkPreview: {
width: "100%",
height: 180,
objectFit: "cover",
},
linkPreviewPlaceholder: {
width: "100%",
height: 180,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 14,
},
linkContent: {
display: "flex",
flexDirection: "column",
gap: 6,
padding: "16px 18px",
},
iframe: {
width: "100%",
height: 360,
border: "1px solid #cbd5f5",
borderRadius: 16,
},
math: {
margin: "1em 0 0",
padding: "14px 16px",
borderRadius: 12,
fontFamily: 'Menlo, Consolas, "SFMono-Regular", ui-monospace',
overflowX: "auto",
},
code: {
margin: "1em 0 0",
padding: "14px 16px",
borderRadius: 12,
overflowX: "auto",
fontSize: 14,
},
hr: {
border: 0,
borderTop: "1px solid",
margin: "24px 0 0",
},
embedFallback: {
padding: "12px 16px",
borderRadius: 12,
border: "1px solid #e2e8f0",
fontSize: 14,
},
};
const theme = {
light: {
container: {
background: "#ffffff",
borderColor: "#e2e8f0",
color: "#0f172a",
boxShadow: "0 4px 18px rgba(15, 23, 42, 0.06)",
},
header: {},
title: {
color: "#0f172a",
},
subtitle: {
color: "#475569",
},
meta: {
color: "#64748b",
},
metaLink: {
color: "#2563eb",
textDecoration: "none",
} satisfies React.CSSProperties,
metaSeparator: {
margin: "0 4px",
} satisfies React.CSSProperties,
paragraph: {
color: "#1f2937",
},
heading: {
1: { color: "#0f172a", fontSize: 30 },
2: { color: "#0f172a", fontSize: 28 },
3: { color: "#0f172a", fontSize: 24 },
4: { color: "#0f172a", fontSize: 20 },
5: { color: "#0f172a", fontSize: 18 },
6: { color: "#0f172a", fontSize: 16 },
} satisfies Record,
blockquote: {
background: "#f8fafc",
borderColor: "#cbd5f5",
color: "#1f2937",
},
figure: {},
imageWrapper: {
background: "#e2e8f0",
},
image: {},
imagePlaceholder: {
color: "#475569",
},
caption: {
color: "#475569",
},
list: {
color: "#1f2937",
},
linkCard: {
borderColor: "#e2e8f0",
background: "#f8fafc",
color: "#0f172a",
},
linkPreview: {},
linkPreviewPlaceholder: {
background: "#e2e8f0",
color: "#475569",
},
linkTitle: {
fontSize: 16,
color: "#0f172a",
} satisfies React.CSSProperties,
linkDescription: {
margin: 0,
fontSize: 14,
color: "#475569",
lineHeight: 1.5,
} satisfies React.CSSProperties,
linkUrl: {
fontSize: 13,
color: "#2563eb",
wordBreak: "break-all",
} satisfies React.CSSProperties,
math: {
background: "#f1f5f9",
color: "#1f2937",
border: "1px solid #e2e8f0",
},
code: {
background: "#0f172a",
color: "#e2e8f0",
},
hr: {
borderColor: "#e2e8f0",
},
},
dark: {
container: {
background: "rgba(15, 23, 42, 0.6)",
borderColor: "rgba(148, 163, 184, 0.3)",
color: "#e2e8f0",
backdropFilter: "blur(8px)",
boxShadow: "0 10px 40px rgba(2, 6, 23, 0.45)",
},
header: {},
title: {
color: "#f8fafc",
},
subtitle: {
color: "#cbd5f5",
},
meta: {
color: "#94a3b8",
},
metaLink: {
color: "#38bdf8",
textDecoration: "none",
} satisfies React.CSSProperties,
metaSeparator: {
margin: "0 4px",
} satisfies React.CSSProperties,
paragraph: {
color: "#e2e8f0",
},
heading: {
1: { color: "#f8fafc", fontSize: 30 },
2: { color: "#f8fafc", fontSize: 28 },
3: { color: "#f8fafc", fontSize: 24 },
4: { color: "#e2e8f0", fontSize: 20 },
5: { color: "#e2e8f0", fontSize: 18 },
6: { color: "#e2e8f0", fontSize: 16 },
} satisfies Record,
blockquote: {
background: "rgba(30, 41, 59, 0.6)",
borderColor: "#38bdf8",
color: "#e2e8f0",
},
figure: {},
imageWrapper: {
background: "#1e293b",
},
image: {},
imagePlaceholder: {
color: "#94a3b8",
},
caption: {
color: "#94a3b8",
},
list: {
color: "#f1f5f9",
},
linkCard: {
borderColor: "rgba(148, 163, 184, 0.3)",
background: "rgba(15, 23, 42, 0.8)",
color: "#e2e8f0",
},
linkPreview: {},
linkPreviewPlaceholder: {
background: "#1e293b",
color: "#94a3b8",
},
linkTitle: {
fontSize: 16,
color: "#e0f2fe",
} satisfies React.CSSProperties,
linkDescription: {
margin: 0,
fontSize: 14,
color: "#cbd5f5",
lineHeight: 1.5,
} satisfies React.CSSProperties,
linkUrl: {
fontSize: 13,
color: "#38bdf8",
wordBreak: "break-all",
} satisfies React.CSSProperties,
math: {
background: "rgba(15, 23, 42, 0.8)",
color: "#e2e8f0",
border: "1px solid rgba(148, 163, 184, 0.35)",
},
code: {
background: "#020617",
color: "#e2e8f0",
},
hr: {
borderColor: "rgba(148, 163, 184, 0.3)",
},
},
} as const;
const linkStyles = {
light: {
color: "#2563eb",
textDecoration: "underline",
} satisfies React.CSSProperties,
dark: {
color: "#38bdf8",
textDecoration: "underline",
} satisfies React.CSSProperties,
} as const;
const inlineCodeStyles = {
light: {
fontFamily: 'Menlo, Consolas, "SFMono-Regular", ui-monospace',
background: "#f1f5f9",
padding: "0 4px",
borderRadius: 4,
} satisfies React.CSSProperties,
dark: {
fontFamily: 'Menlo, Consolas, "SFMono-Regular", ui-monospace',
background: "#1e293b",
padding: "0 4px",
borderRadius: 4,
} satisfies React.CSSProperties,
} as const;
const highlightStyles = {
light: {
background: "#fef08a",
} satisfies React.CSSProperties,
dark: {
background: "#facc15",
color: "#0f172a",
} satisfies React.CSSProperties,
} as const;
export default LeafletDocumentRenderer;