Files
boos e0a5f57793 feat(desktop): embed BoosAPI Desktop client as a subproject
Vendor the rebranded BoosAPI Desktop client (Tauri + Vue) into metapi as
a monorepo subdirectory under desktop/. Source only — node_modules,
dist, Cargo target and the upstream .git history are excluded.

desktop/ keeps its own independent Cargo workspace (desktop/src-tauri
with crates/*); it is not part of the backend boosapi crate, so a
`cargo check` at the metapi root still compiles only the backend. Build
the desktop app from desktop/src-tauri per desktop/README.md.

Note: app icons are still placeholder assets pending the final BoosAPI
logo. The updater signing keys live outside the repo and are not
included.
2026-06-22 11:32:57 +08:00

750 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# React Components & API
markstream-react provides the same powerful components as markstream-vue, but built for React. All components support React 18+ with full TypeScript support.
The root `markstream-react`, `markstream-react/next`, and `markstream-react/server` entrypoints all ship declaration files. Shared renderer and component types such as `NodeRendererProps`, `NodeRendererCodeBlockProps`, `NodeComponentProps`, `RenderContext`, `RenderNodeFn`, `CustomComponentMap`, `CodeBlockMonacoOptions`, `MarkdownCodeBlockNodeProps`, `ListItemNodeProps`, `HtmlPreviewFrameProps`, `TooltipProps`, `TooltipPlacement`, and `LinkNodeStyleProps` can be imported directly from the entrypoint you use.
## Main Component: MarkdownRender
The primary component for rendering markdown content in React.
### Props
`MarkdownRender` uses the `NodeRendererProps` interface from `markstream-react`.
#### Core props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `content` | `string` | - | Markdown content to render |
| `nodes` | `BaseNode[]` | - | Pre-parsed AST nodes (typically `ParsedNode[]` from the parser) |
| `customId` | `string` | - | Identifier for scoping custom components and CSS |
| `final` | `boolean` | `false` | Marks end-of-stream; stops emitting streaming `loading` nodes |
| `parseOptions` | `ParseOptions` | - | Parser options and token hooks (only when `content` is provided) |
| `customHtmlTags` | `readonly string[]` | - | HTML-like tags emitted as custom nodes (e.g. `thinking`) |
| `customMarkdownIt` | `(md: MarkdownIt) => MarkdownIt` | - | Customize the internal MarkdownIt instance |
| `debugPerformance` | `boolean` | `false` | Log parse/render timing and virtualization stats (dev only) |
| `isDark` | `boolean` | `false` | Theme flag forwarded to heavy nodes; adds `.dark` to the root container |
| `indexKey` | `number \| string` | - | Key prefix when rendering multiple instances in lists |
| `typewriter` | `boolean` | `true` | Enable the non-code-node enter transition |
| `showTooltips` | `boolean` | `true` | Global tooltip switch for `LinkNode` and code block nodes |
#### Streaming & heavy-node toggles
| Prop | Default | Description |
|------|---------|-------------|
| `renderCodeBlocksAsPre` | `false` | Render code blocks as `<pre><code>` (Mermaid/D2/Infographic blocks will also fall back) |
| `codeBlockStream` | `true` | Stream code block updates as content arrives |
| `viewportPriority` | `true` | Defer heavy work (Monaco/Mermaid/D2/KaTeX) until near viewport |
| `deferNodesUntilVisible` | `true` | Render heavy nodes as placeholders until visible (non-virtualized mode only) |
#### Performance (virtualization & batching)
| Prop | Default | Description |
|------|---------|-------------|
| `maxLiveNodes` | `320` | Max fully rendered nodes kept in DOM (set `0` to disable virtualization) |
| `liveNodeBuffer` | `60` | Overscan buffer around the focus range |
| `batchRendering` | `true` | Incremental batch rendering when virtualization is disabled |
| `initialRenderBatchSize` | `40` | Nodes rendered immediately before batching starts |
| `renderBatchSize` | `80` | Nodes rendered per batch tick |
| `renderBatchDelay` | `16` | Extra delay (ms) before each batch after rAF |
| `renderBatchBudgetMs` | `6` | Time budget (ms) before adaptive batch sizes shrink |
| `renderBatchIdleTimeoutMs` | `120` | Timeout (ms) for `requestIdleCallback` slices |
#### Global code block options
| Prop | Type | Description |
|------|------|-------------|
| `codeBlockDarkTheme` | `CodeBlockMonacoTheme` | Monaco dark theme object forwarded to every `CodeBlockNode` |
| `codeBlockLightTheme` | `CodeBlockMonacoTheme` | Monaco light theme object forwarded to every `CodeBlockNode` |
| `codeBlockMonacoOptions` | `CodeBlockMonacoOptions` | Options forwarded to `stream-monaco`, including diff hover-action settings like `diffHunkActionsOnHover`, `diffHunkHoverHideDelayMs`, and `onDiffHunkAction` |
| `codeBlockMinWidth` | `string \| number` | Min width forwarded to `CodeBlockNode` |
| `codeBlockMaxWidth` | `string \| number` | Max width forwarded to `CodeBlockNode` |
| `codeBlockProps` | `NodeRendererCodeBlockProps` | Extra props forwarded to every code-block renderer (`CodeBlockNode` / `MarkdownCodeBlockNode`) |
| `mermaidProps` | `Partial<Omit<MermaidBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to Mermaid fences and custom `mermaid` renderers |
| `d2Props` | `Partial<Omit<D2BlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to D2 fences and custom `d2` renderers |
| `infographicProps` | `Partial<Omit<InfographicBlockNodeProps, 'node' \| 'loading' \| 'isDark'>>` | Extra props forwarded to infographic fences and custom `infographic` renderers |
| `themes` | `CodeBlockMonacoTheme[]` | Theme list forwarded to `stream-monaco` |
#### Heavy renderer prop forwarding
`NodeRenderer` can tune heavy blocks directly:
```tsx
<MarkdownRender
content={markdown}
viewportPriority
mermaidProps={{
showHeader: false,
renderDebounceMs: 180,
previewPollDelayMs: 500,
}}
d2Props={{ progressiveIntervalMs: 500 }}
infographicProps={{ showHeader: false }}
/>
```
Streaming notes:
- Keep `viewportPriority` enabled to prevent offscreen Mermaid / Monaco / D2 work from running while text is still streaming.
- For high-frequency SSE, prefer passing `nodes` instead of reparsing the full `content` string every chunk.
- Common Mermaid tuning keys: `renderDebounceMs`, `contentStableDelayMs`, `previewPollDelayMs`, `previewPollMaxDelayMs`, `previewPollMaxAttempts`.
`NodeRendererCodeBlockProps` follows the public `CodeBlockNode` prop surface except for `node`, so you get completion for flags like `showHeader`, `showFontSizeButtons`, and `showTooltips` without dropping to `any`.
```tsx
import type { NodeRendererCodeBlockProps } from 'markstream-react'
const codeBlockProps: NodeRendererCodeBlockProps = {
showHeader: false,
showFontSizeButtons: false,
showTooltips: false,
}
```
#### Events
| Prop | Type | Description |
|------|------|-------------|
| `onCopy` | `(code: string) => void` | Fired when code blocks emit copy events |
| `onHandleArtifactClick` | `(payload: any) => void` | Fired on artifact/preview clicks |
| `onClick` | `(event: React.MouseEvent<HTMLDivElement>) => void` | Click handler for the renderer root |
| `onMouseOver` | `(event: React.MouseEvent<HTMLElement>) => void` | Mouseover handler for the renderer root |
| `onMouseOut` | `(event: React.MouseEvent<HTMLElement>) => void` | Mouseout handler for the renderer root |
### Usage
```tsx
import MarkdownRender from 'markstream-react'
function App() {
const markdown = `# Hello React!
This is markstream-react.`
return (
<MarkdownRender
customId="docs"
content={markdown}
maxLiveNodes={150}
/>
)
}
```
## Code Block Components
### MarkdownCodeBlockNode
Lightweight code highlighting using Shiki.
```tsx
import { MarkdownCodeBlockNode } from 'markstream-react'
function CodeBlock() {
const codeNode = {
type: 'code_block',
language: 'javascript',
code: 'const hello = "world"',
raw: 'const hello = "world"'
}
const handleCopy = () => {
alert('Code copied!')
}
return (
<div className="markstream-react">
<MarkdownCodeBlockNode
node={codeNode}
showCopyButton={true}
showCollapseButton={false}
onCopy={handleCopy}
/>
</div>
)
}
```
### CodeBlockNode
Feature-rich Monaco-powered code blocks.
```tsx
import { CodeBlockNode } from 'markstream-react'
function MonacoCodeBlock() {
const codeNode = {
type: 'code_block',
language: 'typescript',
code: 'const greeting: string = "Hello"',
raw: 'const greeting: string = "Hello"'
}
const handleCopy = (code: string) => {
console.log('Code copied:', code)
}
const handlePreviewCode = (artifact: any) => {
console.log('Preview code:', artifact)
}
return (
<div className="markstream-react">
<CodeBlockNode
node={codeNode}
monacoOptions={{ fontSize: 14, theme: 'vs-dark' }}
stream={true}
showCollapseButton={false}
onCopy={handleCopy}
onPreviewCode={handlePreviewCode}
/>
</div>
)
}
```
## Math Components
### MathBlockNode
Renders block-level math formulas with KaTeX.
```tsx
import { MathBlockNode } from 'markstream-react'
function MathBlock() {
const mathNode = {
type: 'math_block',
content: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}',
raw: '\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}'
}
return (
<div className="markstream-react">
<MathBlockNode node={mathNode} />
</div>
)
}
```
### MathInlineNode
Renders inline math formulas.
```tsx
import { MathInlineNode } from 'markstream-react'
function MathInline() {
const inlineMathNode = {
type: 'math_inline',
content: 'E = mc^2',
raw: 'E = mc^2'
}
return (
<div className="markstream-react">
<p>
The formula is:
{' '}
<MathInlineNode node={inlineMathNode} />
</p>
</div>
)
}
```
## Mermaid Diagrams
### MermaidBlockNode
Progressive Mermaid diagram rendering.
#### Quick reference
- **Tooltip control**: `showTooltips` defaults to `true`; set `false` to disable header action tooltips.
```tsx
import { MermaidBlockNode } from 'markstream-react'
function MermaidDiagram() {
const mermaidNode = {
type: 'code_block',
language: 'mermaid',
code: `graph TD
A[Start] --> B{Is it working?}
B -->|Yes| C[Great!]`,
raw: ''
}
const handleExport = (ev: any) => {
console.log('Mermaid SVG:', ev.svgString)
}
return (
<div className="markstream-react">
<MermaidBlockNode
node={mermaidNode}
isStrict={true}
onExport={handleExport}
showTooltips={true}
/>
</div>
)
}
```
Event notes:
- `onCopy(code: string)` receives the source text directly (no `MermaidBlockEvent` wrapper in React).
- `onExport`, `onOpenModal`, `onToggleMode` receive `MermaidBlockEvent` and support `ev.preventDefault()` to stop the default behavior.
- `onToggleMode` signature: `(target: 'source' | 'preview', ev)`.
- For renderer-wide usage, prefer passing these knobs through `mermaidProps` on `MarkdownRender` / `NodeRenderer`.
## D2 Diagrams
### D2BlockNode
Progressive D2 diagram rendering with source fallback.
```tsx
import { D2BlockNode } from 'markstream-react'
function D2Diagram() {
const d2Node = {
type: 'code_block',
language: 'd2',
code: `direction: right
Client -> API: request
API -> DB: query`,
raw: ''
}
return (
<div className="markstream-react">
<D2BlockNode
node={d2Node}
progressiveIntervalMs={600}
/>
</div>
)
}
```
## Other Node Components
### HeadingNode
```tsx
import { HeadingNode } from 'markstream-react'
function CustomHeading() {
const headingNode = {
type: 'heading',
level: 1
}
return <HeadingNode node={headingNode}>Hello World</HeadingNode>
}
```
### ParagraphNode
```tsx
import { ParagraphNode } from 'markstream-react'
function CustomParagraph() {
const paragraphNode = {
type: 'paragraph'
}
return (
<ParagraphNode node={paragraphNode}>
This is a
{' '}
<strong>bold</strong>
{' '}
word.
</ParagraphNode>
)
}
```
### ListNode
```tsx
import { ListNode, renderNode } from 'markstream-react'
function CustomList() {
const listNode = {
type: 'list',
ordered: false,
items: [
{
type: 'list_item',
children: [
{ type: 'paragraph', children: [{ type: 'text', content: 'Item 1' }] }
]
},
{
type: 'list_item',
children: [
{ type: 'paragraph', children: [{ type: 'text', content: 'Item 2' }] }
]
}
]
}
const ctx = { events: {} }
return <ListNode node={listNode} ctx={ctx} renderNode={renderNode} />
}
```
### LinkNode
```tsx
import { LinkNode } from 'markstream-react'
function CustomLink() {
const linkNode = {
type: 'link',
href: 'https://example.com',
title: 'Example',
text: 'Click me'
}
return (
<LinkNode
node={linkNode}
color="#e11d48"
underlineHeight={3}
showTooltip={true}
/>
)
}
```
### ImageNode
```tsx
import { ImageNode } from 'markstream-react'
function CustomImage() {
const imageNode = {
type: 'image',
src: 'https://example.com/image.jpg',
alt: 'Example image',
title: 'Example',
raw: '![Example image](https://example.com/image.jpg)'
}
const handleClick = () => {
console.log('Image clicked!')
}
const handleLoad = () => {
console.log('Image loaded!')
}
return (
<ImageNode
node={imageNode}
onClick={handleClick}
onLoad={handleLoad}
/>
)
}
```
## Utility Functions
### getMarkdown
Get a configured markdown-it instance.
```tsx
import { getMarkdown } from 'stream-markdown-parser'
const md = getMarkdown('my-msg-id', {
html: true,
linkify: true,
typographer: true
})
const tokens = md.parse('# Hello World')
```
### parseMarkdownToStructure
Parse markdown string to AST structure.
```tsx
import { getMarkdown, parseMarkdownToStructure } from 'stream-markdown-parser'
const md = getMarkdown()
const nodes = parseMarkdownToStructure('# Title\n\nContent here...', md)
// Use with MarkdownRender
// <MarkdownRender nodes={nodes} />
```
### Optional workers (Mermaid / KaTeX)
Mermaid/KaTeX auto-load when installed. If you want off‑thread parsing/rendering, inject workers:
```tsx
import { setKaTeXWorker, setMermaidWorker } from 'markstream-react'
import KatexWorker from 'markstream-react/workers/katexRenderer.worker?worker'
import MermaidWorker from 'markstream-react/workers/mermaidParser.worker?worker'
setMermaidWorker(new MermaidWorker())
setKaTeXWorker(new KatexWorker())
```
## Custom Component API
### Props Interface
All custom node components receive these props:
```tsx
import type { CustomComponentMap, NodeComponentProps, RenderContext, RenderNodeFn } from 'markstream-react'
interface NodeComponentProps<TNode = unknown> {
node: TNode // The parsed node data
ctx?: RenderContext // Renderer context (themes, events, flags)
renderNode?: RenderNodeFn // Helper to render child nodes
indexKey?: React.Key // Unique key for the node
customId?: string // Custom ID for scoping
isDark?: boolean
typewriter?: boolean
children?: React.ReactNode
}
```
`CustomComponentMap` is the typed mapping shape accepted by `setCustomComponents(...)`.
### Example Custom Component
```tsx
import React from 'react'
interface CustomParagraphProps {
node: {
type: string
children: Array<{
type: string
content?: string
children?: any[]
}>
}
indexKey?: number | string
customId?: string
}
function CustomParagraph({ node, indexKey, customId }: CustomParagraphProps) {
return (
<p
className={`custom-paragraph custom-paragraph-${indexKey}`}
data-custom-id={customId}
data-node-type={node.type}
>
{node.children.map((child, i) => (
<span key={i}>{child.content || ''}</span>
))}
</p>
)
}
// Usage
function App() {
const paragraphNode = {
type: 'paragraph',
children: [
{ type: 'text', content: 'Custom paragraph content' }
]
}
return <CustomParagraph node={paragraphNode} indexKey={0} customId="docs" />
}
```
## Context + Custom Components
You can use React Context inside custom node components, while still registering them via `setCustomComponents`:
```tsx
import MarkdownRender, { setCustomComponents } from 'markstream-react'
import React, { createContext, useContext } from 'react'
const ThemeContext = createContext<'light' | 'dark'>('light')
function CustomHeading({ node, customId }: any) {
const theme = useContext(ThemeContext)
const level = node.level || 1
const Tag = `h${level}` as keyof JSX.IntrinsicElements
return (
<Tag className={`custom-heading ${theme}`} data-custom-id={customId}>
{node.children?.map((child: any, i: number) => (
<span key={i}>{child.content || ''}</span>
))}
</Tag>
)
}
setCustomComponents('docs', { heading: CustomHeading })
function App() {
const markdown = `# Custom Heading
This uses a custom heading component.
`
return (
<ThemeContext.Provider value="dark">
<MarkdownRender customId="docs" content={markdown} />
</ThemeContext.Provider>
)
}
```
## Streaming Support
markstream-react supports streaming markdown content:
```tsx
import MarkdownRender from 'markstream-react'
import { useEffect, useState } from 'react'
function StreamingDemo() {
const [content, setContent] = useState('')
const fullContent = `# Streaming Demo
This content streams in **small chunks**.
`
useEffect(() => {
let i = 0
const interval = setInterval(() => {
if (i < fullContent.length) {
setContent(prev => prev + fullContent[i])
i++
}
else {
clearInterval(interval)
}
}, 30)
return () => clearInterval(interval)
}, [])
return <MarkdownRender content={content} />
}
```
## TypeScript Support
markstream-react includes full TypeScript definitions:
```tsx
import type { NodeComponentProps, NodeRendererProps } from 'markstream-react'
import type { ParsedNode } from 'stream-markdown-parser'
import MarkdownRender from 'markstream-react'
function App() {
const markdown = '# Hello TypeScript!'
const nodes: ParsedNode[] = []
return <MarkdownRender content={markdown} nodes={nodes} />
}
```
Code block prop interfaces (`CodeBlockNodeProps`, `MermaidBlockNodeProps`, `D2BlockNodeProps`, `InfographicBlockNodeProps`, `PreCodeNodeProps`) all use `node: CodeBlockNode` from `stream-markdown-parser` (use `language: 'mermaid'` / `language: 'd2'` / `language: 'd2lang'` / `language: 'infographic'` when targeting specialized renderers).
## Next.js Best Practices
Use the dedicated Next SSR entrypoints instead of `mounted` guards or `ssr: false`.
```tsx
import MarkdownRender from 'markstream-react/next'
export default function MarkdownPage() {
return <MarkdownRender content="# Hello Next.js!" final />
}
```
For a pure server render path:
```tsx
import MarkdownRender from 'markstream-react/server'
export default function MarkdownPage() {
return <MarkdownRender content="# Hello!" final />
}
```
See [React Next SSR](/guide/react-next-ssr) for the full entrypoint model.
## Hooks Integration
You can easily integrate with React hooks:
```tsx
import type { ChangeEvent } from 'react'
import MarkdownRender from 'markstream-react'
import { useCallback, useMemo, useState } from 'react'
function MarkdownEditor() {
const [content, setContent] = useState('# Edit me!')
const [theme, setTheme] = useState('light')
const memoizedContent = useMemo(() => content, [content])
const handleChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
setContent(e.target.value)
}, [])
return (
<div>
<textarea value={content} onChange={handleChange} />
<MarkdownRender
content={memoizedContent}
customId={`editor-${theme}`}
/>
</div>
)
}
```
## Error Handling
```tsx
import MarkdownRender from 'markstream-react'
import { useState } from 'react'
function SafeMarkdown({ content }: { content: string }) {
const [error, setError] = useState<Error | null>(null)
if (error) {
return (
<div>
Error rendering markdown:
{error.message}
</div>
)
}
try {
return <MarkdownRender content={content} />
}
catch (err) {
setError(err as Error)
return null
}
}
```
## Next Steps
- See [React Quick Start](/guide/react-quick-start) for setup examples
- Explore [Vue 3 Components](/guide/components) for more component examples (API is similar)
- Check [Usage & API](/guide/usage) for advanced patterns