@archiva/temper-md
v0.1.0
Published
Markdown-first WYSIWYG editor with configurable AI generation and formatting for React frameworks
Downloads
25
Maintainers
Readme
TemperMD
Markdown-first WYSIWYG editor for React (Next.js, Astro, etc.) with configurable AI generation and formatting endpoints.
Install
temper-md is published as a private npm package (restricted access). Authenticate before installing.
# ~/.npmrc or project .npmrc (read-only token with access to CedarLabs packages)
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
npm install temper-mdPeer dependencies: react >= 18, react-dom >= 18.
Quick start
import {
EditorContent,
EditorRoot,
EMPTY_EDITOR_CONTENT,
StarterKit,
MarkdownExtension,
} from "temper-md";
export function Editor() {
return (
<EditorRoot
generateEndpoint="https://api.example.com/ai/generate"
formatEndpoint="https://api.example.com/ai/format"
headers={{ Authorization: "Bearer …" }}
>
<EditorContent
initialContent={EMPTY_EDITOR_CONTENT}
extensions={[StarterKit, MarkdownExtension]}
/>
</EditorRoot>
);
}Component model
The library is headless: it provides editor primitives, Tiptap extensions, and AI wiring. You compose chrome (toolbar, bubble menu, slash menus) yourself, or use the Tailwind reference implementation in apps/web.
EditorRoot AI config + Jotai store + command tunnel
└── EditorContent Tiptap EditorProvider wrapper
├── slotBefore Optional fixed toolbar (e.g. EditorToolbar)
├── children EditorCommand, EditorBubble, custom UI
└── slotAfter Optional post-content UI (e.g. ImageResizer)EditorRoot
| Prop | Type | Default | Effect |
|------|------|---------|--------|
| generateEndpoint | string | "/api/generate" | POST URL for AI writing assistance |
| formatEndpoint | string | "/api/format" | POST URL for AI markdown formatting |
| headers | Record<string, string> | — | Merged into every AI fetch (auth, tenancy, tracing) |
Provides useAIConfig() to descendants. Wrap once at the top of your editor tree.
EditorContent
Extends Tiptap EditorProvider (minus content). Common props:
| Prop | Type | Effect |
|------|------|--------|
| initialContent | JSONContent | Document loaded on mount. Use EMPTY_EDITOR_CONTENT for blank. |
| extensions | Extension[] | Tiptap extensions (include MarkdownExtension for markdown helpers). |
| className | string | Wrapper div class. |
| slotBefore | ReactNode | Renders above the editable surface (typical home for a toolbar). |
| slotAfter | ReactNode | Renders below the surface (e.g. ImageResizer). |
| editorProps | EditorProps | ProseMirror hooks: handlePaste, handleDrop, handleDOMEvents, attributes. |
| onUpdate | ({ editor }) => void | Fires on every document change. |
EditorToolbar
Fixed toolbar shell with role="toolbar". Pass your own buttons/selectors as children. Does not include controls by default.
EditorBubble / EditorBubbleItem
Floating menu anchored to a non-empty text selection. Hidden when the selection is empty, the editor is read-only, the selection is an image node, or the selection is a node drag handle.
| Interaction | Component API |
|-------------|---------------|
| Show on text select | <EditorBubble> with children |
| Run action on click | <EditorBubbleItem onSelect={(editor) => …}> |
| Placement / lifecycle | tippyOptions (e.g. placement: "top", onHidden) |
Slash & inline commands
| Extension | Trigger | Menu element id | Disabled in |
|-----------|---------|-----------------|-------------|
| Command (slash) | / | #slash-command | Code blocks |
| createCommandExtension({ char: "++" }) | ++ | #ai-command | Code blocks |
Both use createSuggestionItems, renderItems, and <EditorCommand> + <EditorCommandItem> children tunneled from EditorRoot.
Keyboard while a menu is open: ↑, ↓, Enter to navigate/select; Esc closes the popup.
Markdown helpers
| Function | Returns |
|----------|---------|
| getSelectionMarkdown(editor) | Markdown for the current selection |
| getDocumentMarkdown(editor) | Full document markdown |
| getPrevTextMarkdown(editor, pos) | Markdown from doc start through pos (used by Continue writing) |
Requires MarkdownExtension in your extension list.
MarkdownReader
Read-only, sanitized markdown renderer for publish/preview views. Unlike the WYSIWYG editor, this path is safe for untrusted markdown input.
import { MarkdownReader } from "temper-md";
<MarkdownReader
markdown={"# Hello\n\n| A | B |\n|---|---|\n| 1 | 2 |"}
className="prose"
/>| Prop | Type | Effect |
|------|------|--------|
| markdown | string | Markdown source to render |
| className | string | Wrapper div classes |
| components | Components | Optional react-markdown element overrides |
Pipeline: remark-gfm (tables, task lists, strikethrough) → rehype-sanitize (strip raw HTML) → safe link/image components.
URL safety: safeHref and safeSrc allow http, https, mailto, relative paths, and data:image/ for images. Other protocols (e.g. javascript:) are dropped.
Limitations: Renders markdown only. Editor-only nodes (math, YouTube, Twitter) are not shown unless exported as plain markdown. For pixel-perfect WYSIWYG preview of JSONContent, use a read-only EditorContent instance instead.
The reference demo wraps this as TemperReader in apps/web with Tailwind prose styling and optional storageKey hydration from localStorage.
User interactions
Below is the full interaction surface from the reference demo (TemperEditor in apps/web). When building your own UI, wire the same Tiptap commands or reuse the demo selectors.
Top toolbar (sticky)
Shown via slotBefore={<TopEditorToolbar … />}. Controls apply to the current selection or block at the cursor.
| Control | Action | Tiptap command |
|---------|--------|----------------|
| Block type dropdown | Text, H1–H3, to-do, bullet/numbered list, quote, code block | clearNodes(), toggleHeading, toggleTaskList, toggleBulletList, toggleOrderedList, toggleBlockquote, toggleCodeBlock |
| Link (Lucide link icon) | Set/unset hyperlink URL | setLink({ href }), unsetLink() |
| Math (Σ) | Wrap selection in LaTeX, or unset if already math | setLatex({ latex }), unsetLatex() |
| Bold / Italic / Underline / Strike / Code | Toggle inline marks | toggleBold, toggleItalic, toggleUnderline, toggleStrike, toggleCode |
| Color | Text color + highlight/background | setColor, unsetColor, setHighlight, unsetHighlight |
| AI (sparkles) | Opens Generate or Format submenus (see AI features) | Opens AI panel via command bridge |
| Save (optional) | Calls your onSave callback | Only rendered when onSave prop is provided |
Selection bubble menu
Same controls as the toolbar (block type, link, math, text marks, color, AI), but appears above selected text. Configure with <EditorBubble> inside EditorContent.
Closing the bubble (click away or hide) also closes any open AI panel and clears AI highlight.
Slash menu (/)
Type / at the start of a line (not inside a code block) to open the block insert menu.
| Item | Effect |
|------|--------|
| Text | Plain paragraph |
| To-do List | Task list with checkboxes |
| Heading 1 / 2 / 3 | Section headings |
| Bullet List / Numbered List | Lists |
| Quote | Blockquote |
| Code | Fenced code block |
| Table | 3×3 table with header row |
| Image | File picker → uploads via your uploadFn |
| Youtube | Prompt for URL → embed |
| Twitter | Prompt for X/Twitter URL → embed |
Filter items by typing after /. Select with click or keyboard.
Inside a table, the slash menu shows row/column edit actions instead of block inserts. Empty table cells suppress the Press '/' for commands placeholder.
AI inline menu (++)
Type ++ (not inside a code block) for quick AI actions without opening the full panel first.
| Group | Options | |-------|---------| | Generate | Improve writing, Fix grammar, Make shorter, Make longer, Continue writing | | Format | Restructure sections, Fix heading hierarchy, Normalize lists, Clean markdown syntax |
Selecting an item removes the trigger text and opens the matching AI panel with that option pre-run.
Text source rules:
- Continue writing — markdown from document start through cursor (
getPrevTextMarkdown) - All other generate options — current selection markdown (
getSelectionMarkdown) - Format options — selection markdown, or full document if nothing is selected
AI features
Three entry points share the same behavior:
- Toolbar / bubble AI menu — sparkles button → Generate or Format submenu
++inline menu — runs immediately- Custom prompt — "Custom prompt…" / "Custom format…" opens the panel for free-text
zapcommands
Generate panel
| Control | Behavior |
|---------|----------|
| Preset commands | Improve, Fix grammar, Shorter, Longer, Continue (same as ++) |
| Custom input + submit | Sends option: "zap" with your command string |
| After streaming completes | Replace selection, Insert below, or Discard |
Uses generateEndpoint. Highlights affected text while the panel is open (addAIHighlight / removeAIHighlight).
Format panel
| Control | Behavior |
|---------|----------|
| Preset commands | Structure, Headings, Lists, Cleanup |
| Custom input + submit | Sends option: "zap" with your format instruction |
| After streaming completes | Replace selection, Insert below, or Discard |
Uses formatEndpoint. Operates on selection markdown, falling back to the full document.
Keyboard shortcuts
| Shortcut | Effect |
|----------|--------|
| Cmd/Ctrl + A (first press) | Select all text within the current block/node |
| Cmd/Ctrl + A (second press) | Normal select-all |
| ↑ / ↓ / Enter | Navigate slash (/) or AI (++) suggestion menus |
| Esc | Close inline suggestion popup or AI panel |
Paste, drop, and images
| Interaction | Hook | Effect |
|-------------|------|--------|
| Paste image | editorProps.handlePaste + handleImagePaste | Uploads and inserts image at cursor |
| Drop image | editorProps.handleDrop + handleImageDrop | Same, when not moving existing content |
| Resize | <ImageResizer /> in slotAfter | Drag handles on selected images |
Provide your own uploadFn (see demo createImageUpload / Vercel Blob route).
Drag handle
GlobalDragHandle extension (included in demo defaults) adds a block drag handle for reordering. Tables are included in drag-handle custom nodes.
Reference demo: TemperEditor
The demo wraps the headless primitives with Tailwind UI. Import from your app copy or mirror the pattern in apps/web/components/temper-editor.tsx.
import TemperEditor from "@/components/temper-editor";
<TemperEditor
generateEndpoint="https://api.example.com/ai/generate"
formatEndpoint="https://api.example.com/ai/format"
aiHeaders={{ Authorization: "Bearer …" }}
initialContent={EMPTY_EDITOR_CONTENT}
storageKey="my-doc"
onSave={() => persistToApi()}
className="rounded-lg border"
/>| Prop | Type | Effect |
|------|------|--------|
| generateEndpoint | string | Required. POST URL for AI writing assistance |
| formatEndpoint | string | Required. POST URL for AI markdown formatting |
| aiHeaders | Record<string, string> | Optional headers for AI fetches |
| initialContent | JSONContent | Starting document. Defaults to EMPTY_EDITOR_CONTENT. |
| storageKey | string | Enables localStorage persistence (JSON + HTML + markdown, 500 ms debounce). Shows save status and word count. |
| onSave | () => void | When provided, renders a Save icon button in the toolbar that calls this function. |
| className | string | Outer editor container classes. |
The demo app wires these from its own .env via getAIEndpointConfig() — that helper is not part of the component API.
Reference demo: TemperReader
The demo provides a styled reader at apps/web/components/temper-reader.tsx and a /read route.
import TemperReader from "@/components/temper-reader";
<TemperReader storageKey="my-doc" />When TemperEditor uses the same storageKey, it persists markdown to ${storageKey}-markdown in localStorage. TemperReader reads that key for a live preview workflow: edit on /, read on /read.
AI endpoints
Both endpoints accept POST JSON:
{
"prompt": "markdown or selected text",
"option": "continue | improve | shorter | longer | fix | zap | structure | headings | lists | cleanup",
"command": "optional free-text command for zap",
"mode": "generate | format"
}Responses should be Vercel AI SDK data streams (toDataStreamResponse()), or any compatible streaming completion consumed by useCompletion.
| Option | Mode | Purpose |
|--------|------|---------|
| continue | generate | Extend text from prior context |
| improve | generate | Polish clarity and flow |
| fix | generate | Grammar and spelling |
| shorter | generate | Condense |
| longer | generate | Expand |
| zap | generate / format | Free-text instruction via command |
| structure | format | Improve document flow |
| headings | format | Normalize heading levels |
| lists | format | Clean up list markdown |
| cleanup | format | Fix spacing and syntax |
Configure per environment:
generateEndpoint/formatEndpointonEditorRoot, or- Demo env vars:
AI_HARNESS,AI_GENERATE_ENDPOINT,AI_FORMAT_ENDPOINT,AI_ENDPOINT_AUTHORIZATION
Default content
Use EMPTY_EDITOR_CONTENT when no document is provided. Pass initialContent to hydrate from props or your persistence layer.
Exports
- Editor primitives:
EditorRoot,EditorContent,EditorBubble,EditorBubbleItem,EditorToolbar,EditorCommand,EditorCommandItem,EditorCommandList,EditorCommandEmpty,useEditor - Reader:
MarkdownReader,MarkdownReaderProps - Slash / command extensions:
Command,createCommandExtension,createSuggestionItems,renderItems,handleCommandNavigation - Markdown:
getSelectionMarkdown,getDocumentMarkdown,getPrevTextMarkdown,MarkdownExtension - URL safety:
safeHref,safeSrc - AI:
AIConfigProvider,useAIConfig,AIHighlight,addAIHighlight,removeAIHighlight, request types - Media:
UploadImagesPlugin,handleImagePaste,handleImageDrop,ImageResizer,UpdatedImage - Tiptap extensions:
StarterKit,TiptapLink,TiptapUnderline,Mathematics,Youtube,Twitter,Table,TableRow,TableCell,TableHeader, and more — see package exports
See apps/web for the full Tailwind reference implementation with toolbar, bubble menu, slash commands, AI panels, and reader view.
