@alsocoder/apna-editor
v0.1.2
Published
A rich text editor React component with toolbar, fullscreen, HTML mode, tables, and optional image upload API.
Maintainers
Readme
@alsocoder/apna-editor
A rich text editor React component built on TipTap with toolbar, fullscreen, HTML mode, tables, and optional image upload API.
Features
- Rich formatting: headings, bold, italic, underline, lists, alignment, links, tables
- Fullscreen editing mode
- Optional Visual / HTML source mode
- Image button only when
imageUploadAPI is provided - Custom media picker (
pick) or direct file upload (upload) - Form-friendly:
readOnly,error,onBlur,namefor native forms - Fully customizable via
className,classNames, and CSS variables - Optional dark theme preset (
dark.css) - Content CSS for rendering saved HTML outside the editor
- Fullscreen helpers for dialog/sheet integration
- No Tailwind required — plain CSS only
Installation
npm install @alsocoder/apna-editorPeer dependencies:
npm install react react-domSetup
Import the stylesheet once in your app entry:
import { ApnaEditor } from "@alsocoder/apna-editor"
import "@alsocoder/apna-editor/styles.css"Optional dark theme:
import "@alsocoder/apna-editor/dark.css"
<div className="apna-editor-dark">
<ApnaEditor ... />
</div>Basic Usage
import { useState } from "react"
import { ApnaEditor } from "@alsocoder/apna-editor"
import "@alsocoder/apna-editor/styles.css"
export function ContentForm() {
const [content, setContent] = useState("")
return (
<ApnaEditor
label="Description"
value={content}
onChange={setContent}
placeholder="Write something…"
/>
)
}Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| label | string | "Content" | Label above editor (used as fullscreen header title) |
| value | string | "" | Controlled HTML value |
| onChange | (html: string) => void | — | Called when content changes |
| className | string | — | Class on field wrapper |
| classNames | ApnaEditorClassNames | — | Per-part class overrides (see below) |
| placeholder | string | "Start writing…" | Empty state placeholder in visual mode |
| minHeight | string | "280px" | Minimum editor body height |
| maxHeight | string | "480px" | Maximum editor body height |
| disabled | boolean | false | Disable all interactions |
| readOnly | boolean | false | View-only — content visible, editing disabled |
| error | string \| Error \| null | — | Validation error state |
| errorText | string | — | Custom error message (overrides error) |
| onBlur | () => void | — | Blur handler for editor / HTML textarea |
| id | string | — | HTML id (linked to label) |
| name | string | — | Hidden input name for native form submit |
| enableHtmlMode | boolean | false | Show Visual / HTML toggle |
| imageUpload | ApnaEditorImageUpload | — | Image API — button hidden without this |
| onNotify | (message) => void | — | Toast handler for clipboard actions |
classNames keys
| Key | Default class |
|-----|---------------|
| field | apna-editor-field |
| label | apna-editor-label |
| editor | apna-editor |
| editorFullscreen | apna-editor-fullscreen |
| body | apna-editor-body |
| scroll | apna-editor-scroll |
| content | apna-editor-content (ProseMirror editable area) |
| htmlTextarea | apna-editor-html |
| footer | apna-editor-footer |
| error | apna-editor-field-error |
Image Upload API
Image button only appears when imageUpload.pick or imageUpload.upload is provided.
Custom media library picker
<ApnaEditor
value={content}
onChange={setContent}
imageUpload={{
pick: () => openMediaLibrary({ folder: "blog" }),
}}
/>Direct file upload
<ApnaEditor
value={content}
onChange={setContent}
imageUpload={{
upload: async (file) => {
const formData = new FormData()
formData.append("file", file)
const res = await fetch("/api/media/upload", { method: "POST", body: formData })
const data = await res.json()
return { url: data.url, alt: file.name }
},
accept: "image/*",
}}
/>HTML Mode
<ApnaEditor
value={content}
onChange={setContent}
enableHtmlMode
/>Form Integration
<form onSubmit={handleSubmit}>
<ApnaEditor
name="description"
id="description"
label="Description"
value={content}
onChange={setContent}
onBlur={() => validate("description")}
error={errors.description}
readOnly={isSubmitted}
/>
</form>react-hook-form Example
import { Controller, useForm } from "react-hook-form"
import { ApnaEditor } from "@alsocoder/apna-editor"
function MyForm() {
const { control, handleSubmit } = useForm()
return (
<form onSubmit={handleSubmit(console.log)}>
<Controller
name="content"
control={control}
render={({ field, fieldState }) => (
<ApnaEditor
label="Content"
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
error={fieldState.error}
errorText={fieldState.error?.message}
/>
)}
/>
<button type="submit">Save</button>
</form>
)
}Notifications
<ApnaEditor
value={content}
onChange={setContent}
onNotify={({ type, title }) => toast[type](title)}
/>Fullscreen in Dialogs
import { preventDismissWhenApnaEditorFullscreen } from "@alsocoder/apna-editor"
<DialogContent onInteractOutside={preventDismissWhenApnaEditorFullscreen}>Theming with CSS Variables
:root {
--apna-editor-primary: #2563eb;
--apna-editor-border: #e5e7eb;
--apna-editor-radius: 0.5rem;
}Dark mode preset
import "@alsocoder/apna-editor/dark.css"
<div className="apna-editor-dark">
<ApnaEditor ... />
</div>Or set data-apna-editor-theme="dark" on a parent element.
Content Styles (tables, quotes, code, etc.)
Editor content styles are injected automatically inside ApnaEditor. To render saved HTML outside the editor:
import "@alsocoder/apna-editor/content.css"
import { APNA_EDITOR_CONTENT_PREVIEW_CLASS } from "@alsocoder/apna-editor"
<div
className={APNA_EDITOR_CONTENT_PREVIEW_CLASS}
dangerouslySetInnerHTML={{ __html: content }}
/>Content element classes
| Class | Element |
|-------|---------|
| apna-editor-table | Table |
| apna-editor-table-header | Table header cell |
| apna-editor-table-cell | Table body cell |
| apna-editor-blockquote | Blockquote |
| apna-editor-code-block | Code block |
| apna-editor-hr | Horizontal rule |
| apna-editor-image | Image |
| apna-editor-link | Link |
Exports
import {
ApnaEditor,
createApnaEditorExtensions,
formatHtml,
APNA_EDITOR_CONTENT_CSS,
APNA_EDITOR_CONTENT_PREVIEW_CLASS,
defaultClassNames,
mergeClassNames,
preventDismissWhenApnaEditorFullscreen,
} from "@alsocoder/apna-editor"Publishing (maintainers)
npm run typecheck
npm run build
npm publish --access publicLicense
MIT © Also Coder
