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,14 @@
import * as nextEntry from 'markstream-react/next'
import * as serverEntry from 'markstream-react/server'
import { SsrLabPage } from '../../src/ssr-lab-page'
export default function AppSsrLabPage() {
return (
<SsrLabPage
version="next15"
router="app"
nextEntry={nextEntry}
serverEntry={serverEntry}
/>
)
}
@@ -0,0 +1,13 @@
import '../styles/globals.css'
export const metadata = {
title: 'Next 15 SSR Lab',
}
export default function RootLayout({ children }: { children: any }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
@@ -0,0 +1,16 @@
import Link from 'next/link'
export default function HomePage() {
return (
<main className="home-shell">
<h1>Next 15 Playground</h1>
<p>Use the dedicated SSR lab routes for formal acceptance.</p>
<nav>
<ul>
<li><Link href="/app-ssr-lab">/app-ssr-lab</Link></li>
<li><Link href="/pages-ssr-lab">/pages-ssr-lab</Link></li>
</ul>
</nav>
</main>
)
}
@@ -0,0 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,26 @@
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const rootDir = fileURLToPath(new URL('.', import.meta.url))
const reactAliases = {
'react': resolve(rootDir, 'node_modules/react'),
'react-dom': resolve(rootDir, 'node_modules/react-dom'),
'react/jsx-runtime': resolve(rootDir, 'node_modules/react/jsx-runtime.js'),
'react/jsx-dev-runtime': resolve(rootDir, 'node_modules/react/jsx-dev-runtime.js'),
}
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['markstream-react', 'stream-markdown-parser'],
webpack(config, { isServer }) {
if (!isServer) {
config.resolve.alias = {
...config.resolve.alias,
...reactAliases,
}
}
return config
},
}
export default nextConfig
@@ -0,0 +1,28 @@
{
"name": "playground-next15",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@antv/infographic": "^0.2.16",
"@terrastruct/d2": "^0.1.33",
"katex": "^0.16.44",
"markstream-react": "workspace:*",
"mermaid": "^11.13.0",
"monaco-editor": "^0.52.2",
"next": "15.5.14",
"react": "19.2.4",
"react-dom": "19.2.4",
"stream-monaco": "latest"
},
"devDependencies": {
"@types/node": "^24.12.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"typescript": "^5.9.3"
}
}
@@ -0,0 +1,6 @@
import type { AppProps } from 'next/app'
import '../styles/globals.css'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
@@ -0,0 +1,206 @@
import * as nextEntry from 'markstream-react/next'
import * as serverEntry from 'markstream-react/server'
import React from 'react'
import {
createMatrixCases,
FALLBACK_DEMOS,
SERVER_BASIC_NODES,
SERVER_CUSTOM_ALT_ID,
SERVER_CUSTOM_ALT_NODES,
SERVER_CUSTOM_ID,
SERVER_CUSTOM_NODES,
} from '../src/ssr-lab-data'
import { SsrLabPage } from '../src/ssr-lab-page'
function readAttr(node: any, name: string) {
if (!node?.attrs)
return undefined
if (Array.isArray(node.attrs))
return node.attrs.find((item: [string, string | null]) => item[0] === name)?.[1] ?? undefined
return node.attrs[name]
}
function renderCustomChildren(props: any) {
if (!Array.isArray(props.node?.children) || !props.renderNode || !props.ctx)
return null
return props.node.children.map((child: any, idx: number) => (
<React.Fragment key={`${String(props.indexKey ?? 'custom')}-${idx}`}>
{props.renderNode(child, `${String(props.indexKey ?? 'custom')}-${idx}`, props.ctx)}
</React.Fragment>
))
}
function ServerParagraphOverride({ node }: any) {
return (
<div data-ssr-status="server-paragraph-override">
{node.raw}
</div>
)
}
function ServerInsightNode(props: any) {
return (
<section data-ssr-status="server-custom-node">
<strong>{props.node.label}</strong>
<div data-ssr-status="server-custom-node-children">
{renderCustomChildren(props)}
</div>
</section>
)
}
function ServerThinkingTag({ node }: any) {
return (
<aside
data-ssr-status="server-thinking-tag"
data-ssr-tone={readAttr(node, 'data-tone')}
data-ssr-scope={readAttr(node, 'data-scope')}
>
{node.content}
</aside>
)
}
function ServerParagraphAlt({ node }: any) {
return (
<div data-ssr-status="server-paragraph-alt">
{node.raw}
</div>
)
}
function ServerInsightAlt(props: any) {
return (
<section data-ssr-status="server-custom-node-alt">
<strong>{props.node.label}</strong>
<div data-ssr-status="server-custom-node-alt-children">
{renderCustomChildren(props)}
</div>
</section>
)
}
function ServerThinkingTagAlt({ node }: any) {
return (
<aside
data-ssr-status="server-thinking-tag-alt"
data-ssr-scope={readAttr(node, 'data-scope')}
>
{node.content}
</aside>
)
}
function renderServerFallbackHtml() {
return (
<>
<div data-ssr-demo="mermaid" className="ssr-card">
<serverEntry.MermaidBlockNode node={FALLBACK_DEMOS.mermaid as any} />
</div>
<div data-ssr-demo="d2" className="ssr-card">
<serverEntry.D2BlockNode node={FALLBACK_DEMOS.d2 as any} />
</div>
<div data-ssr-demo="infographic" className="ssr-card">
<serverEntry.InfographicBlockNode node={FALLBACK_DEMOS.infographic as any} />
</div>
</>
)
}
function renderServerMatrixHtml() {
const items = createMatrixCases(serverEntry, serverEntry)
return (
<div className="ssr-matrix">
{items.map(item => (
<section key={item.name} data-ssr-export={item.name} className="ssr-card">
<h3>{item.name}</h3>
<div className="markstream-react ssr-matrix-renderer">{item.element}</div>
</section>
))}
</div>
)
}
function renderServerCustomHtml() {
return (
<>
<section data-ssr-custom-id="server-primary" className="ssr-card">
<serverEntry.NodeRenderer
customId={SERVER_CUSTOM_ID}
customHtmlTags={['thinking']}
nodes={SERVER_CUSTOM_NODES}
final
showTooltips={false}
/>
</section>
<section data-ssr-custom-id="server-alt" className="ssr-card">
<serverEntry.NodeRenderer
customId={SERVER_CUSTOM_ALT_ID}
customHtmlTags={['thinking']}
nodes={SERVER_CUSTOM_ALT_NODES}
final
showTooltips={false}
/>
</section>
</>
)
}
interface PagesSsrLabProps {
serverBasicHtml: string
serverCustomHtml: string
serverFallbackHtml: string
serverMatrixHtml: string
}
export default function PagesSsrLabPage({
serverBasicHtml,
serverCustomHtml,
serverFallbackHtml,
serverMatrixHtml,
}: PagesSsrLabProps) {
return (
<SsrLabPage
version="next15"
router="pages"
nextEntry={nextEntry}
serverEntry={serverEntry}
serverBasicHtml={serverBasicHtml}
serverCustomHtml={serverCustomHtml}
serverFallbackHtml={serverFallbackHtml}
serverMatrixHtml={serverMatrixHtml}
/>
)
}
export async function getServerSideProps() {
const { renderToStaticMarkup } = await import('react-dom/server')
serverEntry.setCustomComponents(SERVER_CUSTOM_ID, {
paragraph: ServerParagraphOverride,
insight: ServerInsightNode,
thinking: ServerThinkingTag,
})
serverEntry.setCustomComponents(SERVER_CUSTOM_ALT_ID, {
paragraph: ServerParagraphAlt,
insight: ServerInsightAlt,
thinking: ServerThinkingTagAlt,
})
return {
props: {
serverBasicHtml: renderToStaticMarkup(
<serverEntry.NodeRenderer
customId={SERVER_CUSTOM_ID}
nodes={SERVER_BASIC_NODES}
final
showTooltips={false}
/>,
),
serverCustomHtml: renderToStaticMarkup(renderServerCustomHtml()),
serverFallbackHtml: renderToStaticMarkup(renderServerFallbackHtml()),
serverMatrixHtml: renderToStaticMarkup(renderServerMatrixHtml()),
},
}
}
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
@@ -0,0 +1 @@
markstream-next15
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="40" viewBox="0 0 80 40" role="img" aria-labelledby="title desc">
<title id="title">SSR image</title>
<desc id="desc">Static test image for Next SSR acceptance.</desc>
<rect width="80" height="40" fill="#dbeafe" rx="6" />
<text x="40" y="25" text-anchor="middle" font-size="12" fill="#1e3a8a" font-family="system-ui, sans-serif">SSR</text>
</svg>

