npm package: @s3rd/canvas-artifacts
Description: Drop-in Canvas editor for React AI applications
Latest version: 0.1.6

# @s3rd/canvas-artifacts

A drop-in React Canvas editor for AI applications. Give your chat UI a document editing experience — streaming-aware, fully controlled, no AI SDK lock-in.

Inspired by OpenCanvas (LangChain) and ChatGPT Canvas, distributed as a reusable npm package.

---

## Getting Started

### 1. Install

```bash
npm install @s3rd/canvas-artifacts
```

Install peer dependencies if you don't already have them:

```bash
npm install react react-dom tailwindcss
```

### 2. Import styles

Add the stylesheet once — at your app entry point or global CSS file:

```tsx
import '@s3rd/canvas-artifacts/styles.css'
```

### 3. Configure Tailwind

Add the package to your `tailwind.config` content paths so its classes are included:

```js
// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{ts,tsx}',
    './node_modules/@s3rd/canvas-artifacts/dist/**/*.{js,mjs}',
  ],
}
```

### 4. Render the Canvas

```tsx
import { Canvas } from '@s3rd/canvas-artifacts'

function App() {
  const [content, setContent] = useState('# Hello\n\nStart typing...')

  return <Canvas value={content} onChange={setContent} />
}
```

`value` is a markdown string. `onChange` fires on every user edit. That's the whole API.

---

## Demo

The [`demo-nextjs`](./demo-nextjs) directory is a full working example built with Next.js and the Vercel AI SDK. It shows how to wire up a chat interface with a live canvas panel — the AI writes documents directly into the editor as it streams.

**What it covers:**
- Server-side tool definition with `canvasTool` from `@s3rd/canvas-artifacts/vercel-ai/server`
- Client-side canvas state via `useCanvasTool` from `@s3rd/canvas-artifacts/vercel-ai`
- Streaming document content into the editor
- Canvas action handling (polish, adjust length, suggest edits)

**Run it locally:**

```bash
cd demo-nextjs
cp .env.example .env.local   # add your OPENAI_API_KEY
npm install
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) to see it in action.

---

## Variants

### `inline` (default)

Always renders an interactive editor in page flow.

```tsx
<Canvas value={content} onChange={setContent} variant="inline" />
```

### `sideview`

Shows a non-interactive preview inline. Clicking **Edit** slides open a side panel with the full editor.

```tsx
<Canvas value={content} onChange={setContent} variant="sideview" />
```

### `both`

Starts as an inline editor. An expand button in the toolbar transitions it to a side panel.

```tsx
<Canvas value={content} onChange={setContent} variant="both" />
```

---

## AI streaming

The canvas handles streaming out of the box. Just update `value` rapidly from your AI stream — the editor locks, appends content efficiently, and unlocks when the stream settles.

`onChange` is **not** called during streaming. It only fires on user-initiated edits.

```tsx
// Works with any AI SDK — Vercel AI SDK, raw fetch, anything
const [content, setContent] = useState('')

// Stream into value — canvas handles the rest
useEffect(() => {
  streamFromAI((chunk) => setContent(prev => prev + chunk))
}, [])

<Canvas value={content} onChange={setContent} variant="sideview" />
```

---

## Panel control

Use `useCanvas` to control the side panel programmatically — for example, auto-opening it when an AI response finishes.

```tsx
import { Canvas, useCanvas } from '@s3rd/canvas-artifacts'

function App() {
  const [content, setContent] = useState('')
  const canvas = useCanvas({ defaultOpen: false })

  useEffect(() => {
    if (streamComplete) canvas.openPanel()
  }, [streamComplete])

  return (
    <Canvas
      {...canvas}
      value={content}
      onChange={setContent}
      variant="sideview"
    />
  )
}
```

`useCanvas` returns `{ open, onOpenChange, openPanel, closePanel, togglePanel }`.

---

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `value` | `string` | — | Markdown string (required) |
| `onChange` | `(value: string) => void` | — | Called on user edits (required) |
| `variant` | `'inline' \| 'sideview' \| 'both'` | `'inline'` | Layout mode |
| `readOnly` | `boolean` | `false` | Disables editing, shows static preview |
| `toolbar` | `boolean \| ToolbarConfig` | `true` | Show/hide or configure toolbar |
| `open` | `boolean` | — | Controlled panel open state |
| `defaultOpen` | `boolean` | `false` | Initial panel open state |
| `onOpenChange` | `(open: boolean) => void` | — | Panel open state callback |
| `className` | `string` | — | Tailwind classes on the root element |

### ToolbarConfig

All buttons are enabled by default. Pass `{ bold: false }` to hide specific buttons.

```ts
interface ToolbarConfig {
  bold?: boolean
  italic?: boolean
  underline?: boolean
  strike?: boolean
  heading?: boolean      // h1/h2/h3
  bulletList?: boolean
  orderedList?: boolean
  blockquote?: boolean
  code?: boolean         // inline code
  link?: boolean
}
```

---

## Theming

**Tier 1 — CSS variables** (no ejection needed):

```css
:root {
  --canvas-bg: #ffffff;
  --canvas-border: #e2e8f0;
  --canvas-toolbar-bg: #f8fafc;
  --canvas-prose-color: #1a202c;
  --canvas-accent: #6366f1;
}
```

Dark mode is supported via the `.dark` class (shadcn/ui compatible).

**Tier 2 — `className` prop**:

```tsx
<Canvas className="rounded-xl shadow-lg" value={content} onChange={setContent} />
```

**Tier 3 — Eject and own the source** (shadcn-style):

```bash
npx @s3rd/canvas-artifacts add toolbar
npx @s3rd/canvas-artifacts add canvas-preview
npx @s3rd/canvas-artifacts add canvas-layout
```

Copies the component into `src/components/canvas/` — you own it from that point. The `core/` layer (editor sync, markdown bridge, state machine) stays in `node_modules` and receives updates automatically.

---

## License

MIT