osameditor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 osameditor authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,452 @@
1
+ # osameditor
2
+
3
+ A **configurable rich text editor** for **React** and **Next.js** (TypeScript or
4
+ JavaScript), built for blog platforms.
5
+
6
+ - 🧩 **Pick exactly the tools you want** β€” want only H2 + H3, bold and a link? Pass
7
+ a list. Every button is opt-in.
8
+ - πŸ–ΌοΈ **Media built in** β€” image upload / by-URL / **your own library**, resize,
9
+ **float text around the image**, caption, alt & title, **link an image**,
10
+ YouTube, Instagram, video (incl. HLS), generic embeds, PDF documents.
11
+ - ☁️ **Storage = OsamStorage or your own API** β€” chunked uploads via
12
+ [**OsamStorage**](https://www.npmjs.com/package/osamstorage), or POST to your
13
+ **own backend / VPS**. Plus a GET/POST **media-library** hook for a reusable
14
+ image list.
15
+ - πŸ–₯️ **Fullscreen**, **HTML source view**, and **Custom HTML/CSS blocks** with a
16
+ built-in sanitizer.
17
+ - πŸ§‘β€πŸ’» **Code blocks** with syntax highlighting + one-click **Copy**.
18
+ - 🎨 **Recolor everything from props** β€” surface, toolbar, buttons β€” or via CSS
19
+ variables. Light/dark aware. Ships `"use client"`, SSR-safe for the Next.js
20
+ App Router.
21
+
22
+ Built on [TipTap](https://tiptap.dev/) / ProseMirror.
23
+
24
+ ---
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ npm install osameditor
30
+ # optional β€” only if you upload through OsamStorage:
31
+ npm install osamstorage
32
+ # optional β€” only if you embed HLS (.m3u8) video, e.g. OsamStorage-processed video:
33
+ npm install hls.js
34
+ ```
35
+
36
+ `react` and `react-dom` (>=17) are peer dependencies.
37
+
38
+ ---
39
+
40
+ ## Quick start
41
+
42
+ ### Next.js (App Router)
43
+
44
+ ```tsx
45
+ // app/write/editor.tsx
46
+ "use client";
47
+
48
+ import { useState } from "react";
49
+ import { OsamEditor } from "osameditor";
50
+ import "osameditor/styles.css";
51
+
52
+ export default function Editor() {
53
+ const [html, setHtml] = useState("<p>Hello world</p>");
54
+
55
+ return (
56
+ <OsamEditor
57
+ toolbar="blog"
58
+ defaultValue={html}
59
+ onChange={(next) => setHtml(next)}
60
+ />
61
+ );
62
+ }
63
+ ```
64
+
65
+ ### Plain React (Vite / CRA)
66
+
67
+ Same component, same import. No extra setup β€” just render `<OsamEditor />` inside
68
+ a client component.
69
+
70
+ ---
71
+
72
+ ## Choose what goes in your toolbar
73
+
74
+ Pass `toolbar` an **array of items**, a **preset name**, or `false` (no toolbar).
75
+
76
+ ```tsx
77
+ // Only what you need:
78
+ <OsamEditor toolbar={["h2", "h3", "bold", "italic", "link", "bulletList"]} />
79
+
80
+ // A preset:
81
+ <OsamEditor toolbar="minimal" /> // "full" | "blog" | "basic" | "minimal"
82
+
83
+ // Group with dividers and a right-aligned section:
84
+ <OsamEditor
85
+ toolbar={["headings", "|", "bold", "italic", "spacer", "undo", "redo"]}
86
+ />
87
+ ```
88
+
89
+ **The toolbar drives the schema.** If `image` / `youtube` / `codeBlock` etc. aren't
90
+ in your toolbar (and you didn't enable them explicitly), those node types aren't
91
+ even loaded β€” smaller schema, cleaner output. `"|"` is a divider, `"spacer"`
92
+ pushes the rest to the right.
93
+
94
+ <details>
95
+ <summary><strong>Every toolbar item</strong></summary>
96
+
97
+ | Group | Items |
98
+ |---|---|
99
+ | **Formatting** | `bold` `italic` `underline` `strike` `code` `color` `highlight` `clearFormatting` |
100
+ | **Headings** | `headings` (dropdown) Β· or individual `h1` `h2` `h3` `h4` `h5` `h6` Β· `paragraph` |
101
+ | **Alignment** | `alignLeft` `alignCenter` `alignRight` `alignJustify` `hardBreak` |
102
+ | **Lists** | `bulletList` `orderedList` `taskList` `indent` `outdent` |
103
+ | **Links** | `link` `unlink` |
104
+ | **Quotes & code** | `blockquote` `codeBlock` |
105
+ | **Media** | `image` `imageUrl` `mediaLibrary` `youtube` `instagram` `video` `embed` `pdf` |
106
+ | **View / advanced** | `fullscreen` `source` (HTML view) `customHtml` (HTML block) |
107
+ | **Misc** | `horizontalRule` `undo` `redo` |
108
+ | **Layout** | `\|` (divider) `spacer` (push rest right) |
109
+
110
+ </details>
111
+
112
+ ### Restrict heading levels
113
+
114
+ ```tsx
115
+ <OsamEditor toolbar={["headings", "bold"]} heading={{ levels: [2, 3] }} />
116
+ ```
117
+
118
+ Using `h2` / `h3` items in the toolbar does this automatically:
119
+
120
+ ```tsx
121
+ <OsamEditor toolbar={["h2", "h3", "bold"]} /> // schema allows only H2 & H3
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Storage / uploads
127
+
128
+ The editor never talks to a backend directly β€” it calls one **`UploadHandler`**.
129
+ Three ways to provide one:
130
+
131
+ ### 1. OsamStorage (images β†’ WebP, video β†’ HLS, PDF)
132
+
133
+ ```tsx
134
+ import { OsamEditor, createOsamStorageUploader } from "osameditor";
135
+
136
+ const storage = createOsamStorageUploader({
137
+ // Same contract as the osamstorage SDK β€” point at your token route.
138
+ getToken: async () => (await fetch("/api/osam-token", { method: "POST" }).then(r => r.json())).token,
139
+ resolveUrl: (data) => data?.url ?? data?.fileUrl, // map the merge response β†’ URL
140
+ resolvePoster: (data) => data?.poster, // optional (video thumbnail)
141
+ });
142
+
143
+ <OsamEditor toolbar="blog" upload={{ handler: storage }} />
144
+ ```
145
+
146
+ Your token route keeps `MAIN_TOKEN` on the server β€” see
147
+ [`example/nextjs-api-token-route.ts`](example/nextjs-api-token-route.ts).
148
+
149
+ ### 2. Your own VPS / server
150
+
151
+ ```tsx
152
+ import { OsamEditor, createVpsUploader } from "osameditor";
153
+
154
+ const uploader = createVpsUploader({
155
+ endpoint: "/api/upload", // multipart/form-data POST, field "file"
156
+ headers: () => ({ Authorization: `Bearer ${token}` }),
157
+ resolveUrl: (res) => res.url, // shape of YOUR JSON response
158
+ });
159
+
160
+ <OsamEditor toolbar="blog" upload={{ handler: uploader, maxSize: 25 * 1024 * 1024 }} />
161
+ ```
162
+
163
+ Reports real upload progress (`XHR`) and supports cancellation.
164
+
165
+ ### 3. Any custom handler
166
+
167
+ ```tsx
168
+ const uploader: UploadHandler = async (file, { kind, onProgress, signal }) => {
169
+ const url = await myS3PresignedUpload(file, onProgress, signal);
170
+ return { url, kind };
171
+ };
172
+ ```
173
+
174
+ ```ts
175
+ type UploadHandler = (
176
+ file: File,
177
+ ctx: { kind: "image" | "video" | "pdf"; onProgress?: (pct: number) => void; signal?: AbortSignal },
178
+ ) => Promise<{ url: string; kind?: "image" | "video" | "pdf"; poster?: string; width?: number; height?: number }>;
179
+ ```
180
+
181
+ > **No handler?** Upload buttons disappear, but `imageUrl` / `youtube` / `embed`
182
+ > (by-URL) still work.
183
+
184
+ ---
185
+
186
+ ## Feature configuration
187
+
188
+ ```tsx
189
+ <OsamEditor
190
+ toolbar="full"
191
+ placeholder="Write something…"
192
+ dir="ltr"
193
+
194
+ heading={{ levels: [1, 2, 3] }}
195
+
196
+ link={{
197
+ allowTargetBlank: true, // "open in new tab" checkbox
198
+ allowRelAttributes: true, // nofollow / sponsored checkboxes
199
+ defaultRel: "noopener noreferrer nofollow",
200
+ protocols: ["http", "https", "mailto", "tel"],
201
+ autolink: true,
202
+ }}
203
+
204
+ color={{ colors: ["#111", "#e11", "#1a1"] }}
205
+ highlight={{ highlights: ["#fff3a3", "#a5d8ff"] }}
206
+
207
+ image={{
208
+ resizable: true, // drag-resize (10–100% of the row)
209
+ caption: true, // <figcaption>
210
+ align: true, // left / right FLOAT (text wraps), center = block
211
+ link: true, // wrap the image in <a>
212
+ accept: "image/*",
213
+ maxSize: 8 * 1024 * 1024,
214
+ }}
215
+
216
+ upload={{ handler: storage, video: true, pdf: true }}
217
+
218
+ embed={{
219
+ allowedHosts: ["youtube.com", "vimeo.com", "codepen.io"], // "*" = any
220
+ defaultRatio: "16/9",
221
+ }}
222
+
223
+ codeBlock={{ copyButton: true, defaultLanguage: "tsx" }} // or just `codeBlock`
224
+ taskList
225
+ horizontalRule
226
+
227
+ branding // "powered by osamtech.com" credit (on by default)
228
+ defaultFullscreen={false}
229
+ />
230
+ ```
231
+
232
+ Set any feature to `false` to force it off; omit it and it's inferred from the
233
+ toolbar.
234
+
235
+ ### Image + text layout
236
+
237
+ `left` / `right` alignment **float** the image, so when the reader shrinks it to
238
+ 10–20% the body text flows into the freed space:
239
+
240
+ ```
241
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” Your paragraph text wraps around the floated
242
+ β”‚ img β”‚ image and fills the rest of the row instead of
243
+ β”‚ (20%) β”‚ leaving it blank. Pick "center" for a normal
244
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ full-width block with text above and below.
245
+ ```
246
+
247
+ Toolbar controls appear on the selected image: float ⇀ / ⇔ / β‡₯, size
248
+ (25/50/75/100%), reset, and πŸ”— to link it.
249
+
250
+ ### Image library (your own GET/POST API)
251
+
252
+ Give the editor your own endpoints and users get a **"Library" tab** of their
253
+ past uploads:
254
+
255
+ ```tsx
256
+ <OsamEditor
257
+ toolbar={["image", "mediaLibrary", /* … */]}
258
+ upload={{ handler: storage }}
259
+ mediaLibrary={{
260
+ list: "/api/my-images", // GET β†’ ["https://…", …] or [{ url, name, thumbnail }]
261
+ save: "/api/my-images", // POST { url } after each upload
262
+ headers: () => ({ Authorization: `Bearer ${token}` }),
263
+ // resolveList: (res) => res.data, // if your GET shape differs
264
+ }}
265
+ />
266
+ ```
267
+
268
+ `list` / `save` can also be functions (`() => Promise<string[]>` / `(url) => …`).
269
+
270
+ ### HTML / inline CSS
271
+
272
+ ```tsx
273
+ <OsamEditor
274
+ html={{
275
+ sourceView: true, // toolbar "</>" toggles the whole post to an HTML textarea
276
+ customBlock: true, // insert discrete "Custom HTML/CSS" blocks
277
+ styleAttributes: true, // keep style="" on paragraphs / headings / quotes
278
+ classAttributes: true, // keep class / id
279
+ allowStyleTags: false, // <style> blocks (<script> is ALWAYS stripped)
280
+ }}
281
+ />
282
+ // shorthand: html (all of the above with defaults)
283
+ ```
284
+
285
+ A built-in sanitizer removes `<script>`, inline `on*` handlers and
286
+ `javascript:` URLs on every source-view apply, custom-block render and paste.
287
+
288
+ ### Fullscreen
289
+
290
+ Add `"fullscreen"` to the toolbar (it's in the `full` / `blog` presets). The
291
+ editor goes `position: fixed` and fills the entire viewport β€” a 32β€³ screen gets
292
+ a 32β€³ editor. `Esc` exits. Control it with `fullscreen` / `onFullscreenChange`
293
+ if you want.
294
+
295
+ ### Localization
296
+
297
+ ```tsx
298
+ <OsamEditor labels={{ bold: "Gras", "dialog.insert": "InsΓ©rer", link: "Lien" }} />
299
+ ```
300
+
301
+ ---
302
+
303
+ ## Controlled vs uncontrolled
304
+
305
+ ```tsx
306
+ // Uncontrolled (recommended for big documents)
307
+ <OsamEditor defaultValue={initialHtml} onChange={(html, { json }) => save(json)} />
308
+
309
+ // Controlled
310
+ <OsamEditor value={html} onChange={setHtml} />
311
+ ```
312
+
313
+ `onChange(html, { json, editor })` β€” `html` for storage/rendering, `json` (TipTap
314
+ doc) for a structured store.
315
+
316
+ ---
317
+
318
+ ## Rendering saved content on your site
319
+
320
+ ```tsx
321
+ import { OsamContent } from "osameditor";
322
+ import "osameditor/styles.css";
323
+
324
+ export default function Post({ html }: { html: string }) {
325
+ return <OsamContent html={html} />; // same look as the editor
326
+ }
327
+ ```
328
+
329
+ `OsamContent` also upgrades `.m3u8` videos with `hls.js` (if installed) outside
330
+ Safari. Pass `hls={false}` to skip.
331
+
332
+ ---
333
+
334
+ ## Advanced β€” build your own UI
335
+
336
+ ```tsx
337
+ import { useOsamEditor, Toolbar } from "osameditor";
338
+ import { EditorContent } from "@tiptap/react";
339
+
340
+ function MyEditor() {
341
+ const { editor, config } = useOsamEditor({
342
+ toolbar: ["bold", "italic", "h2"],
343
+ content: "<p>hi</p>",
344
+ onUpdate: ({ html }) => console.log(html),
345
+ });
346
+
347
+ return (
348
+ <>
349
+ <MyFloatingMenu editor={editor} />
350
+ <Toolbar editor={editor} config={config} />
351
+ <EditorContent editor={editor} />
352
+ </>
353
+ );
354
+ }
355
+ ```
356
+
357
+ Add raw TipTap extensions:
358
+
359
+ ```tsx
360
+ import Mention from "@tiptap/extension-mention";
361
+
362
+ <OsamEditor extensions={(base) => [...base, Mention.configure({ /* … */ })]} />
363
+ ```
364
+
365
+ `ResizableImage`, `Embed`, `CodeBlock`, `CustomHtml`, `PreserveAttributes`,
366
+ `resolveEmbed`, `buildExtensions`, `resolveConfig`, `sanitizeHtml`,
367
+ `themeToStyle`, `loadMediaLibrary` are also exported.
368
+
369
+ ---
370
+
371
+ ## Props
372
+
373
+ | Prop | Type | Default |
374
+ |---|---|---|
375
+ | `toolbar` | `ToolbarItem[] \| "full" \| "blog" \| "basic" \| "minimal" \| false` | `"full"` |
376
+ | `defaultValue` / `value` | `string \| JSONContent` | `""` |
377
+ | `onChange` | `(html, { json, editor }) => void` | β€” |
378
+ | `onReady` | `(editor) => void` | β€” |
379
+ | `editable` | `boolean` | `true` |
380
+ | `autofocus` | `boolean \| "start" \| "end" \| number` | `false` |
381
+ | `placeholder` | `string` | `"Write something…"` |
382
+ | `minHeight` | `number \| string` | `260` |
383
+ | `toolbarPosition` | `"top" \| "bottom" \| "none"` | `"top"` |
384
+ | `stickyToolbar` | `boolean` | `false` |
385
+ | `fullscreen` / `defaultFullscreen` / `onFullscreenChange` | `boolean` / `boolean` / `(b) => void` | `false` |
386
+ | `theme` | `OsamEditorTheme` (surface / toolbar / button colors) | β€” |
387
+ | `branding` | `boolean` β€” show "powered by osamtech.com" | `true` |
388
+ | `mediaLibrary` | `{ list, save?, headers?, resolveList? }` | β€” |
389
+ | `html` | `boolean \| { sourceView, customBlock, styleAttributes, classAttributes, allowStyleTags }` | inferred |
390
+ | `heading` `link` `color` `highlight` `image` `embed` `upload` `codeBlock` `taskList` `blockquote` `horizontalRule` `textAlign` | see [Feature configuration](#feature-configuration) | inferred |
391
+ | `extensions` | `AnyExtension[] \| (base) => AnyExtension[]` | β€” |
392
+ | `starterKit` | `object` | `{}` |
393
+ | `labels` | `Record<string, string>` | β€” |
394
+ | `dir` | `"ltr" \| "rtl"` | `"ltr"` |
395
+ | `className` / `style` | β€” | β€” |
396
+
397
+ ---
398
+
399
+ ## Theming
400
+
401
+ **From props** β€” every editor instance can be recolored (users can pick their
402
+ own colors):
403
+
404
+ ```tsx
405
+ <OsamEditor
406
+ theme={{
407
+ background: "#ffffff",
408
+ foreground: "#1a1a1a",
409
+ accent: "#7c3aed",
410
+ border: "#e5e7eb",
411
+ radius: 12,
412
+ fontFamily: "'Inter', sans-serif",
413
+ // toolbar + buttons
414
+ toolbarBackground: "#faf5ff",
415
+ buttonColor: "#4b5563",
416
+ buttonHoverBackground: "#f3e8ff",
417
+ buttonActiveBackground: "#ede9fe",
418
+ buttonActiveColor: "#6d28d9",
419
+ contentBackground: "#ffffff",
420
+ codeBackground: "#f6f8fa",
421
+ }}
422
+ />
423
+ ```
424
+
425
+ **From CSS** β€” the same knobs are CSS variables:
426
+
427
+ ```css
428
+ .osam-editor {
429
+ --osam-accent: #7c3aed;
430
+ --osam-toolbar-bg: #faf5ff;
431
+ --osam-btn-fg: #4b5563;
432
+ --osam-radius: 12px;
433
+ }
434
+ /* force a theme regardless of system: */
435
+ .osam-editor[data-theme="dark"] { /* … */ }
436
+ ```
437
+
438
+ ---
439
+
440
+ ## Notes
441
+
442
+ - **Next.js:** the package is a client module. Import `<OsamEditor />` from a
443
+ `"use client"` component (or `next/dynamic` with `ssr: false`). Server rendering
444
+ is safe; it just mounts empty and hydrates.
445
+ - **Output HTML** uses semantic markup: `<figure><img><figcaption>` for images,
446
+ `<div data-osam-embed><iframe|video|object></div>` for media β€” renders fine
447
+ without any JS via `OsamContent` or a plain container.
448
+ - Only images, video and PDF are accepted for upload (matches OsamStorage).
449
+
450
+ ## License
451
+
452
+ MIT