A long piece of writing as a document that writes itself in.
Every shadcn style · one file
v2.0 · Updated Oct 13pnpm dlx shadcn@latest add @notchset-pro/artifactpnpm dlx shadcn@latest add @notchset-pro/artifactNeeds your registry key. Every components.json style gets the same files.
Works with React 18.3 or 19 on Tailwind CSS v4, in any shadcn project. What's tested
The CLI adds the styles to the CSS file named in components.json (tailwind.css). Check that it points at your global stylesheet, that the project uses Tailwind CSS v4, and that the file is imported in your root layout. Installed by hand: paste styles.css into that file.
Import it and pass your content. The props that matter most are listed next.
"use client";
import * as React from "react";
import { Artifact, type ArtifactBlock, type ArtifactVersion } from "@/components/ui/notchset/blocks/artifact";
import { Button } from "@/components/ui/notchset/button";
const V1: ArtifactBlock[] = [
{ id: "h1", kind: "heading", text: "Background" },
{ id: "p1", kind: "paragraph", text: "Over the last two quarters the onboarding team has looked at how new workspaces get set up, and some findings came out of the beta." },
{ id: "p2", kind: "paragraph", text: "We would like to launch on 14 November if everyone is comfortable with that." },
];
const V2: ArtifactBlock[] = [
{ id: "a1", kind: "heading", text: "The ask" },
{ id: "a2", kind: "paragraph", text: "Approve a 14 November launch to all 2,400 workspace admins." },
{ id: "a3", kind: "heading", text: "Why now" },
{ id: "a4", kind: "paragraph", text: "Beta admins finished setup in 9 minutes on average, down from 31." },
{ id: "a5", kind: "item", text: "Owner for billing risk: Priya, Finance." },
{ id: "a6", kind: "item", text: "Owner for help articles: Dev, Docs, by 10 November." },
];
export default function ArtifactDemo() {
const [versions, setVersions] = React.useState<ArtifactVersion[]>([
{ id: "v1", n: 1, by: "you", at: "3:58 pm", blocks: V1 },
{ id: "v2", n: 2, by: "assistant", at: "4:02 pm", blocks: V2, changed: ["a1", "a2", "a4"] },
]);
const [current, setCurrent] = React.useState("v2");
const [writing, setWriting] = React.useState(false);
const [asked, setAsked] = React.useState<string | null>(null);
/* the sample writes a version in, one passage at a time */
const rewrite = () => {
const base = versions[versions.length - 1]!;
const next: ArtifactBlock[] = [...base.blocks.slice(0, 4), { id: "b1", kind: "paragraph", text: "Setup tickets fell by half over the same six weeks." }, ...base.blocks.slice(4)];
const id = `v${versions.length + 1}`;
setWriting(true);
setVersions((vs) => [...vs, { id, n: vs.length + 1, by: "assistant", at: "now", blocks: next.slice(0, 2), changed: [] }]);
setCurrent(id);
next.forEach((_, i) =>
setTimeout(() => {
setVersions((vs) => vs.map((v) => (v.id === id ? { ...v, blocks: next.slice(0, i + 1), changed: next.slice(0, i + 1).some((b) => b.id === "b1") ? ["b1"] : [] } : v)));
if (i === next.length - 1) setWriting(false);
}, 350 * (i + 1)),
);
};
return (
<div className="flex flex-col gap-3">
<div className="h-[420px] overflow-hidden rounded-[var(--ns-radius-lg)] shadow-[inset_0_0_0_1px_var(--ns-line)]">
<Artifact
title="Notchset launch brief"
versions={versions}
current={current}
onPick={setCurrent}
writing={writing}
onAsk={(t) => setAsked(t)}
onSave={(blocks) => {
const id = `v${versions.length + 1}`;
setVersions((vs) => [...vs, { id, n: vs.length + 1, by: "you", at: "now", blocks }]);
setCurrent(id);
}}
onRestore={(from) => {
const src = versions.find((v) => v.id === from)!;
const id = `v${versions.length + 1}`;
setVersions((vs) => [...vs, { ...src, id, n: vs.length + 1, by: "you", at: "now", changed: [] }]);
setCurrent(id);
}}
/>
</div>
<div className="flex flex-wrap items-center gap-3">
<Button size="sm" disabled={writing} onClick={rewrite}>
Add a figure
</Button>
<span className="min-w-0 truncate text-[length:var(--ns-text-small)] text-[var(--ns-ink-3)]">{asked ? `Sent to the thread: “${asked}”` : "Select a passage to ask about it."}</span>
</div>
</div>
);
}components/ui/notchset/blocks/artifact.tsx
This component is included with Pro.
Try the live preview above. Get Pro to install the source.
title–
stringRequired.
versions–
ArtifactVersion[]Required.
current–
stringthe version on screen (the newest when none)
onPick–
(id: string) => voidwritingfalse
booleanthe assistant is writing the newest version now
onAsk–
(passage: string) => void"Ask about this" on a selected passage
onSave–
(blocks: ArtifactBlock[]) => voida hand edit, saved as your version
onRestore–
(id: string) => voidmakes an older version the newest (as a new version: nothing is overwritten)
onClose–
() => voidshowChanges–
booleanthe quiet marks on the passages the shown version changed; uncontrolled it starts on, and Show changes toggles it
onShowChangesChange–
(on: boolean) => voidonAccept–
(blockId: string) => voidkeep one change as it is: it stops being marked
onAcceptAll–
() => voidkeep every change at once (one action, one Undo)
onReject–
(blockId: string) => voidtake one change back: the passage returns to what it was, as a new version
assistantName"Assistant"
stringthe assistant's name in "Maya via Assistant"
userName–
stringthe person's name: their versions are theirs ("Maya"), the assistant's are made for them ("Maya via Assistant")
Give your coding assistant the Markdown guide instead of screenshots.
https://notchset.dev/api/mcp (setup)v2.0.0 · 2026-10-13
Released with the Notchset system.
The same file, at work in a finished app. Open it live and find it on its pages.
Pro is in early access.