After

Width:  |  Height:  |  Size: 413 B

@@ -0,0 +1,249 @@
'use client'
import * as nextEntry from 'markstream-react/next'
import { NodeRenderer, setCustomComponents } from 'markstream-react/next'
import React, { useEffect, useState } from 'react'
import { createMatrixCases, NEXT_CUSTOM_ALT_ID, NEXT_CUSTOM_ID } from './ssr-lab-data'
function readAttr(node: any, name: string) {
if (!node?.attrs)
return undefined
if (Array.isArray(node.attrs))
return node.attrs.find((item: [string, string | null]) => item[0] === name)?.[1] ?? undefined
return node.attrs[name]
}
function renderCustomChildren(props: any) {
if (!Array.isArray(props.node?.children) || !props.renderNode || !props.ctx)
return null
return props.node.children.map((child: any, idx: number) => (
<React.Fragment key={`${String(props.indexKey ?? 'custom')}-${idx}`}>
{props.renderNode(child, `${String(props.indexKey ?? 'custom')}-${idx}`, props.ctx)}
</React.Fragment>
))
}
setCustomComponents(NEXT_CUSTOM_ID, {
paragraph: ({ node }: any) => (
<div data-ssr-status="next-paragraph-override">
{node.raw}
</div>
),
insight: (props: any) => (
<section data-ssr-status="next-custom-node">
<strong>{props.node.label}</strong>
<div data-ssr-status="next-custom-node-children">
{renderCustomChildren(props)}
</div>
</section>
),
thinking: ({ node }: any) => (
<aside
data-ssr-status="next-thinking-tag"
data-ssr-tone={readAttr(node, 'data-tone')}
data-ssr-scope={readAttr(node, 'data-scope')}
>
{node.content}
</aside>
),
})
setCustomComponents(NEXT_CUSTOM_ALT_ID, {
paragraph: ({ node }: any) => (
<div data-ssr-status="next-paragraph-alt">
{node.raw}
</div>
),
insight: (props: any) => (
<section data-ssr-status="next-custom-node-alt">
<strong>{props.node.label}</strong>
<div data-ssr-status="next-custom-node-alt-children">
{renderCustomChildren(props)}
</div>
</section>
),
thinking: ({ node }: any) => (
<aside
data-ssr-status="next-thinking-tag-alt"
data-ssr-scope={readAttr(node, 'data-scope')}
>
{node.content}
</aside>
),
})
export function NextCustomRenderer({ customId, nodes }: { customId: string, nodes: any[] }) {
return (
<NodeRenderer
customId={customId}
customHtmlTags={['thinking']}
nodes={nodes}
final
/>
)
}
function TooltipExportDemo() {
const [visible, setVisible] = useState(false)
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null)
return (
<div className="flex flex-wrap items-center gap-3">
<button
type="button"
className="rounded-full border border-stone-300 bg-white px-4 py-2 text-xs font-semibold tracking-[0.04em] text-stone-900 shadow-sm transition hover:bg-stone-50 focus:outline-none focus:ring-2 focus:ring-stone-300"
onBlur={() => setVisible(false)}
onFocus={(event) => {
setAnchorEl(event.currentTarget)
setVisible(true)
}}
onMouseEnter={(event) => {
setAnchorEl(event.currentTarget)
setVisible(true)
}}
onMouseLeave={() => setVisible(false)}
>
Hover for tooltip
</button>
<span className="text-xs font-medium text-stone-500">
hover / focus overlay export
</span>
<nextEntry.Tooltip
visible={visible}
anchorEl={anchorEl}
content="Tooltip body"
/>
</div>
)
}
function HtmlPreviewFrameExportDemo() {
const [open, setOpen] = useState(false)
return (
<div className="flex flex-wrap items-center gap-3">
<button
type="button"
className="rounded-full border border-stone-300 bg-white px-4 py-2 text-xs font-semibold tracking-[0.04em] text-stone-900 shadow-sm transition hover:bg-stone-50 focus:outline-none focus:ring-2 focus:ring-stone-300"
onClick={() => setOpen(true)}
>
Open preview
</button>
<span className="text-xs font-medium text-stone-500">
interactive modal export
</span>
{open
? (
<nextEntry.HtmlPreviewFrame
code="<p>Preview</p>"
onClose={() => setOpen(false)}
/>
)
: null}
</div>
)
}
export function NextExportMatrix() {
const items = createMatrixCases(nextEntry, nextEntry)
return (
<article data-ssr-case="export-matrix" data-ssr-entry="next">
<h2>Next Export Matrix</h2>
<div className="ssr-matrix">
{items.map(item => (
<section key={item.name} data-ssr-export={item.name} className="ssr-card">
<h3>{item.name}</h3>
<div className="markstream-react ssr-matrix-renderer">
{item.name === 'Tooltip'
? <TooltipExportDemo />
: item.name === 'HtmlPreviewFrame'
? <HtmlPreviewFrameExportDemo />
: item.element}
</div>
</section>
))}
</div>
</article>
)
}
function readEnhancementState() {
const nextEnhanced = document.querySelector('[data-ssr-case="enhanced"][data-ssr-entry="next"]')
const serverFallback = document.querySelector('[data-ssr-case="fallback"][data-ssr-entry="server"]')
const codeReady = Boolean(
nextEnhanced?.querySelector('[data-ssr-demo="code"] .monaco-editor')
|| nextEnhanced?.querySelector('[data-ssr-demo="code"] .code-editor-fallback-surface'),
)
const mathReady = Boolean(nextEnhanced?.querySelector('[data-ssr-demo="math"] .katex'))
const mermaidReady = Boolean(
nextEnhanced?.querySelector('[data-ssr-demo="mermaid"] .mermaid-block')
&& !nextEnhanced?.querySelector('[data-ssr-demo="mermaid"] [data-ssr-fallback="mermaid"]'),
)
const d2Ready = Boolean(nextEnhanced?.querySelector('[data-ssr-demo="d2"] .d2-svg svg'))
const infographicReady = Boolean(nextEnhanced?.querySelector('[data-ssr-demo="infographic"] svg'))
const fallbackStable = (serverFallback?.querySelectorAll('[data-ssr-fallback]').length ?? 0) >= 3
return {
codeReady,
mathReady,
mermaidReady,
d2Ready,
infographicReady,
fallbackStable,
}
}
export function HydrationProbe() {
const [nextStatus, setNextStatus] = useState('server')
const [serverStatus, setServerStatus] = useState('server')
const [summary, setSummary] = useState('pending')
useEffect(() => {
let attempts = 0
const timer = window.setInterval(() => {
attempts += 1
const state = readEnhancementState()
setSummary([
`code:${state.codeReady ? 'ready' : 'pending'}`,
`math:${state.mathReady ? 'ready' : 'pending'}`,
`mermaid:${state.mermaidReady ? 'ready' : 'pending'}`,
`d2:${state.d2Ready ? 'ready' : 'fallback'}`,
`infographic:${state.infographicReady ? 'ready' : 'fallback'}`,
].join(' '))
if (state.codeReady && state.mathReady && state.mermaidReady)
setNextStatus('enhanced')
else
setNextStatus(attempts > 4 ? 'hydrating' : 'server')
if (state.fallbackStable)
setServerStatus('fallback-stable')
else if (attempts > 4)
setServerStatus('fallback-missing')
if ((state.codeReady && state.mathReady && state.mermaidReady) && state.fallbackStable)
window.clearInterval(timer)
if (attempts >= 40)
window.clearInterval(timer)
}, 250)
return () => window.clearInterval(timer)
}, [])
return (
<>
<article data-ssr-case="hydration" data-ssr-entry="next" data-ssr-status={nextStatus}>
<h2>Hydration / Next</h2>
<p>{summary}</p>
</article>
<article data-ssr-case="hydration" data-ssr-entry="server" data-ssr-status={serverStatus}>
<h2>Hydration / Server</h2>
<p>Server fallback should stay stable after hydration.</p>
</article>
</>
)
}
@@ -0,0 +1,618 @@
import React from 'react'
const DEMO_IMAGE_SRC = '/ssr-image.svg'
function textNode(content: string) {
return {
type: 'text',
content,
raw: content,
}
}
function paragraphNode(content: string) {
return {
type: 'paragraph',
raw: content,
children: [textNode(content)],
}
}
function headingNode(content: string, level = 2) {
return {
type: 'heading',
level,
raw: `${'#'.repeat(level)} ${content}`,
children: [textNode(content)],
}
}
function listItemNode(content: string) {
return {
type: 'list_item',
raw: content,
children: [paragraphNode(content)],
}
}
function definitionListNode() {
return {
type: 'definition_list',
items: [
{
type: 'definition_item',
term: [textNode('Term')],
definition: [paragraphNode('Definition')],
},
],
}
}
function tableNode() {
return {
type: 'table',
header: {
cells: [
{ children: [textNode('Name')] },
{ children: [textNode('Value')] },
],
},
rows: [
{
cells: [
{ children: [textNode('Alpha')] },
{ children: [textNode('One')] },
],
},
],
}
}
function vmrContainerNode() {
return {
type: 'vmr_container',
name: 'tip',
attrs: [['data-kind', 'tip']],
children: [paragraphNode('Container body')],
}
}
function createRenderCtx(serverEntry: any) {
return {
customComponents: {},
events: {},
typewriter: false,
codeBlockStream: true,
showTooltips: false,
renderCodeBlocksAsPre: false,
codeBlockThemes: {},
customId: 'matrix',
renderNode: serverEntry.renderNode,
} as any
}
function insightNode(label: string, body: string) {
return {
type: 'insight',
raw: label,
label,
children: [paragraphNode(body)],
}
}
function customThinkingNode(scope: string, body: string) {
return {
type: 'html_block',
tag: 'thinking',
attrs: [
['data-tone', 'calm'],
['data-scope', scope],
],
content: `<thinking data-tone="calm" data-scope="${scope}">${body}</thinking>`,
raw: `<thinking data-tone="calm" data-scope="${scope}">${body}</thinking>`,
}
}
export const BASIC_NEXT_MARKDOWN = `
# Next Entry Basic
Server-first markdown content with <strong>inline HTML</strong>, a [stable link](https://example.com), and a footnote.[^1]
<div data-ssr-status="next-basic-html">Next HTML block</div>
| Name | Value |
| --- | --- |
| Next | SSR |
![Next SSR Image](${DEMO_IMAGE_SRC} "Next SSR Image")
[^1]: Footnote text from the Next entry.
`.trim()
export const NEXT_CUSTOM_ID = 'next-ssr-lab'
export const NEXT_CUSTOM_ALT_ID = 'next-ssr-lab-alt'
export const SERVER_CUSTOM_ID = 'server-ssr-lab'
export const SERVER_CUSTOM_ALT_ID = 'server-ssr-lab-alt'
export const NEXT_CUSTOM_NODES = [
paragraphNode('Next scoped paragraph'),
insightNode('Next custom node', 'Next custom child body'),
customThinkingNode('next-primary', 'Next scoped thinking body'),
] as any[]
export const NEXT_CUSTOM_ALT_NODES = [
paragraphNode('Next isolated paragraph'),
insightNode('Next isolated custom node', 'Next isolated child body'),
customThinkingNode('next-alt', 'Next isolated thinking body'),
] as any[]
export const SERVER_BASIC_NODES = [
headingNode('Server Entry Basic'),
paragraphNode('Server scoped paragraph'),
{
type: 'link',
href: 'https://example.com/server',
title: 'Server link',
text: 'Server link',
raw: '[Server link](https://example.com/server)',
children: [textNode('Server link')],
},
tableNode(),
{
type: 'html_block',
content: '<div data-ssr-status="server-basic-html">Server HTML block</div>',
raw: '<div data-ssr-status="server-basic-html">Server HTML block</div>',
},
] as any[]
export const SERVER_CUSTOM_NODES = [
paragraphNode('Server scoped paragraph'),
insightNode('Server custom node', 'Server custom child body'),
customThinkingNode('server-primary', 'Server scoped thinking body'),
] as any[]
export const SERVER_CUSTOM_ALT_NODES = [
paragraphNode('Server isolated paragraph'),
insightNode('Server isolated custom node', 'Server isolated child body'),
customThinkingNode('server-alt', 'Server isolated thinking body'),
] as any[]
export const ENHANCED_DEMOS = {
code: {
type: 'code_block',
language: 'ts',
code: 'export const answer = 42\nconsole.log(answer)',
raw: 'export const answer = 42\nconsole.log(answer)',
loading: false,
},
markdown: {
type: 'code_block',
language: 'md',
code: '# Nested markdown\n\n- item one\n- item two',
raw: '# Nested markdown\n\n- item one\n- item two',
loading: false,
},
mathBlock: {
type: 'math_block',
content: 'x^2 + y^2 = z^2',
raw: '$$x^2 + y^2 = z^2$$',
loading: false,
},
mathInline: {
type: 'math_inline',
content: 'a+b',
raw: '$a+b$',
markup: '$',
loading: false,
},
mermaid: {
type: 'code_block',
language: 'mermaid',
code: 'graph TD;A[Start]-->B[Done];',
raw: 'graph TD;A[Start]-->B[Done];',
loading: false,
},
d2: {
type: 'code_block',
language: 'd2',
code: 'api -> db',
raw: 'api -> db',
loading: false,
},
infographic: {
type: 'code_block',
language: 'infographic',
code: '{"title":"SSR Lab","children":[{"title":"Node A"},{"title":"Node B"}]}',
raw: '{"title":"SSR Lab","children":[{"title":"Node A"},{"title":"Node B"}]}',
loading: false,
},
} as const
export const FALLBACK_DEMOS = {
nextPre: {
type: 'code_block',
language: 'ts',
code: 'const stableFallback = true',
raw: 'const stableFallback = true',
loading: false,
},
mermaid: {
type: 'code_block',
language: 'mermaid',
code: 'graph TD;Fallback-->Stable;',
raw: 'graph TD;Fallback-->Stable;',
loading: false,
},
d2: {
type: 'code_block',
language: 'd2',
code: 'fallback -> stable',
raw: 'fallback -> stable',
loading: false,
},
infographic: {
type: 'code_block',
language: 'infographic',
code: '{"title":"Fallback"}',
raw: '{"title":"Fallback"}',
loading: false,
},
} as const
export function createMatrixCases(entry: any, serverEntry: any) {
const ctx = createRenderCtx(serverEntry)
const withCtx = (props: Record<string, any>) => ({
...props,
ctx,
renderNode: serverEntry.renderNode,
indexKey: 'matrix',
customId: 'matrix',
})
return [
{
name: 'NodeRenderer',
element: React.createElement(entry.NodeRenderer, {
content: '# Matrix Heading\n\nParagraph text',
final: true,
}),
},
{
name: 'AdmonitionNode',
element: React.createElement(entry.AdmonitionNode, withCtx({
node: {
type: 'admonition',
kind: 'note',
children: [paragraphNode('Alert body')],
},
})),
},
{
name: 'BlockquoteNode',
element: React.createElement(entry.BlockquoteNode, withCtx({
node: {
type: 'blockquote',
children: [paragraphNode('Quoted body')],
},
})),
},
{
name: 'CheckboxNode',
element: React.createElement(entry.CheckboxNode, {
node: { type: 'checkbox', checked: true },
}),
},
{
name: 'CodeBlockNode',
element: React.createElement(entry.CodeBlockNode, {
node: {
type: 'code_block',
language: 'ts',
code: 'const answer = 42',
raw: 'const answer = 42',
},
}),
},
{
name: 'ReactCodeBlockNode',
element: React.createElement(entry.ReactCodeBlockNode, {
node: {
type: 'code_block',
language: 'ts',
code: 'const answer = 42',
raw: 'const answer = 42',
},
}),
},
{
name: 'MarkdownCodeBlockNode',
element: React.createElement(entry.MarkdownCodeBlockNode, {
node: {
type: 'code_block',
language: 'md',
code: '# nested markdown',
raw: '# nested markdown',
},
}),
},
{
name: 'PreCodeNode',
element: React.createElement(entry.PreCodeNode, {
node: {
type: 'code_block',
language: 'js',
code: 'const fallback = true',
raw: 'const fallback = true',
},
}),
},
{
name: 'D2BlockNode',
element: React.createElement(entry.D2BlockNode, {
node: {
type: 'code_block',
language: 'd2',
code: 'x -> y',
raw: 'x -> y',
},
}),
},
{
name: 'MermaidBlockNode',
element: React.createElement(entry.MermaidBlockNode, {
node: {
type: 'code_block',
language: 'mermaid',
code: 'graph TD;A-->B;',
raw: 'graph TD;A-->B;',
},
}),
},
{
name: 'InfographicBlockNode',
element: React.createElement(entry.InfographicBlockNode, {
node: {
type: 'code_block',
language: 'infographic',
code: '{"title":"Demo"}',
raw: '{"title":"Demo"}',
},
}),
},
{
name: 'DefinitionListNode',
element: React.createElement(entry.DefinitionListNode, withCtx({
node: definitionListNode(),
})),
},
{
name: 'EmojiNode',
element: React.createElement(entry.EmojiNode, {
node: { type: 'emoji', name: 'sparkles' },
}),
},
{
name: 'EmphasisNode',
element: React.createElement(entry.EmphasisNode, {
node: { type: 'emphasis' },
children: 'Emphasis',
}),
},
{
name: 'StrongNode',
element: React.createElement(entry.StrongNode, {
node: { type: 'strong' },
children: 'Strong',
}),
},
{
name: 'StrikethroughNode',
element: React.createElement(entry.StrikethroughNode, {
node: { type: 'strikethrough' },
children: 'Strike',
}),
},
{
name: 'HighlightNode',
element: React.createElement(entry.HighlightNode, {
node: { type: 'highlight' },
children: 'Highlight',
}),
},
{
name: 'InsertNode',
element: React.createElement(entry.InsertNode, {
node: { type: 'insert' },
children: 'Insert',
}),
},
{
name: 'SubscriptNode',
element: React.createElement(entry.SubscriptNode, {
node: { type: 'subscript' },
children: 'Sub',
}),
},
{
name: 'SuperscriptNode',
element: React.createElement(entry.SuperscriptNode, {
node: { type: 'superscript' },
children: 'Sup',
}),
},
{
name: 'FootnoteNode',
element: React.createElement(entry.FootnoteNode, withCtx({
node: {
type: 'footnote',
id: '1',
children: [paragraphNode('Footnote body')],
},
})),
},
{
name: 'FootnoteReferenceNode',
element: React.createElement(entry.FootnoteReferenceNode, {
node: { type: 'footnote_reference', id: '1' },
}),
},
{
name: 'FootnoteAnchorNode',
element: React.createElement(entry.FootnoteAnchorNode, {
node: { type: 'footnote_anchor', id: '1' },
}),
},
{
name: 'HardBreakNode',
element: React.createElement(entry.HardBreakNode, {
node: { type: 'hardbreak' },
}),
},
{
name: 'HeadingNode',
element: React.createElement(entry.HeadingNode, {
node: { type: 'heading', level: 2 },
children: 'Heading',
}),
},
{
name: 'ParagraphNode',
element: React.createElement(entry.ParagraphNode, {
node: { type: 'paragraph' },
children: 'Paragraph',
}),
},
{
name: 'HtmlBlockNode',
element: React.createElement(entry.HtmlBlockNode, {
node: { type: 'html_block', content: '<div><strong>Block HTML</strong></div>' },
}),
},
{
name: 'HtmlInlineNode',
element: React.createElement(entry.HtmlInlineNode, {
node: { type: 'html_inline', content: '<span>Inline HTML</span>' },
}),
},
{
name: 'ImageNode',
element: React.createElement(entry.ImageNode, {
node: {
type: 'image',
src: DEMO_IMAGE_SRC,
alt: 'Demo image',
title: 'Demo image',
raw: '![Demo image](https://example.com/demo.png)',
},
}),
},
{
name: 'InlineCodeNode',
element: React.createElement(entry.InlineCodeNode, {
node: { type: 'inline_code', code: 'inline-demo' },
}),
},
{
name: 'LinkNode',
element: React.createElement(entry.LinkNode, {
node: {
type: 'link',
href: 'https://example.com',
title: 'Example',
text: 'Example',
raw: '[Example](https://example.com)',
children: [textNode('Example')],
},
children: 'Example',
}),
},
{
name: 'ListItemNode',
element: React.createElement(entry.ListItemNode, {
node: { type: 'list_item' },
children: 'List item',
}),
},
{
name: 'ListNode',
element: React.createElement(entry.ListNode, withCtx({
node: {
type: 'list',
ordered: false,
items: [listItemNode('List body')],
},
})),
},
{
name: 'MathBlockNode',
element: React.createElement(entry.MathBlockNode, {
node: {
type: 'math_block',
content: 'x^2',
raw: '$$x^2$$',
},
}),
},
{
name: 'MathInlineNode',
element: React.createElement(entry.MathInlineNode, {
node: {
type: 'math_inline',
content: 'a+b',
raw: '$a+b$',
markup: '$',
},
}),
},
{
name: 'ReferenceNode',
element: React.createElement(entry.ReferenceNode, {
node: { type: 'reference', id: 'ref-1' },
}),
},
{
name: 'TableNode',
element: React.createElement(entry.TableNode, withCtx({
node: tableNode(),
})),
},
{
name: 'TextNode',
element: React.createElement(entry.TextNode, {
node: { type: 'text', content: 'Text body' },
}),
},
{
name: 'ThematicBreakNode',
element: React.createElement(entry.ThematicBreakNode, {
node: { type: 'thematic_break' },
}),
},
{
name: 'Tooltip',
element: React.createElement(entry.Tooltip, {
visible: true,
anchorEl: null,
content: 'Tooltip body',
}),
},
{
name: 'HtmlPreviewFrame',
element: React.createElement(entry.HtmlPreviewFrame, {
code: '<p>Preview</p>',
}),
},
{
name: 'VmrContainerNode',
element: React.createElement(entry.VmrContainerNode, withCtx({
node: vmrContainerNode(),
})),
},
{
name: 'FallbackComponent',
element: React.createElement(entry.FallbackComponent, {
node: { type: 'mystery' },
}),
},
]
}
@@ -0,0 +1,288 @@
import React from 'react'
import { HydrationProbe, NextCustomRenderer, NextExportMatrix } from './ssr-lab-client'
import {
BASIC_NEXT_MARKDOWN,
createMatrixCases,
ENHANCED_DEMOS,
FALLBACK_DEMOS,
NEXT_CUSTOM_ALT_ID,
NEXT_CUSTOM_ALT_NODES,
NEXT_CUSTOM_ID,
NEXT_CUSTOM_NODES,
SERVER_BASIC_NODES,
SERVER_CUSTOM_ALT_ID,
SERVER_CUSTOM_ALT_NODES,
SERVER_CUSTOM_ID,
SERVER_CUSTOM_NODES,
} from './ssr-lab-data'
function readAttr(node: any, name: string) {
if (!node?.attrs)
return undefined
if (Array.isArray(node.attrs))
return node.attrs.find((item: [string, string | null]) => item[0] === name)?.[1] ?? undefined
return node.attrs[name]
}
function renderCustomChildren(props: any) {
if (!Array.isArray(props.node?.children) || !props.renderNode || !props.ctx)
return null
return props.node.children.map((child: any, idx: number) => (
<React.Fragment key={`${String(props.indexKey ?? 'custom')}-${idx}`}>
{props.renderNode(child, `${String(props.indexKey ?? 'custom')}-${idx}`, props.ctx)}
</React.Fragment>
))
}
function ServerParagraphOverride({ node }: any) {
return (
<div data-ssr-status="server-paragraph-override">
{node.raw}
</div>
)
}
function ServerInsightNode(props: any) {
return (
<section data-ssr-status="server-custom-node">
<strong>{props.node.label}</strong>
<div data-ssr-status="server-custom-node-children">
{renderCustomChildren(props)}
</div>
</section>
)
}
function ServerThinkingTag({ node }: any) {
return (
<aside
data-ssr-status="server-thinking-tag"
data-ssr-tone={readAttr(node, 'data-tone')}
data-ssr-scope={readAttr(node, 'data-scope')}
>
{node.content}
</aside>
)
}
function ServerParagraphAlt({ node }: any) {
return (
<div data-ssr-status="server-paragraph-alt">
{node.raw}
</div>
)
}
function ServerInsightAlt(props: any) {
return (
<section data-ssr-status="server-custom-node-alt">
<strong>{props.node.label}</strong>
<div data-ssr-status="server-custom-node-alt-children">
{renderCustomChildren(props)}
</div>
</section>
)
}
function ServerThinkingTagAlt({ node }: any) {
return (
<aside
data-ssr-status="server-thinking-tag-alt"
data-ssr-scope={readAttr(node, 'data-scope')}
>
{node.content}
</aside>
)
}
function MatrixSection({ caseName, entryName, items }: { caseName: string, entryName: string, items: Array<{ name: string, element: React.ReactElement }> }) {
return (
<article data-ssr-case={caseName} data-ssr-entry={entryName}>
<h2>{entryName === 'next' ? 'Next Export Matrix' : 'Server Export Matrix'}</h2>
<div className="ssr-matrix">
{items.map(item => (
<section key={item.name} data-ssr-export={item.name} className="ssr-card">
<h3>{item.name}</h3>
<div className="markstream-react ssr-matrix-renderer">{item.element}</div>
</section>
))}
</div>
</article>
)
}
export function SsrLabPage({
version,
router,
nextEntry,
serverEntry,
serverBasicHtml,
serverCustomHtml,
serverFallbackHtml,
serverMatrixHtml,
}: {
version: string
router: 'app' | 'pages'
nextEntry: any
serverEntry: any
serverBasicHtml?: string
serverCustomHtml?: string
serverFallbackHtml?: string
serverMatrixHtml?: string
}) {
serverEntry.setCustomComponents(SERVER_CUSTOM_ID, {
paragraph: ServerParagraphOverride,
insight: ServerInsightNode,
thinking: ServerThinkingTag,
})
serverEntry.setCustomComponents(SERVER_CUSTOM_ALT_ID, {
paragraph: ServerParagraphAlt,
insight: ServerInsightAlt,
thinking: ServerThinkingTagAlt,
})
const serverMatrix = createMatrixCases(serverEntry, serverEntry)
return (
<main data-ssr-version={version} data-ssr-router={router} className="ssr-lab-shell markstream-react">
<header className="ssr-hero">
<p className="ssr-eyebrow">
{version}
{' / '}
{router}
</p>
<h1>Markstream React Next SSR Lab</h1>
<p>Dual entry, dual router, raw HTML first, hydration afterwards.</p>
</header>
<div className="ssr-grid">
<article data-ssr-case="basic" data-ssr-entry="next">
<h2>Basic / Next</h2>
<nextEntry.NodeRenderer
content={BASIC_NEXT_MARKDOWN}
final
showTooltips={false}
/>
</article>
<article data-ssr-case="basic" data-ssr-entry="server">
<h2>Basic / Server</h2>
{serverBasicHtml
? <div dangerouslySetInnerHTML={{ __html: serverBasicHtml }} />
: (
<serverEntry.NodeRenderer
customId={SERVER_CUSTOM_ID}
customHtmlTags={['thinking']}
nodes={SERVER_BASIC_NODES}
final
showTooltips={false}
/>
)}
</article>
<article data-ssr-case="custom" data-ssr-entry="next">
<h2>Custom / Next</h2>
<section data-ssr-custom-id="next-primary" className="ssr-card">
<NextCustomRenderer customId={NEXT_CUSTOM_ID} nodes={NEXT_CUSTOM_NODES} />
</section>
<section data-ssr-custom-id="next-alt" className="ssr-card">
<NextCustomRenderer customId={NEXT_CUSTOM_ALT_ID} nodes={NEXT_CUSTOM_ALT_NODES} />
</section>
</article>
<article data-ssr-case="custom" data-ssr-entry="server">
<h2>Custom / Server</h2>
{serverCustomHtml
? <div dangerouslySetInnerHTML={{ __html: serverCustomHtml }} />
: (
<>
<section data-ssr-custom-id="server-primary" className="ssr-card">
<serverEntry.NodeRenderer
customId={SERVER_CUSTOM_ID}
customHtmlTags={['thinking']}
nodes={SERVER_CUSTOM_NODES}
final
showTooltips={false}
/>
</section>
<section data-ssr-custom-id="server-alt" className="ssr-card">
<serverEntry.NodeRenderer
customId={SERVER_CUSTOM_ALT_ID}
customHtmlTags={['thinking']}
nodes={SERVER_CUSTOM_ALT_NODES}
final
showTooltips={false}
/>
</section>
</>
)}
</article>
<article data-ssr-case="enhanced" data-ssr-entry="next">
<h2>Enhanced / Next</h2>
<div data-ssr-demo="code" className="ssr-card">
<nextEntry.CodeBlockNode node={ENHANCED_DEMOS.code as any} />
</div>
<div data-ssr-demo="markdown" className="ssr-card">
<nextEntry.MarkdownCodeBlockNode node={ENHANCED_DEMOS.markdown as any} />
</div>
<div data-ssr-demo="math" className="ssr-card">
<nextEntry.MathBlockNode node={ENHANCED_DEMOS.mathBlock as any} />
<p>
Inline math:
{' '}
<nextEntry.MathInlineNode node={ENHANCED_DEMOS.mathInline as any} />
</p>
</div>
<div data-ssr-demo="mermaid" className="ssr-card">
<nextEntry.MermaidBlockNode node={ENHANCED_DEMOS.mermaid as any} />
</div>
<div data-ssr-demo="d2" className="ssr-card">
<nextEntry.D2BlockNode node={ENHANCED_DEMOS.d2 as any} />
</div>
<div data-ssr-demo="infographic" className="ssr-card">
<nextEntry.InfographicBlockNode node={ENHANCED_DEMOS.infographic as any} />
</div>
</article>
<article data-ssr-case="fallback" data-ssr-entry="next">
<h2>Fallback / Next</h2>
<div data-ssr-demo="pre" className="ssr-card">
<nextEntry.PreCodeNode node={FALLBACK_DEMOS.nextPre as any} />
</div>
</article>
<article data-ssr-case="fallback" data-ssr-entry="server">
<h2>Fallback / Server</h2>
{serverFallbackHtml
? <div dangerouslySetInnerHTML={{ __html: serverFallbackHtml }} />
: (
<>
<div data-ssr-demo="mermaid" className="ssr-card">
<serverEntry.MermaidBlockNode node={FALLBACK_DEMOS.mermaid as any} />
</div>
<div data-ssr-demo="d2" className="ssr-card">
<serverEntry.D2BlockNode node={FALLBACK_DEMOS.d2 as any} />
</div>
<div data-ssr-demo="infographic" className="ssr-card">
<serverEntry.InfographicBlockNode node={FALLBACK_DEMOS.infographic as any} />
</div>
</>
)}
</article>
<NextExportMatrix />
{serverMatrixHtml
? (
<article data-ssr-case="server-export-matrix" data-ssr-entry="server">
<h2>Server Export Matrix</h2>
<div dangerouslySetInnerHTML={{ __html: serverMatrixHtml }} />
</article>
)
: <MatrixSection caseName="server-export-matrix" entryName="server" items={serverMatrix} />}
<HydrationProbe />
</div>
</main>
)
}
@@ -0,0 +1,91 @@
@import 'katex/dist/katex.min.css';
@import 'markstream-react/index.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
background: #f4f1e8;
color: #201a17;
}
body {
margin: 0;
font-family: Georgia, 'Times New Roman', serif;
}
a {
color: inherit;
}
* {
box-sizing: border-box;
}
.home-shell,
.ssr-lab-shell {
width: min(1200px, calc(100vw - 32px));
margin: 0 auto;
padding: 32px 0 64px;
}
.ssr-hero {
margin-bottom: 24px;
}
.ssr-eyebrow {
margin: 0 0 8px;
font: 600 12px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.ssr-grid {
display: grid;
gap: 20px;
}
[data-ssr-case] {
background: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(32, 26, 23, 0.14);
border-radius: 18px;
padding: 18px;
overflow: hidden;
}
.ssr-card {
border: 1px solid rgba(32, 26, 23, 0.12);
border-radius: 14px;
padding: 14px;
background: rgba(255, 255, 255, 0.72);
overflow: hidden;
}
.ssr-card + .ssr-card {
margin-top: 14px;
}
.ssr-matrix {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
}
.ssr-matrix-renderer {
position: relative;
}
.ssr-divider {
height: 1px;
margin: 16px 0;
background: rgba(32, 26, 23, 0.12);
}
@media (max-width: 720px) {
.home-shell,
.ssr-lab-shell {
width: min(100vw - 20px, 1200px);
padding-top: 20px;
}
}
@@ -0,0 +1,15 @@
/** @type {import('tailwindcss').Config} */
export default {
content: {
relative: true,
files: [
'./app/**/*.{js,ts,jsx,tsx,mdx}',
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/**/*.{js,ts,jsx,tsx,mdx}',
],
},
theme: {
extend: {},
},
plugins: [],
}
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"incremental": true,
"target": "ES2022",
"jsx": "preserve",
"lib": ["dom", "dom.iterable", "es2022"],
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"allowJs": false,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"isolatedModules": true,
"skipLibCheck": true,
"plugins": [{ "name": "next" }]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}