38 / Documents
Markdown Blocks
Extracted document regions rendered as markdown, each one selectable so it can be tied back to where it came from.
"use client"
import { MarkdownBlocks } from "mischief-ui/markdown-blocks"
const blocks = [
{
id: "title",
kind: "heading" as const,
page: 1,
content: "# Master Services Agreement",
},
{
id: "intro",
kind: "paragraph" as const,
page: 1,
content:
"This agreement is made between **Northwind Traders** and the supplier named below, effective on the date of the last signature.",
},
{
id: "terms",
kind: "table" as const,
page: 2,
content:
"| Term | Value |\n| --- | --- |\n| Net | 30 days |\n| Currency | USD |",
},
{
id: "notes",
kind: "list" as const,
page: 2,
content:
"- Late payment accrues 1.5% monthly\n- Disputed lines pause the clock",
},
]
export function MarkdownBlocksDemo() {
return <MarkdownBlocks className="w-full max-w-2xl" blocks={blocks} />
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/markdown-blocksimport { MarkdownBlocks } from "mischief-ui/markdown-blocks"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import Markdown from "react-markdown"import remarkGfm from "remark-gfm"import { cn } from "@/lib/utils" export type MarkdownBlockKind = "heading" | "paragraph" | "table" | "list" | "figure" | "footer" export type MarkdownBlock = { id: string kind?: MarkdownBlockKind content: stringUsage
const blocks = [
{ id: "title", kind: "heading", content: "# Master Agreement", page: 1 },
]
export function Layout() {
return <MarkdownBlocks blocks={blocks} onActiveChange={highlightRegion} />
}Why blocks rather than a document
Document extraction does not return an essay, it returns pieces: a heading here, a table there, a paragraph that came from page four. Keeping them as separate blocks means each one can be pointed at, highlighted, corrected, or traced back to where it came from, which a single rendered string cannot do.
kind is what the extractor thought a block was, and page is where it found it. Both are optional, and both are what make it possible to line this up with a page navigator or a set of bounding boxes over the original.
<MarkdownBlocks
blocks={[
{ id: "h1", kind: "heading", content: "## Payment terms", page: 2 },
{ id: "p1", kind: "paragraph", content: "Invoices are payable **net 30**.", page: 2 },
{ id: "t1", kind: "table", content: tableMarkdown, page: 3 },
]}
activeId={selected}
onActiveChange={setSelected}
/>What the markdown may contain
Blocks are rendered with react-markdown and GitHub Flavoured Markdown, so tables, strikethrough, task lists, and bare autolinks all work on top of the usual syntax. Raw HTML inside the content is not rendered as HTML -- there is no rehype-raw here, which is what keeps text extracted from someone else's document from bringing markup into your page.
react-markdown and remark-gfm are optional peers, so this component is imported from its own entry and needs both installed alongside.
npm install mischief-ui react-markdown remark-gfmAPI
blocksMarkdownBlock[]Id, markdown content, and optional kind, page, and label.activeId, defaultActiveIdstring | nullThe selected block, controlled or uncontrolled.onActiveChange(id: string | null) => voidRuns when a block is selected or cleared. Pair with Bounding Boxes.showKindsbooleanShows the kind and page above each block.MarkdownBlock
idstringUnique within the set. Drives selection.kindMarkdownBlockKindheading, paragraph, table, list, figure, or footer.contentstringThe markdown for this block.pagenumberWhere it came from in the original.labelstringOverrides the wording of the kind badge.Accessibility
Blocks are an ordered list of toggle buttons, so selection is reachable by keyboard and announced. Raw HTML inside a block is not rendered, since react-markdown ignores it unless a raw plugin is added, which this component deliberately does not add. Tables come from GitHub flavoured markdown and render as real tables.