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 +21 -0
- package/README.md +452 -0
- package/dist/index.cjs +2665 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +598 -0
- package/dist/index.d.ts +598 -0
- package/dist/index.js +2615 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +710 -0
- package/package.json +85 -0
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
|