{"_id":"@archiva/temper-md","name":"@archiva/temper-md","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@archiva/temper-md","version":"0.1.0","description":"Markdown-first WYSIWYG editor with configurable AI generation and formatting for React frameworks","license":"Apache-2.0","type":"module","main":"dist/index.cjs","module":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"dev":"tsup --watch","typecheck":"tsc --noEmit","build":"tsup","prepublishOnly":"pnpm run build","lint":"biome lint ./src","format":"biome format ./src "},"sideEffects":false,"publishConfig":{"access":"restricted"},"repository":{"type":"git","url":"git+https://github.com/KwongJR/temper-write.git","directory":"packages/headless"},"bugs":{"url":"https://github.com/KwongJR/temper-write/issues"},"peerDependencies":{"react":">=18","react-dom":">=18"},"dependencies":{"@radix-ui/react-slot":"^1.1.1","@tiptap/core":"^2.27.2","@tiptap/extension-character-count":"^2.27.2","@tiptap/extension-code-block-lowlight":"^2.27.2","@tiptap/extension-color":"^2.27.2","@tiptap/extension-highlight":"^2.27.2","@tiptap/extension-horizontal-rule":"^2.27.2","@tiptap/extension-image":"^2.27.2","@tiptap/extension-link":"^2.27.2","@tiptap/extension-placeholder":"^2.27.2","@tiptap/extension-table":"^2.27.2","@tiptap/extension-table-cell":"^2.27.2","@tiptap/extension-table-header":"^2.27.2","@tiptap/extension-table-row":"^2.27.2","@tiptap/extension-task-item":"^2.27.2","@tiptap/extension-task-list":"^2.27.2","@tiptap/extension-text-style":"^2.27.2","@tiptap/extension-underline":"^2.27.2","@tiptap/extension-youtube":"^2.27.2","@tiptap/pm":"^2.27.2","@tiptap/react":"^2.27.2","@tiptap/starter-kit":"^2.27.2","@tiptap/suggestion":"^2.27.2","cmdk":"^1.0.4","jotai":"^2.11.0","katex":"^0.16.20","react-markdown":"^9.0.3","rehype-sanitize":"^6.0.0","remark-gfm":"^4.0.0","react-moveable":"^0.56.0","react-tweet":"^3.2.1","tippy.js":"^6.3.7","tiptap-extension-global-drag-handle":"^0.1.16","tiptap-markdown":"^0.8.10","tunnel-rat":"^0.1.2"},"devDependencies":{"@types/node":"^22.10.6","@biomejs/biome":"^1.9.4","@types/katex":"^0.16.7","@types/react":"^18.2.55","@types/react-dom":"18.2.19","tsconfig":"workspace:*","tsup":"^8.3.5","typescript":"^5.7.3"},"author":{"name":"CedarLabs","email":"hello@cedarlabs.dev"},"homepage":"https://cedarlabs.dev","keywords":["ai","temper-md","editor","markdown","nextjs","react","tiptap","wysiwyg"],"gitHead":"cf943bea45d4eaae42a80605584eb4dafe81ebf0","_id":"@archiva/temper-md@0.1.0","_nodeVersion":"24.11.0","_npmVersion":"11.8.0","dist":{"integrity":"sha512-TrD4uRZa3lPL8S1CStMFvc5cFqr2xTwzH2nhjh73vA9F5AZ2FEALbM87Ffk+l1MiAf7vIEKGQ1lwz+TA6FvkUQ==","shasum":"155acc215b5f412c60061d88ff4dd9fbd8bfc077","tarball":"https://registry.npmjs.org/@archiva/temper-md/-/temper-md-0.1.0.tgz","fileCount":6,"unpackedSize":87004,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDT/qqqRVu7tPwB5ZMNUPbk7n2XFrLhO67Es+A8LmhfqQIgWwZ/5sO1ib7DoYRBP+P09AD+JxCfS9kqG/Uxc5jlGdA="}]},"_npmUser":{"name":"ksondevs","email":"jason@cedarlabs.dev"},"directories":{},"maintainers":[{"name":"ksondevs","email":"jason@cedarlabs.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/temper-md_0.1.0_1783868704914_0.920955424771863"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-12T15:05:04.659Z","0.1.0":"2026-07-12T15:05:05.058Z","modified":"2026-07-12T15:05:05.430Z"},"maintainers":[{"name":"ksondevs","email":"jason@cedarlabs.dev"}],"description":"Markdown-first WYSIWYG editor with configurable AI generation and formatting for React frameworks","homepage":"https://cedarlabs.dev","keywords":["ai","temper-md","editor","markdown","nextjs","react","tiptap","wysiwyg"],"repository":{"type":"git","url":"git+https://github.com/KwongJR/temper-write.git","directory":"packages/headless"},"author":{"name":"CedarLabs","email":"hello@cedarlabs.dev"},"bugs":{"url":"https://github.com/KwongJR/temper-write/issues"},"license":"Apache-2.0","readme":"# TemperMD\r\n\r\nMarkdown-first WYSIWYG editor for React (Next.js, Astro, etc.) with configurable AI generation and formatting endpoints.\r\n\r\n## Install\r\n\r\n`temper-md` is published as a **private** npm package (restricted access). Authenticate before installing.\r\n\r\n```bash\r\n# ~/.npmrc or project .npmrc (read-only token with access to CedarLabs packages)\r\n//registry.npmjs.org/:_authToken=${NPM_TOKEN}\r\n\r\nnpm install temper-md\r\n```\r\n\r\nPeer dependencies: `react >= 18`, `react-dom >= 18`.\r\n\r\n## Quick start\r\n\r\n```tsx\r\nimport {\r\n  EditorContent,\r\n  EditorRoot,\r\n  EMPTY_EDITOR_CONTENT,\r\n  StarterKit,\r\n  MarkdownExtension,\r\n} from \"temper-md\";\r\n\r\nexport function Editor() {\r\n  return (\r\n    <EditorRoot\r\n      generateEndpoint=\"https://api.example.com/ai/generate\"\r\n      formatEndpoint=\"https://api.example.com/ai/format\"\r\n      headers={{ Authorization: \"Bearer …\" }}\r\n    >\r\n      <EditorContent\r\n        initialContent={EMPTY_EDITOR_CONTENT}\r\n        extensions={[StarterKit, MarkdownExtension]}\r\n      />\r\n    </EditorRoot>\r\n  );\r\n}\r\n```\r\n\r\n## Component model\r\n\r\nThe 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`](../../apps/web).\r\n\r\n```\r\nEditorRoot          AI config + Jotai store + command tunnel\r\n└── EditorContent   Tiptap EditorProvider wrapper\r\n    ├── slotBefore  Optional fixed toolbar (e.g. EditorToolbar)\r\n    ├── children    EditorCommand, EditorBubble, custom UI\r\n    └── slotAfter   Optional post-content UI (e.g. ImageResizer)\r\n```\r\n\r\n### `EditorRoot`\r\n\r\n| Prop | Type | Default | Effect |\r\n|------|------|---------|--------|\r\n| `generateEndpoint` | `string` | `\"/api/generate\"` | POST URL for AI writing assistance |\r\n| `formatEndpoint` | `string` | `\"/api/format\"` | POST URL for AI markdown formatting |\r\n| `headers` | `Record<string, string>` | — | Merged into every AI fetch (auth, tenancy, tracing) |\r\n\r\nProvides `useAIConfig()` to descendants. Wrap once at the top of your editor tree.\r\n\r\n### `EditorContent`\r\n\r\nExtends Tiptap `EditorProvider` (minus `content`). Common props:\r\n\r\n| Prop | Type | Effect |\r\n|------|------|--------|\r\n| `initialContent` | `JSONContent` | Document loaded on mount. Use `EMPTY_EDITOR_CONTENT` for blank. |\r\n| `extensions` | `Extension[]` | Tiptap extensions (include `MarkdownExtension` for markdown helpers). |\r\n| `className` | `string` | Wrapper div class. |\r\n| `slotBefore` | `ReactNode` | Renders above the editable surface (typical home for a toolbar). |\r\n| `slotAfter` | `ReactNode` | Renders below the surface (e.g. `ImageResizer`). |\r\n| `editorProps` | `EditorProps` | ProseMirror hooks: `handlePaste`, `handleDrop`, `handleDOMEvents`, `attributes`. |\r\n| `onUpdate` | `({ editor }) => void` | Fires on every document change. |\r\n\r\n### `EditorToolbar`\r\n\r\nFixed toolbar shell with `role=\"toolbar\"`. Pass your own buttons/selectors as `children`. Does not include controls by default.\r\n\r\n### `EditorBubble` / `EditorBubbleItem`\r\n\r\nFloating 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.\r\n\r\n| Interaction | Component API |\r\n|-------------|---------------|\r\n| Show on text select | `<EditorBubble>` with `children` |\r\n| Run action on click | `<EditorBubbleItem onSelect={(editor) => …}>` |\r\n| Placement / lifecycle | `tippyOptions` (e.g. `placement: \"top\"`, `onHidden`) |\r\n\r\n### Slash & inline commands\r\n\r\n| Extension | Trigger | Menu element id | Disabled in |\r\n|-----------|---------|-----------------|-------------|\r\n| `Command` (slash) | `/` | `#slash-command` | Code blocks |\r\n| `createCommandExtension({ char: \"++\" })` | `++` | `#ai-command` | Code blocks |\r\n\r\nBoth use `createSuggestionItems`, `renderItems`, and `<EditorCommand>` + `<EditorCommandItem>` children tunneled from `EditorRoot`.\r\n\r\nKeyboard while a menu is open: **↑**, **↓**, **Enter** to navigate/select; **Esc** closes the popup.\r\n\r\n### Markdown helpers\r\n\r\n| Function | Returns |\r\n|----------|---------|\r\n| `getSelectionMarkdown(editor)` | Markdown for the current selection |\r\n| `getDocumentMarkdown(editor)` | Full document markdown |\r\n| `getPrevTextMarkdown(editor, pos)` | Markdown from doc start through `pos` (used by **Continue writing**) |\r\n\r\nRequires `MarkdownExtension` in your extension list.\r\n\r\n### `MarkdownReader`\r\n\r\nRead-only, sanitized markdown renderer for publish/preview views. Unlike the WYSIWYG editor, this path is safe for **untrusted** markdown input.\r\n\r\n```tsx\r\nimport { MarkdownReader } from \"temper-md\";\r\n\r\n<MarkdownReader\r\n  markdown={\"# Hello\\n\\n| A | B |\\n|---|---|\\n| 1 | 2 |\"}\r\n  className=\"prose\"\r\n/>\r\n```\r\n\r\n| Prop | Type | Effect |\r\n|------|------|--------|\r\n| `markdown` | `string` | Markdown source to render |\r\n| `className` | `string` | Wrapper div classes |\r\n| `components` | `Components` | Optional `react-markdown` element overrides |\r\n\r\n**Pipeline:** `remark-gfm` (tables, task lists, strikethrough) → `rehype-sanitize` (strip raw HTML) → safe link/image components.\r\n\r\n**URL safety:** `safeHref` and `safeSrc` allow `http`, `https`, `mailto`, relative paths, and `data:image/` for images. Other protocols (e.g. `javascript:`) are dropped.\r\n\r\n**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.\r\n\r\nThe reference demo wraps this as `TemperReader` in [`apps/web`](../../apps/web) with Tailwind prose styling and optional `storageKey` hydration from `localStorage`.\r\n\r\n## User interactions\r\n\r\nBelow 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.\r\n\r\n### Top toolbar (sticky)\r\n\r\nShown via `slotBefore={<TopEditorToolbar … />}`. Controls apply to the current selection or block at the cursor.\r\n\r\n| Control | Action | Tiptap command |\r\n|---------|--------|----------------|\r\n| **Block type** dropdown | Text, H1–H3, to-do, bullet/numbered list, quote, code block | `clearNodes()`, `toggleHeading`, `toggleTaskList`, `toggleBulletList`, `toggleOrderedList`, `toggleBlockquote`, `toggleCodeBlock` |\r\n| **Link** (Lucide link icon) | Set/unset hyperlink URL | `setLink({ href })`, `unsetLink()` |\r\n| **Math** (Σ) | Wrap selection in LaTeX, or unset if already math | `setLatex({ latex })`, `unsetLatex()` |\r\n| **Bold / Italic / Underline / Strike / Code** | Toggle inline marks | `toggleBold`, `toggleItalic`, `toggleUnderline`, `toggleStrike`, `toggleCode` |\r\n| **Color** | Text color + highlight/background | `setColor`, `unsetColor`, `setHighlight`, `unsetHighlight` |\r\n| **AI** (sparkles) | Opens Generate or Format submenus (see [AI features](#ai-features)) | Opens AI panel via command bridge |\r\n| **Save** (optional) | Calls your `onSave` callback | Only rendered when `onSave` prop is provided |\r\n\r\n### Selection bubble menu\r\n\r\nSame controls as the toolbar (block type, link, math, text marks, color, AI), but appears above selected text. Configure with `<EditorBubble>` inside `EditorContent`.\r\n\r\nClosing the bubble (click away or hide) also closes any open AI panel and clears AI highlight.\r\n\r\n### Slash menu (`/`)\r\n\r\nType `/` at the start of a line (not inside a code block) to open the block insert menu.\r\n\r\n| Item | Effect |\r\n|------|--------|\r\n| Text | Plain paragraph |\r\n| To-do List | Task list with checkboxes |\r\n| Heading 1 / 2 / 3 | Section headings |\r\n| Bullet List / Numbered List | Lists |\r\n| Quote | Blockquote |\r\n| Code | Fenced code block |\r\n| Table | 3×3 table with header row |\r\n| Image | File picker → uploads via your `uploadFn` |\r\n| Youtube | Prompt for URL → embed |\r\n| Twitter | Prompt for X/Twitter URL → embed |\r\n\r\nFilter items by typing after `/`. Select with click or keyboard.\r\n\r\nInside a table, the slash menu shows row/column edit actions instead of block inserts. Empty table cells suppress the `Press '/' for commands` placeholder.\r\n\r\n### AI inline menu (`++`)\r\n\r\nType `++` (not inside a code block) for quick AI actions without opening the full panel first.\r\n\r\n| Group | Options |\r\n|-------|---------|\r\n| **Generate** | Improve writing, Fix grammar, Make shorter, Make longer, Continue writing |\r\n| **Format** | Restructure sections, Fix heading hierarchy, Normalize lists, Clean markdown syntax |\r\n\r\nSelecting an item removes the trigger text and opens the matching AI panel with that option pre-run.\r\n\r\n**Text source rules:**\r\n\r\n- **Continue writing** — markdown from document start through cursor (`getPrevTextMarkdown`)\r\n- **All other generate options** — current selection markdown (`getSelectionMarkdown`)\r\n- **Format options** — selection markdown, or full document if nothing is selected\r\n\r\n### AI features\r\n\r\nThree entry points share the same behavior:\r\n\r\n1. **Toolbar / bubble AI menu** — sparkles button → Generate or Format submenu\r\n2. **`++` inline menu** — runs immediately\r\n3. **Custom prompt** — \"Custom prompt…\" / \"Custom format…\" opens the panel for free-text `zap` commands\r\n\r\n#### Generate panel\r\n\r\n| Control | Behavior |\r\n|---------|----------|\r\n| Preset commands | Improve, Fix grammar, Shorter, Longer, Continue (same as `++`) |\r\n| Custom input + submit | Sends `option: \"zap\"` with your `command` string |\r\n| After streaming completes | **Replace selection**, **Insert below**, or **Discard** |\r\n\r\nUses `generateEndpoint`. Highlights affected text while the panel is open (`addAIHighlight` / `removeAIHighlight`).\r\n\r\n#### Format panel\r\n\r\n| Control | Behavior |\r\n|---------|----------|\r\n| Preset commands | Structure, Headings, Lists, Cleanup |\r\n| Custom input + submit | Sends `option: \"zap\"` with your format instruction |\r\n| After streaming completes | **Replace selection**, **Insert below**, or **Discard** |\r\n\r\nUses `formatEndpoint`. Operates on selection markdown, falling back to the full document.\r\n\r\n### Keyboard shortcuts\r\n\r\n| Shortcut | Effect |\r\n|----------|--------|\r\n| `Cmd/Ctrl + A` (first press) | Select all text within the current block/node |\r\n| `Cmd/Ctrl + A` (second press) | Normal select-all |\r\n| `↑` / `↓` / `Enter` | Navigate slash (`/`) or AI (`++`) suggestion menus |\r\n| `Esc` | Close inline suggestion popup or AI panel |\r\n\r\n### Paste, drop, and images\r\n\r\n| Interaction | Hook | Effect |\r\n|-------------|------|--------|\r\n| Paste image | `editorProps.handlePaste` + `handleImagePaste` | Uploads and inserts image at cursor |\r\n| Drop image | `editorProps.handleDrop` + `handleImageDrop` | Same, when not moving existing content |\r\n| Resize | `<ImageResizer />` in `slotAfter` | Drag handles on selected images |\r\n\r\nProvide your own `uploadFn` (see demo `createImageUpload` / Vercel Blob route).\r\n\r\n### Drag handle\r\n\r\n`GlobalDragHandle` extension (included in demo defaults) adds a block drag handle for reordering. Tables are included in drag-handle custom nodes.\r\n\r\n## Reference demo: `TemperEditor`\r\n\r\nThe demo wraps the headless primitives with Tailwind UI. Import from your app copy or mirror the pattern in `apps/web/components/temper-editor.tsx`.\r\n\r\n```tsx\r\nimport TemperEditor from \"@/components/temper-editor\";\r\n\r\n<TemperEditor\r\n  generateEndpoint=\"https://api.example.com/ai/generate\"\r\n  formatEndpoint=\"https://api.example.com/ai/format\"\r\n  aiHeaders={{ Authorization: \"Bearer …\" }}\r\n  initialContent={EMPTY_EDITOR_CONTENT}\r\n  storageKey=\"my-doc\"\r\n  onSave={() => persistToApi()}\r\n  className=\"rounded-lg border\"\r\n/>\r\n```\r\n\r\n| Prop | Type | Effect |\r\n|------|------|--------|\r\n| `generateEndpoint` | `string` | **Required.** POST URL for AI writing assistance |\r\n| `formatEndpoint` | `string` | **Required.** POST URL for AI markdown formatting |\r\n| `aiHeaders` | `Record<string, string>` | Optional headers for AI fetches |\r\n| `initialContent` | `JSONContent` | Starting document. Defaults to `EMPTY_EDITOR_CONTENT`. |\r\n| `storageKey` | `string` | Enables localStorage persistence (JSON + HTML + markdown, 500 ms debounce). Shows save status and word count. |\r\n| `onSave` | `() => void` | When provided, renders a **Save** icon button in the toolbar that calls this function. |\r\n| `className` | `string` | Outer editor container classes. |\r\n\r\nThe demo app wires these from its own `.env` via `getAIEndpointConfig()` — that helper is not part of the component API.\r\n\r\n## Reference demo: `TemperReader`\r\n\r\nThe demo provides a styled reader at `apps/web/components/temper-reader.tsx` and a `/read` route.\r\n\r\n```tsx\r\nimport TemperReader from \"@/components/temper-reader\";\r\n\r\n<TemperReader storageKey=\"my-doc\" />\r\n```\r\n\r\nWhen `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`.\r\n\r\n## AI endpoints\r\n\r\nBoth endpoints accept `POST` JSON:\r\n\r\n```json\r\n{\r\n  \"prompt\": \"markdown or selected text\",\r\n  \"option\": \"continue | improve | shorter | longer | fix | zap | structure | headings | lists | cleanup\",\r\n  \"command\": \"optional free-text command for zap\",\r\n  \"mode\": \"generate | format\"\r\n}\r\n```\r\n\r\nResponses should be Vercel AI SDK data streams (`toDataStreamResponse()`), or any compatible streaming completion consumed by `useCompletion`.\r\n\r\n| Option | Mode | Purpose |\r\n|--------|------|---------|\r\n| `continue` | generate | Extend text from prior context |\r\n| `improve` | generate | Polish clarity and flow |\r\n| `fix` | generate | Grammar and spelling |\r\n| `shorter` | generate | Condense |\r\n| `longer` | generate | Expand |\r\n| `zap` | generate / format | Free-text instruction via `command` |\r\n| `structure` | format | Improve document flow |\r\n| `headings` | format | Normalize heading levels |\r\n| `lists` | format | Clean up list markdown |\r\n| `cleanup` | format | Fix spacing and syntax |\r\n\r\nConfigure per environment:\r\n\r\n- `generateEndpoint` / `formatEndpoint` on `EditorRoot`, or\r\n- Demo env vars: `AI_HARNESS`, `AI_GENERATE_ENDPOINT`, `AI_FORMAT_ENDPOINT`, `AI_ENDPOINT_AUTHORIZATION`\r\n\r\n## Default content\r\n\r\nUse `EMPTY_EDITOR_CONTENT` when no document is provided. Pass `initialContent` to hydrate from props or your persistence layer.\r\n\r\n## Exports\r\n\r\n- **Editor primitives:** `EditorRoot`, `EditorContent`, `EditorBubble`, `EditorBubbleItem`, `EditorToolbar`, `EditorCommand`, `EditorCommandItem`, `EditorCommandList`, `EditorCommandEmpty`, `useEditor`\r\n- **Reader:** `MarkdownReader`, `MarkdownReaderProps`\r\n- **Slash / command extensions:** `Command`, `createCommandExtension`, `createSuggestionItems`, `renderItems`, `handleCommandNavigation`\r\n- **Markdown:** `getSelectionMarkdown`, `getDocumentMarkdown`, `getPrevTextMarkdown`, `MarkdownExtension`\r\n- **URL safety:** `safeHref`, `safeSrc`\r\n- **AI:** `AIConfigProvider`, `useAIConfig`, `AIHighlight`, `addAIHighlight`, `removeAIHighlight`, request types\r\n- **Media:** `UploadImagesPlugin`, `handleImagePaste`, `handleImageDrop`, `ImageResizer`, `UpdatedImage`\r\n- **Tiptap extensions:** `StarterKit`, `TiptapLink`, `TiptapUnderline`, `Mathematics`, `Youtube`, `Twitter`, `Table`, `TableRow`, `TableCell`, `TableHeader`, and more — see package exports\r\n\r\nSee [`apps/web`](../../apps/web) for the full Tailwind reference implementation with toolbar, bubble menu, slash commands, AI panels, and reader view.\r\n","readmeFilename":"README.md","_rev":"1-21080ddab0d7d4b8de2cb05aea1e1fe9"}