feat: implement markdown components for enhanced rendering

This commit is contained in:
Johnny 2026-01-23 09:04:42 +08:00
parent c0d6224155
commit 7154ce0228
16 changed files with 472 additions and 354 deletions

View File

@ -6,14 +6,15 @@ import { useAuth } from "@/contexts/AuthContext";
import { cn } from "@/lib/utils";
import { getThemeWithFallback, resolveTheme } from "@/utils/theme";
import { MermaidBlock } from "./MermaidBlock";
import type { ReactMarkdownProps } from "./markdown/types";
import { extractCodeContent, extractLanguage } from "./utils";
interface CodeBlockProps {
interface CodeBlockProps extends ReactMarkdownProps {
children?: React.ReactNode;
className?: string;
}
export const CodeBlock = ({ children, className, ...props }: CodeBlockProps) => {
export const CodeBlock = ({ children, className, node: _node, ...props }: CodeBlockProps) => {
const { userGeneralSetting } = useAuth();
const [copied, setCopied] = useState(false);
@ -114,20 +115,41 @@ export const CodeBlock = ({ children, className, ...props }: CodeBlockProps) =>
};
return (
<pre className="relative">
<div className="absolute right-2 leading-3 top-1.5 flex flex-row justify-end items-center gap-1 opacity-60 hover:opacity-80">
<span className="text-[10px] font-medium text-muted-foreground/60 uppercase tracking-wider select-none">{language}</span>
<pre className="relative my-3 rounded-lg border border-border bg-muted/30 overflow-hidden">
{/* Header with language label and copy button */}
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border bg-accent/30">
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide select-none">{language || "text"}</span>
<button
onClick={handleCopy}
className={cn("rounded-md transition-all", "hover:bg-accent/50", copied ? "text-primary" : "text-muted-foreground")}
className={cn(
"inline-flex items-center gap-1.5 px-2 py-0.5 rounded-md text-xs font-medium",
"transition-all duration-200",
"hover:bg-accent/80 active:scale-95",
copied ? "text-primary bg-primary/10" : "text-muted-foreground bg-transparent",
)}
aria-label={copied ? "Copied" : "Copy code"}
title={copied ? "Copied!" : "Copy code"}
>
{copied ? <CheckIcon className="w-3 h-3" /> : <CopyIcon className="w-3 h-3" />}
{copied ? (
<>
<CheckIcon className="w-3.5 h-3.5" />
<span>Copied</span>
</>
) : (
<>
<CopyIcon className="w-3.5 h-3.5" />
<span>Copy</span>
</>
)}
</button>
</div>
<div className={className} {...props}>
<code className={`language-${language}`} dangerouslySetInnerHTML={{ __html: highlightedCode }} />
{/* Code content */}
<div className="overflow-x-auto">
<code
className={cn("block px-3 py-2 text-sm leading-relaxed", `language-${language}`)}
dangerouslySetInnerHTML={{ __html: highlightedCode }}
/>
</div>
</pre>
);

View File

@ -0,0 +1,83 @@
import { cn } from "@/lib/utils";
import type { ReactMarkdownProps } from "./markdown/types";
interface TableProps extends React.HTMLAttributes<HTMLTableElement>, ReactMarkdownProps {
children: React.ReactNode;
}
export const Table = ({ children, className, node: _node, ...props }: TableProps) => {
return (
<div className="w-full overflow-x-auto rounded-lg border border-border my-4">
<table className={cn("w-full border-collapse text-sm", className)} {...props}>
{children}
</table>
</div>
);
};
interface TableHeadProps extends React.HTMLAttributes<HTMLTableSectionElement>, ReactMarkdownProps {
children: React.ReactNode;
}
export const TableHead = ({ children, className, node: _node, ...props }: TableHeadProps) => {
return (
<thead className={cn("bg-accent", className)} {...props}>
{children}
</thead>
);
};
interface TableBodyProps extends React.HTMLAttributes<HTMLTableSectionElement>, ReactMarkdownProps {
children: React.ReactNode;
}
export const TableBody = ({ children, className, node: _node, ...props }: TableBodyProps) => {
return (
<tbody className={cn("divide-y divide-border", className)} {...props}>
{children}
</tbody>
);
};
interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement>, ReactMarkdownProps {
children: React.ReactNode;
}
export const TableRow = ({ children, className, node: _node, ...props }: TableRowProps) => {
return (
<tr className={cn("transition-colors hover:bg-muted/50", "even:bg-accent/50", className)} {...props}>
{children}
</tr>
);
};
interface TableHeaderCellProps extends React.ThHTMLAttributes<HTMLTableCellElement>, ReactMarkdownProps {
children: React.ReactNode;
}
export const TableHeaderCell = ({ children, className, node: _node, ...props }: TableHeaderCellProps) => {
return (
<th
className={cn(
"px-4 py-3 text-left text-xs font-semibold uppercase tracking-wider text-muted-foreground",
"border-b-2 border-border",
className,
)}
{...props}
>
{children}
</th>
);
};
interface TableCellProps extends React.TdHTMLAttributes<HTMLTableCellElement>, ReactMarkdownProps {
children: React.ReactNode;
}
export const TableCell = ({ children, className, node: _node, ...props }: TableCellProps) => {
return (
<td className={cn("px-4 py-3 text-left", className)} {...props}>
{children}
</td>
);
};

View File

@ -1,16 +1,15 @@
import type { Element } from "hast";
import { useRef } from "react";
import { Checkbox } from "@/components/ui/checkbox";
import { useUpdateMemo } from "@/hooks/useMemoQueries";
import { toggleTaskAtIndex } from "@/utils/markdown-manipulation";
import { useMemoViewContext, useMemoViewDerived } from "../MemoView/MemoViewContext";
import type { ReactMarkdownProps } from "./markdown/types";
interface TaskListItemProps extends React.InputHTMLAttributes<HTMLInputElement> {
node?: Element; // AST node from react-markdown
interface TaskListItemProps extends React.InputHTMLAttributes<HTMLInputElement>, ReactMarkdownProps {
checked?: boolean;
}
export const TaskListItem: React.FC<TaskListItemProps> = ({ checked, ...props }) => {
export const TaskListItem: React.FC<TaskListItemProps> = ({ checked, node: _node, ...props }) => {
const { memo } = useMemoViewContext();
const { readonly } = useMemoViewDerived();
const checkboxRef = useRef<HTMLButtonElement>(null);
@ -35,14 +34,19 @@ export const TaskListItem: React.FC<TaskListItemProps> = ({ checked, ...props })
if (taskIndexStr !== null) {
taskIndex = parseInt(taskIndexStr);
} else {
// Fallback: Calculate index by counting ALL task list items in the memo
// Find the markdown-content container by traversing up from the list item
const container = listItem.closest(".markdown-content");
if (!container) {
return;
// Fallback: Calculate index by counting task list items
// Walk up to find the parent element with all task items
let searchRoot = listItem.parentElement;
while (searchRoot && !searchRoot.classList.contains("contains-task-list")) {
searchRoot = searchRoot.parentElement;
}
const allTaskItems = container.querySelectorAll("li.task-list-item");
// If not found, search from the document root
if (!searchRoot) {
searchRoot = document.body;
}
const allTaskItems = searchRoot.querySelectorAll("li.task-list-item");
for (let i = 0; i < allTaskItems.length; i++) {
if (allTaskItems[i] === listItem) {
taskIndex = i;

View File

@ -17,6 +17,8 @@ import { CodeBlock } from "./CodeBlock";
import { isTagNode, isTaskListItemNode } from "./ConditionalComponent";
import { COMPACT_MODE_CONFIG, SANITIZE_SCHEMA } from "./constants";
import { useCompactLabel, useCompactMode } from "./hooks";
import { Blockquote, Heading, HorizontalRule, Image, InlineCode, Link, List, ListItem, Paragraph } from "./markdown";
import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow } from "./Table";
import { Tag } from "./Tag";
import { TaskListItem } from "./TaskListItem";
import type { MemoContentProps } from "./types";
@ -37,7 +39,7 @@ const MemoContent = (props: MemoContentProps) => {
<div
ref={memoContentContainerRef}
className={cn(
"markdown-content relative w-full max-w-full wrap-break-word text-base leading-6",
"relative w-full max-w-full wrap-break-word text-base leading-6",
showCompactMode === "ALL" && `max-h-[${COMPACT_MODE_CONFIG.maxHeightVh}vh] overflow-hidden`,
contentClassName,
)}
@ -62,12 +64,38 @@ const MemoContent = (props: MemoContentProps) => {
}
return <span {...rest} />;
}) as React.ComponentType<React.ComponentProps<"span">>,
pre: CodeBlock,
a: ({ href, children, ...aProps }) => (
<a href={href} target="_blank" rel="noopener noreferrer" {...aProps}>
// Headings
h1: ({ children }) => <Heading level={1}>{children}</Heading>,
h2: ({ children }) => <Heading level={2}>{children}</Heading>,
h3: ({ children }) => <Heading level={3}>{children}</Heading>,
h4: ({ children }) => <Heading level={4}>{children}</Heading>,
h5: ({ children }) => <Heading level={5}>{children}</Heading>,
h6: ({ children }) => <Heading level={6}>{children}</Heading>,
// Block elements
p: ({ children }) => <Paragraph>{children}</Paragraph>,
blockquote: ({ children }) => <Blockquote>{children}</Blockquote>,
hr: () => <HorizontalRule />,
// Lists
ul: ({ children, ...props }) => <List {...props}>{children}</List>,
ol: ({ children, ...props }) => (
<List ordered {...props}>
{children}
</a>
</List>
),
li: ({ children, ...props }) => <ListItem {...props}>{children}</ListItem>,
// Inline elements
a: ({ children, ...props }) => <Link {...props}>{children}</Link>,
code: ({ children }) => <InlineCode>{children}</InlineCode>,
img: ({ ...props }) => <Image {...props} />,
// Code blocks
pre: CodeBlock,
// Tables
table: ({ children }) => <Table>{children}</Table>,
thead: ({ children }) => <TableHead>{children}</TableHead>,
tbody: ({ children }) => <TableBody>{children}</TableBody>,
tr: ({ children }) => <TableRow>{children}</TableRow>,
th: ({ children, ...props }) => <TableHeaderCell {...props}>{children}</TableHeaderCell>,
td: ({ children, ...props }) => <TableCell {...props}>{children}</TableCell>,
}}
>
{content}

View File

@ -0,0 +1,17 @@
import { cn } from "@/lib/utils";
import type { ReactMarkdownProps } from "./types";
interface BlockquoteProps extends React.BlockquoteHTMLAttributes<HTMLQuoteElement>, ReactMarkdownProps {
children: React.ReactNode;
}
/**
* Blockquote component with left border accent
*/
export const Blockquote = ({ children, className, node: _node, ...props }: BlockquoteProps) => {
return (
<blockquote className={cn("my-0 mb-2 border-l-4 border-border pl-3 text-muted-foreground", className)} {...props}>
{children}
</blockquote>
);
};

View File

@ -0,0 +1,30 @@
import { cn } from "@/lib/utils";
import type { ReactMarkdownProps } from "./types";
interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement>, ReactMarkdownProps {
level: 1 | 2 | 3 | 4 | 5 | 6;
children: React.ReactNode;
}
/**
* Heading component for h1-h6 elements
* Renders semantic heading levels with consistent styling
*/
export const Heading = ({ level, children, className, node: _node, ...props }: HeadingProps) => {
const Component = `h${level}` as const;
const levelClasses = {
1: "text-3xl font-bold border-b border-border pb-1",
2: "text-2xl border-b border-border pb-1",
3: "text-xl",
4: "text-base",
5: "text-sm",
6: "text-sm text-muted-foreground",
};
return (
<Component className={cn("mt-3 mb-2 font-semibold leading-tight", levelClasses[level], className)} {...props}>
{children}
</Component>
);
};

View File

@ -0,0 +1,11 @@
import { cn } from "@/lib/utils";
import type { ReactMarkdownProps } from "./types";
interface HorizontalRuleProps extends React.HTMLAttributes<HTMLHRElement>, ReactMarkdownProps {}
/**
* Horizontal rule separator
*/
export const HorizontalRule = ({ className, node: _node, ...props }: HorizontalRuleProps) => {
return <hr className={cn("my-2 h-0 border-0 border-b border-border", className)} {...props} />;
};

View File

@ -0,0 +1,12 @@
import { cn } from "@/lib/utils";
import type { ReactMarkdownProps } from "./types";
interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement>, ReactMarkdownProps {}
/**
* Image component for markdown images
* Responsive with rounded corners
*/
export const Image = ({ className, alt, node: _node, ...props }: ImageProps) => {
return <img className={cn("max-w-full h-auto rounded-lg my-2", className)} alt={alt} {...props} />;
};

View File

@ -0,0 +1,17 @@
import { cn } from "@/lib/utils";
import type { ReactMarkdownProps } from "./types";
interface InlineCodeProps extends React.HTMLAttributes<HTMLElement>, ReactMarkdownProps {
children: React.ReactNode;
}
/**
* Inline code component with background and monospace font
*/
export const InlineCode = ({ children, className, node: _node, ...props }: InlineCodeProps) => {
return (
<code className={cn("font-mono text-sm bg-muted px-1 py-0.5 rounded", className)} {...props}>
{children}
</code>
);
};

View File

@ -0,0 +1,24 @@
import { cn } from "@/lib/utils";
import type { ReactMarkdownProps } from "./types";
interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement>, ReactMarkdownProps {
children: React.ReactNode;
}
/**
* Link component for external links
* Opens in new tab with security attributes
*/
export const Link = ({ children, className, href, node: _node, ...props }: LinkProps) => {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className={cn("text-primary underline transition-opacity hover:opacity-80", className)}
{...props}
>
{children}
</a>
);
};

View File

@ -0,0 +1,66 @@
import { cn } from "@/lib/utils";
import type { ReactMarkdownProps } from "./types";
interface ListProps extends React.HTMLAttributes<HTMLUListElement | HTMLOListElement>, ReactMarkdownProps {
ordered?: boolean;
children: React.ReactNode;
}
/**
* List component for both regular and task lists (GFM)
* Detects task lists via the "contains-task-list" class added by remark-gfm
*/
export const List = ({ ordered, children, className, node: _node, ...domProps }: ListProps) => {
const Component = ordered ? "ol" : "ul";
const isTaskList = className?.includes("contains-task-list");
return (
<Component
className={cn(
"my-0 mb-2 list-outside",
isTaskList ? "pl-0 list-none" : cn("pl-6", ordered ? "list-decimal" : "list-disc"),
className,
)}
{...domProps}
>
{children}
</Component>
);
};
interface ListItemProps extends React.LiHTMLAttributes<HTMLLIElement>, ReactMarkdownProps {
children: React.ReactNode;
}
/**
* List item component for both regular and task list items
* Detects task items via the "task-list-item" class added by remark-gfm
* Applies specialized styling for task checkboxes
*/
export const ListItem = ({ children, className, node: _node, ...domProps }: ListItemProps) => {
const isTaskListItem = className?.includes("task-list-item");
if (isTaskListItem) {
return (
<li
className={cn(
"mt-0.5 leading-6 list-none",
// Task item styles: checkbox margins, inline paragraph, nested list indent
"[&>button]:mr-2 [&>button]:align-middle",
"[&>p]:inline [&>p]:m-0",
"[&>.contains-task-list]:pl-6",
className,
)}
{...domProps}
>
{children}
</li>
);
}
return (
<li className={cn("mt-0.5 leading-6", className)} {...domProps}>
{children}
</li>
);
};

View File

@ -0,0 +1,17 @@
import { cn } from "@/lib/utils";
import type { ReactMarkdownProps } from "./types";
interface ParagraphProps extends React.HTMLAttributes<HTMLParagraphElement>, ReactMarkdownProps {
children: React.ReactNode;
}
/**
* Paragraph component with compact spacing
*/
export const Paragraph = ({ children, className, node: _node, ...props }: ParagraphProps) => {
return (
<p className={cn("my-0 mb-2 leading-6", className)} {...props}>
{children}
</p>
);
};

View File

@ -0,0 +1,97 @@
# Markdown Components
Modern, type-safe React components for rendering markdown content via react-markdown.
## Architecture
### Component-Based Rendering
Following patterns from popular AI chat apps (ChatGPT, Claude, Perplexity), we use React components instead of CSS selectors for markdown rendering. This provides:
- **Type Safety**: Full TypeScript support with proper prop types
- **Maintainability**: Components are easier to test, modify, and understand
- **Performance**: No CSS specificity conflicts, cleaner DOM
- **Modularity**: Each element is independently styled and documented
### Type System
All components extend `ReactMarkdownProps` which includes the AST `node` prop passed by react-markdown. This is explicitly destructured as `node: _node` to:
1. Filter it from DOM props (avoids `node="[object Object]"` in HTML)
2. Keep it available for advanced use cases (e.g., detecting task lists)
3. Maintain type safety without `as any` casts
### GFM Task Lists
Task lists (from remark-gfm) are handled by:
- **Detection**: `contains-task-list` and `task-list-item` classes from remark-gfm
- **Styling**: Tailwind utilities with arbitrary variants for nested elements
- **Checkboxes**: Custom `TaskListItem` component with Radix UI checkbox
- **Interactivity**: Updates memo content via `toggleTaskAtIndex` utility
### Component Patterns
Each component follows this structure:
```tsx
import { cn } from "@/lib/utils";
import type { ReactMarkdownProps } from "./types";
interface ComponentProps extends React.HTMLAttributes<HTMLElement>, ReactMarkdownProps {
children?: React.ReactNode;
// component-specific props
}
/**
* JSDoc description
*/
export const Component = ({ children, className, node: _node, ...props }: ComponentProps) => {
return (
<element className={cn("base-classes", className)} {...props}>
{children}
</element>
);
};
```
## Components
| Component | Element | Purpose |
|-----------|---------|---------|
| `Heading` | h1-h6 | Semantic headings with level-based styling |
| `Paragraph` | p | Compact paragraphs with consistent spacing |
| `Link` | a | External links with security attributes |
| `List` | ul/ol | Regular and GFM task lists |
| `ListItem` | li | List items with task checkbox support |
| `Blockquote` | blockquote | Quotes with left border accent |
| `InlineCode` | code | Inline code with background |
| `Image` | img | Responsive images with rounded corners |
| `HorizontalRule` | hr | Section separators |
## Styling Approach
- **Tailwind CSS**: All styling uses Tailwind utilities
- **Design Tokens**: Colors use CSS variables (e.g., `--primary`, `--muted-foreground`)
- **Responsive**: Max-width constraints, responsive images
- **Accessibility**: Semantic HTML, proper ARIA attributes via Radix UI
## Integration
Components are mapped to HTML elements in `MemoContent/index.tsx`:
```tsx
<ReactMarkdown
components={{
h1: ({ children }) => <Heading level={1}>{children}</Heading>,
p: ({ children, ...props }) => <Paragraph {...props}>{children}</Paragraph>,
// ... more mappings
}}
>
{content}
</ReactMarkdown>
```
## Future Enhancements
- [ ] Syntax highlighting themes for code blocks
- [ ] Table sorting/filtering interactions
- [ ] Image lightbox/zoom functionality
- [ ] Collapsible sections for long content
- [ ] Copy button for code blocks

View File

@ -0,0 +1,8 @@
export { Blockquote } from "./Blockquote";
export { Heading } from "./Heading";
export { HorizontalRule } from "./HorizontalRule";
export { Image } from "./Image";
export { InlineCode } from "./InlineCode";
export { Link } from "./Link";
export { List, ListItem } from "./List";
export { Paragraph } from "./Paragraph";

View File

@ -0,0 +1,9 @@
import type { Element } from "hast";
/**
* Props passed by react-markdown to custom components
* Includes the AST node for advanced use cases
*/
export interface ReactMarkdownProps {
node?: Element;
}

View File

@ -15,343 +15,16 @@
}
/* ========================================
* Task List Styles
* Based on GitHub's implementation for proper nesting
* Embedded Content
* ======================================== */
/* Task list items - remove default list styling */
.markdown-content .task-list-item,
.prose .task-list-item {
list-style-type: none;
}
/* Task list checkboxes - use negative margin for proper alignment */
.markdown-content .task-list-item > input[type="checkbox"],
.prose .task-list-item > input[type="checkbox"] {
margin: 0 0.2em 0.25em -1.4em;
vertical-align: middle;
}
/* Paragraphs inside task items should not have extra margins */
.markdown-content .task-list-item > p,
.prose .task-list-item > p {
display: inline;
margin: 0;
}
/* Task list containers maintain standard list spacing */
.markdown-content .contains-task-list,
.prose .contains-task-list {
list-style: none;
padding-left: 0;
}
/* Nested task lists get proper indentation (standard list padding) */
.markdown-content .task-list-item .contains-task-list,
.prose .task-list-item .contains-task-list {
padding-left: 1.5em;
}
/* ========================================
* Markdown Content Styles
* Compact spacing optimized for memos/notes
*
* Key principles:
* 1. Block elements use 8px (0.5rem) bottom margin (compact)
* 2. First child has no top margin, last child has no bottom margin
* 3. Nested elements have minimal spacing
* 4. Inline elements have no vertical spacing
* ======================================== */
.markdown-content {
font-size: 1rem;
line-height: 1.5;
color: var(--foreground);
word-wrap: break-word;
}
/* ========================================
* First/Last Child Normalization
* Remove boundary spacing to prevent double margins
* ======================================== */
.markdown-content > :first-child {
margin-top: 0 !important;
}
.markdown-content > :last-child {
margin-bottom: 0 !important;
}
/* ========================================
* Block Elements
* Compact 8px bottom margin
* ======================================== */
.markdown-content p,
.markdown-content blockquote,
.markdown-content ul,
.markdown-content ol,
.markdown-content dl,
.markdown-content table,
.markdown-content pre,
.markdown-content hr {
margin-top: 0;
margin-bottom: 0.5rem;
}
/* ========================================
* Headings
* Compact spacing for visual separation
* ======================================== */
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
margin-top: 0.75rem;
margin-bottom: 0.5rem;
font-weight: 600;
line-height: 1.25;
}
.markdown-content h1 {
font-size: 2em;
font-weight: 700;
border-bottom: 1px solid var(--border);
padding-bottom: 0.25rem;
}
.markdown-content h2 {
font-size: 1.5em;
border-bottom: 1px solid var(--border);
padding-bottom: 0.25rem;
}
.markdown-content h3 {
font-size: 1.25em;
}
.markdown-content h4 {
font-size: 1em;
}
.markdown-content h5 {
font-size: 0.875em;
}
.markdown-content h6 {
font-size: 0.85em;
color: var(--muted-foreground);
}
/* ========================================
* Paragraphs
* ======================================== */
.markdown-content p {
line-height: 1.5;
}
/* ========================================
* Links
* ======================================== */
.markdown-content a {
color: var(--primary);
text-decoration: underline;
transition: opacity 150ms;
}
.markdown-content a:hover {
opacity: 0.8;
}
/* ========================================
* Lists
* ======================================== */
.markdown-content ul,
.markdown-content ol {
padding-left: 1.5em;
list-style-position: outside;
}
.markdown-content ul {
list-style-type: disc;
}
.markdown-content ol {
list-style-type: decimal;
}
.markdown-content li {
margin-top: 0.125rem;
line-height: 1.5;
}
.markdown-content li > p {
margin-bottom: 0.125rem;
}
/* Nested lists should have minimal spacing */
.markdown-content li > ul,
.markdown-content li > ol {
margin-top: 0.125rem;
margin-bottom: 0.125rem;
}
/* First and last items in lists */
.markdown-content li:first-child {
margin-top: 0;
}
.markdown-content li + li {
margin-top: 0.125rem;
}
/* ========================================
* Code (inline and blocks)
* ======================================== */
.markdown-content code {
font-family: var(--font-mono);
font-size: 0.875em;
background: var(--muted);
padding: 0.125rem 0.25rem;
border-radius: 0.25rem;
}
.markdown-content pre {
font-family: var(--font-mono);
font-size: 0.875rem;
background: var(--muted);
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.markdown-content pre code {
background: none;
padding: 0;
font-size: inherit;
border-radius: 0;
}
/* ========================================
* Blockquotes
* ======================================== */
.markdown-content blockquote {
padding: 0 0.75rem;
color: var(--muted-foreground);
border-left: 0.25rem solid var(--border);
}
.markdown-content blockquote > :first-child {
margin-top: 0;
}
.markdown-content blockquote > :last-child {
margin-bottom: 0;
}
/* ========================================
* Horizontal Rules
* ======================================== */
.markdown-content hr {
height: 0.25em;
padding: 0;
background: transparent;
border: 0;
border-bottom: 1px solid var(--border);
}
/* ========================================
* Tables
* ======================================== */
.markdown-content table {
display: block;
width: 100%;
width: max-content;
max-width: 100%;
overflow: auto;
border-spacing: 0;
border-collapse: collapse;
}
.markdown-content table th,
.markdown-content table td {
padding: 0.25rem 0.5rem;
border: 1px solid var(--border);
}
.markdown-content table th {
font-weight: 600;
background: var(--muted);
}
.markdown-content table tr {
background: transparent;
border-top: 1px solid var(--border);
}
.markdown-content table tr:nth-child(2n) {
background: var(--muted);
opacity: 0.5;
}
/* ========================================
* Images
* ======================================== */
.markdown-content img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
}
/* ========================================
* Embedded Content (iframes, videos)
* ======================================== */
.markdown-content iframe {
/* iframes (e.g., YouTube embeds, maps) */
iframe {
max-width: 100%;
border-radius: 0.5rem;
border: 1px solid var(--border);
}
/* ========================================
* Inline Elements
* No vertical spacing
* ======================================== */
.markdown-content strong {
font-weight: 600;
}
.markdown-content em {
font-style: italic;
}
.markdown-content code,
.markdown-content strong,
.markdown-content em,
.markdown-content a {
vertical-align: baseline;
}
/* Strikethrough (GFM) */
.markdown-content del {
text-decoration: line-through;
}
/* Leaflet Popup Overrides */
.leaflet-popup-content-wrapper {
border-radius: 0.5rem !important;