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.
This commit is contained in:
boos
2026-06-22 11:32:57 +08:00
parent d9c78ecc73
commit e0a5f57793
2305 changed files with 449355 additions and 0 deletions
@@ -0,0 +1,749 @@
# 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