diff --git a/web/src/components/MemoContent/CodeBlock.tsx b/web/src/components/MemoContent/CodeBlock.tsx index 86bb4a1ef..89c4ead63 100644 --- a/web/src/components/MemoContent/CodeBlock.tsx +++ b/web/src/components/MemoContent/CodeBlock.tsx @@ -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 ( -
-      
- {language} +
+      {/* Header with language label and copy button */}
+      
+ {language || "text"}
-
- + + {/* Code content */} +
+
); diff --git a/web/src/components/MemoContent/Table.tsx b/web/src/components/MemoContent/Table.tsx new file mode 100644 index 000000000..47f260a1a --- /dev/null +++ b/web/src/components/MemoContent/Table.tsx @@ -0,0 +1,83 @@ +import { cn } from "@/lib/utils"; +import type { ReactMarkdownProps } from "./markdown/types"; + +interface TableProps extends React.HTMLAttributes, ReactMarkdownProps { + children: React.ReactNode; +} + +export const Table = ({ children, className, node: _node, ...props }: TableProps) => { + return ( +
+ + {children} +
+
+ ); +}; + +interface TableHeadProps extends React.HTMLAttributes, ReactMarkdownProps { + children: React.ReactNode; +} + +export const TableHead = ({ children, className, node: _node, ...props }: TableHeadProps) => { + return ( + + {children} + + ); +}; + +interface TableBodyProps extends React.HTMLAttributes, ReactMarkdownProps { + children: React.ReactNode; +} + +export const TableBody = ({ children, className, node: _node, ...props }: TableBodyProps) => { + return ( + + {children} + + ); +}; + +interface TableRowProps extends React.HTMLAttributes, ReactMarkdownProps { + children: React.ReactNode; +} + +export const TableRow = ({ children, className, node: _node, ...props }: TableRowProps) => { + return ( + + {children} + + ); +}; + +interface TableHeaderCellProps extends React.ThHTMLAttributes, ReactMarkdownProps { + children: React.ReactNode; +} + +export const TableHeaderCell = ({ children, className, node: _node, ...props }: TableHeaderCellProps) => { + return ( + + {children} + + ); +}; + +interface TableCellProps extends React.TdHTMLAttributes, ReactMarkdownProps { + children: React.ReactNode; +} + +export const TableCell = ({ children, className, node: _node, ...props }: TableCellProps) => { + return ( + + {children} + + ); +}; diff --git a/web/src/components/MemoContent/TaskListItem.tsx b/web/src/components/MemoContent/TaskListItem.tsx index 07f06764a..9f5d3c646 100644 --- a/web/src/components/MemoContent/TaskListItem.tsx +++ b/web/src/components/MemoContent/TaskListItem.tsx @@ -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 { - node?: Element; // AST node from react-markdown +interface TaskListItemProps extends React.InputHTMLAttributes, ReactMarkdownProps { checked?: boolean; } -export const TaskListItem: React.FC = ({ checked, ...props }) => { +export const TaskListItem: React.FC = ({ checked, node: _node, ...props }) => { const { memo } = useMemoViewContext(); const { readonly } = useMemoViewDerived(); const checkboxRef = useRef(null); @@ -35,14 +34,19 @@ export const TaskListItem: React.FC = ({ 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; diff --git a/web/src/components/MemoContent/index.tsx b/web/src/components/MemoContent/index.tsx index 8342f6582..b0f11e3c8 100644 --- a/web/src/components/MemoContent/index.tsx +++ b/web/src/components/MemoContent/index.tsx @@ -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) => {
{ } return ; }) as React.ComponentType>, - pre: CodeBlock, - a: ({ href, children, ...aProps }) => ( - + // Headings + h1: ({ children }) => {children}, + h2: ({ children }) => {children}, + h3: ({ children }) => {children}, + h4: ({ children }) => {children}, + h5: ({ children }) => {children}, + h6: ({ children }) => {children}, + // Block elements + p: ({ children }) => {children}, + blockquote: ({ children }) =>
{children}
, + hr: () => , + // Lists + ul: ({ children, ...props }) => {children}, + ol: ({ children, ...props }) => ( + {children} -
+ ), + li: ({ children, ...props }) => {children}, + // Inline elements + a: ({ children, ...props }) => {children}, + code: ({ children }) => {children}, + img: ({ ...props }) => , + // Code blocks + pre: CodeBlock, + // Tables + table: ({ children }) => {children}
, + thead: ({ children }) => {children}, + tbody: ({ children }) => {children}, + tr: ({ children }) => {children}, + th: ({ children, ...props }) => {children}, + td: ({ children, ...props }) => {children}, }} > {content} diff --git a/web/src/components/MemoContent/markdown/Blockquote.tsx b/web/src/components/MemoContent/markdown/Blockquote.tsx new file mode 100644 index 000000000..914505729 --- /dev/null +++ b/web/src/components/MemoContent/markdown/Blockquote.tsx @@ -0,0 +1,17 @@ +import { cn } from "@/lib/utils"; +import type { ReactMarkdownProps } from "./types"; + +interface BlockquoteProps extends React.BlockquoteHTMLAttributes, ReactMarkdownProps { + children: React.ReactNode; +} + +/** + * Blockquote component with left border accent + */ +export const Blockquote = ({ children, className, node: _node, ...props }: BlockquoteProps) => { + return ( +
+ {children} +
+ ); +}; diff --git a/web/src/components/MemoContent/markdown/Heading.tsx b/web/src/components/MemoContent/markdown/Heading.tsx new file mode 100644 index 000000000..c77b5a481 --- /dev/null +++ b/web/src/components/MemoContent/markdown/Heading.tsx @@ -0,0 +1,30 @@ +import { cn } from "@/lib/utils"; +import type { ReactMarkdownProps } from "./types"; + +interface HeadingProps extends React.HTMLAttributes, 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 ( + + {children} + + ); +}; diff --git a/web/src/components/MemoContent/markdown/HorizontalRule.tsx b/web/src/components/MemoContent/markdown/HorizontalRule.tsx new file mode 100644 index 000000000..dc798b778 --- /dev/null +++ b/web/src/components/MemoContent/markdown/HorizontalRule.tsx @@ -0,0 +1,11 @@ +import { cn } from "@/lib/utils"; +import type { ReactMarkdownProps } from "./types"; + +interface HorizontalRuleProps extends React.HTMLAttributes, ReactMarkdownProps {} + +/** + * Horizontal rule separator + */ +export const HorizontalRule = ({ className, node: _node, ...props }: HorizontalRuleProps) => { + return
; +}; diff --git a/web/src/components/MemoContent/markdown/Image.tsx b/web/src/components/MemoContent/markdown/Image.tsx new file mode 100644 index 000000000..05def40f7 --- /dev/null +++ b/web/src/components/MemoContent/markdown/Image.tsx @@ -0,0 +1,12 @@ +import { cn } from "@/lib/utils"; +import type { ReactMarkdownProps } from "./types"; + +interface ImageProps extends React.ImgHTMLAttributes, ReactMarkdownProps {} + +/** + * Image component for markdown images + * Responsive with rounded corners + */ +export const Image = ({ className, alt, node: _node, ...props }: ImageProps) => { + return {alt}; +}; diff --git a/web/src/components/MemoContent/markdown/InlineCode.tsx b/web/src/components/MemoContent/markdown/InlineCode.tsx new file mode 100644 index 000000000..e01371b23 --- /dev/null +++ b/web/src/components/MemoContent/markdown/InlineCode.tsx @@ -0,0 +1,17 @@ +import { cn } from "@/lib/utils"; +import type { ReactMarkdownProps } from "./types"; + +interface InlineCodeProps extends React.HTMLAttributes, ReactMarkdownProps { + children: React.ReactNode; +} + +/** + * Inline code component with background and monospace font + */ +export const InlineCode = ({ children, className, node: _node, ...props }: InlineCodeProps) => { + return ( + + {children} + + ); +}; diff --git a/web/src/components/MemoContent/markdown/Link.tsx b/web/src/components/MemoContent/markdown/Link.tsx new file mode 100644 index 000000000..7414dfdd8 --- /dev/null +++ b/web/src/components/MemoContent/markdown/Link.tsx @@ -0,0 +1,24 @@ +import { cn } from "@/lib/utils"; +import type { ReactMarkdownProps } from "./types"; + +interface LinkProps extends React.AnchorHTMLAttributes, 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 ( + + {children} + + ); +}; diff --git a/web/src/components/MemoContent/markdown/List.tsx b/web/src/components/MemoContent/markdown/List.tsx new file mode 100644 index 000000000..76a0c5012 --- /dev/null +++ b/web/src/components/MemoContent/markdown/List.tsx @@ -0,0 +1,66 @@ +import { cn } from "@/lib/utils"; +import type { ReactMarkdownProps } from "./types"; + +interface ListProps extends React.HTMLAttributes, 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 ( + + {children} + + ); +}; + +interface ListItemProps extends React.LiHTMLAttributes, 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 ( +
  • button]:mr-2 [&>button]:align-middle", + "[&>p]:inline [&>p]:m-0", + "[&>.contains-task-list]:pl-6", + className, + )} + {...domProps} + > + {children} +
  • + ); + } + + return ( +
  • + {children} +
  • + ); +}; diff --git a/web/src/components/MemoContent/markdown/Paragraph.tsx b/web/src/components/MemoContent/markdown/Paragraph.tsx new file mode 100644 index 000000000..ecf5e67e6 --- /dev/null +++ b/web/src/components/MemoContent/markdown/Paragraph.tsx @@ -0,0 +1,17 @@ +import { cn } from "@/lib/utils"; +import type { ReactMarkdownProps } from "./types"; + +interface ParagraphProps extends React.HTMLAttributes, ReactMarkdownProps { + children: React.ReactNode; +} + +/** + * Paragraph component with compact spacing + */ +export const Paragraph = ({ children, className, node: _node, ...props }: ParagraphProps) => { + return ( +

    + {children} +

    + ); +}; diff --git a/web/src/components/MemoContent/markdown/README.md b/web/src/components/MemoContent/markdown/README.md new file mode 100644 index 000000000..a6ba08cc7 --- /dev/null +++ b/web/src/components/MemoContent/markdown/README.md @@ -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, ReactMarkdownProps { + children?: React.ReactNode; + // component-specific props +} + +/** + * JSDoc description + */ +export const Component = ({ children, className, node: _node, ...props }: ComponentProps) => { + return ( + + {children} + + ); +}; +``` + +## 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 + {children}, + p: ({ children, ...props }) => {children}, + // ... more mappings + }} +> + {content} + +``` + +## 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 diff --git a/web/src/components/MemoContent/markdown/index.ts b/web/src/components/MemoContent/markdown/index.ts new file mode 100644 index 000000000..e395d51eb --- /dev/null +++ b/web/src/components/MemoContent/markdown/index.ts @@ -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"; diff --git a/web/src/components/MemoContent/markdown/types.ts b/web/src/components/MemoContent/markdown/types.ts new file mode 100644 index 000000000..b50d4fcc3 --- /dev/null +++ b/web/src/components/MemoContent/markdown/types.ts @@ -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; +} diff --git a/web/src/index.css b/web/src/index.css index 9f68b0ec5..680b91673 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -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;