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/dist/index.cjs ADDED
@@ -0,0 +1,2665 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var React10 = require('react');
5
+ var react = require('@tiptap/react');
6
+ var StarterKit = require('@tiptap/starter-kit');
7
+ var extensionTextStyle = require('@tiptap/extension-text-style');
8
+ var Highlight = require('@tiptap/extension-highlight');
9
+ var TextAlign = require('@tiptap/extension-text-align');
10
+ var extensionTaskList = require('@tiptap/extension-task-list');
11
+ var extensionTaskItem = require('@tiptap/extension-task-item');
12
+ var Placeholder = require('@tiptap/extension-placeholder');
13
+ var lowlight = require('lowlight');
14
+ var Image = require('@tiptap/extension-image');
15
+ var core = require('@tiptap/core');
16
+ var jsxRuntime = require('react/jsx-runtime');
17
+ var extensionCodeBlockLowlight = require('@tiptap/extension-code-block-lowlight');
18
+
19
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
20
+
21
+ function _interopNamespace(e) {
22
+ if (e && e.__esModule) return e;
23
+ var n = Object.create(null);
24
+ if (e) {
25
+ Object.keys(e).forEach(function (k) {
26
+ if (k !== 'default') {
27
+ var d = Object.getOwnPropertyDescriptor(e, k);
28
+ Object.defineProperty(n, k, d.get ? d : {
29
+ enumerable: true,
30
+ get: function () { return e[k]; }
31
+ });
32
+ }
33
+ });
34
+ }
35
+ n.default = e;
36
+ return Object.freeze(n);
37
+ }
38
+
39
+ var React10__namespace = /*#__PURE__*/_interopNamespace(React10);
40
+ var StarterKit__default = /*#__PURE__*/_interopDefault(StarterKit);
41
+ var Highlight__default = /*#__PURE__*/_interopDefault(Highlight);
42
+ var TextAlign__default = /*#__PURE__*/_interopDefault(TextAlign);
43
+ var Placeholder__default = /*#__PURE__*/_interopDefault(Placeholder);
44
+ var Image__default = /*#__PURE__*/_interopDefault(Image);
45
+
46
+ // src/OsamEditor.tsx
47
+
48
+ // src/presets.ts
49
+ var TOOLBAR_PRESETS = {
50
+ full: [
51
+ "undo",
52
+ "redo",
53
+ "|",
54
+ "headings",
55
+ "|",
56
+ "bold",
57
+ "italic",
58
+ "underline",
59
+ "strike",
60
+ "code",
61
+ "|",
62
+ "color",
63
+ "highlight",
64
+ "clearFormatting",
65
+ "|",
66
+ "alignLeft",
67
+ "alignCenter",
68
+ "alignRight",
69
+ "alignJustify",
70
+ "|",
71
+ "bulletList",
72
+ "orderedList",
73
+ "taskList",
74
+ "outdent",
75
+ "indent",
76
+ "|",
77
+ "link",
78
+ "unlink",
79
+ "|",
80
+ "blockquote",
81
+ "codeBlock",
82
+ "horizontalRule",
83
+ "|",
84
+ "image",
85
+ "imageUrl",
86
+ "mediaLibrary",
87
+ "youtube",
88
+ "instagram",
89
+ "video",
90
+ "embed",
91
+ "pdf",
92
+ "|",
93
+ "customHtml",
94
+ "source",
95
+ "spacer",
96
+ "fullscreen"
97
+ ],
98
+ blog: [
99
+ "headings",
100
+ "|",
101
+ "bold",
102
+ "italic",
103
+ "underline",
104
+ "strike",
105
+ "|",
106
+ "highlight",
107
+ "|",
108
+ "bulletList",
109
+ "orderedList",
110
+ "taskList",
111
+ "|",
112
+ "link",
113
+ "blockquote",
114
+ "codeBlock",
115
+ "|",
116
+ "image",
117
+ "mediaLibrary",
118
+ "youtube",
119
+ "instagram",
120
+ "embed",
121
+ "pdf",
122
+ "|",
123
+ "undo",
124
+ "redo",
125
+ "spacer",
126
+ "source",
127
+ "fullscreen"
128
+ ],
129
+ basic: [
130
+ "bold",
131
+ "italic",
132
+ "underline",
133
+ "strike",
134
+ "|",
135
+ "h2",
136
+ "h3",
137
+ "|",
138
+ "bulletList",
139
+ "orderedList",
140
+ "|",
141
+ "link",
142
+ "blockquote",
143
+ "|",
144
+ "image",
145
+ "|",
146
+ "undo",
147
+ "redo"
148
+ ],
149
+ minimal: ["bold", "italic", "link", "|", "bulletList", "orderedList"]
150
+ };
151
+ function resolveToolbar(toolbar) {
152
+ if (toolbar === false) return false;
153
+ if (toolbar === void 0) return TOOLBAR_PRESETS.full;
154
+ if (typeof toolbar === "string") return TOOLBAR_PRESETS[toolbar] ?? TOOLBAR_PRESETS.full;
155
+ return toolbar;
156
+ }
157
+ function tidyToolbar(items) {
158
+ const out = [];
159
+ for (const item of items) {
160
+ if (item === "|" && (out.length === 0 || out[out.length - 1] === "|")) continue;
161
+ out.push(item);
162
+ }
163
+ while (out.length && out[out.length - 1] === "|") out.pop();
164
+ return out;
165
+ }
166
+
167
+ // src/config.ts
168
+ var HEADING_ITEMS = {
169
+ h1: 1,
170
+ h2: 2,
171
+ h3: 3,
172
+ h4: 4,
173
+ h5: 5,
174
+ h6: 6
175
+ };
176
+ var DEFAULT_COLORS = [
177
+ "#000000",
178
+ "#434343",
179
+ "#666666",
180
+ "#999999",
181
+ "#b7b7b7",
182
+ "#cccccc",
183
+ "#ffffff",
184
+ "#e60000",
185
+ "#ff9900",
186
+ "#ffff00",
187
+ "#008a00",
188
+ "#0066cc",
189
+ "#9933ff",
190
+ "#ff66cc"
191
+ ];
192
+ var DEFAULT_HIGHLIGHTS = [
193
+ "#fff3a3",
194
+ "#ffd8a8",
195
+ "#ffc9c9",
196
+ "#d3f9d8",
197
+ "#a5d8ff",
198
+ "#d0bfff",
199
+ "#fcc2d7"
200
+ ];
201
+ var DEFAULT_EMBED_HOSTS = [
202
+ "youtube.com",
203
+ "youtu.be",
204
+ "youtube-nocookie.com",
205
+ "vimeo.com",
206
+ "player.vimeo.com",
207
+ "instagram.com",
208
+ "twitter.com",
209
+ "x.com",
210
+ "codepen.io",
211
+ "codesandbox.io",
212
+ "open.spotify.com",
213
+ "soundcloud.com",
214
+ "google.com",
215
+ "maps.google.com",
216
+ "loom.com",
217
+ "figma.com",
218
+ "gist.github.com"
219
+ ];
220
+ var DEFAULT_LABELS = {
221
+ bold: "Bold",
222
+ italic: "Italic",
223
+ underline: "Underline",
224
+ strike: "Strikethrough",
225
+ code: "Inline code",
226
+ color: "Text color",
227
+ highlight: "Highlight",
228
+ clearFormatting: "Clear formatting",
229
+ paragraph: "Paragraph",
230
+ headings: "Heading",
231
+ h1: "Heading 1",
232
+ h2: "Heading 2",
233
+ h3: "Heading 3",
234
+ h4: "Heading 4",
235
+ h5: "Heading 5",
236
+ h6: "Heading 6",
237
+ alignLeft: "Align left",
238
+ alignCenter: "Align center",
239
+ alignRight: "Align right",
240
+ alignJustify: "Justify",
241
+ hardBreak: "Line break",
242
+ bulletList: "Bullet list",
243
+ orderedList: "Numbered list",
244
+ taskList: "Checklist",
245
+ indent: "Indent / nest",
246
+ outdent: "Outdent",
247
+ link: "Link",
248
+ unlink: "Remove link",
249
+ blockquote: "Blockquote",
250
+ codeBlock: "Code block",
251
+ image: "Upload image",
252
+ imageUrl: "Image by URL",
253
+ youtube: "YouTube",
254
+ instagram: "Instagram",
255
+ video: "Video",
256
+ embed: "Embed",
257
+ pdf: "PDF / document",
258
+ horizontalRule: "Divider",
259
+ undo: "Undo",
260
+ redo: "Redo",
261
+ "dialog.url": "URL",
262
+ "dialog.insert": "Insert",
263
+ "dialog.cancel": "Cancel",
264
+ "dialog.upload": "Upload a file",
265
+ "dialog.or": "or",
266
+ "dialog.alt": "Alt text",
267
+ "dialog.title": "Title",
268
+ "dialog.caption": "Caption",
269
+ "dialog.openInNewTab": "Open in new tab",
270
+ "dialog.nofollow": "nofollow",
271
+ "dialog.sponsored": "sponsored",
272
+ "dialog.linkText": "Text",
273
+ "dialog.uploading": "Uploading\u2026",
274
+ "dialog.ratio": "Aspect ratio",
275
+ "dialog.library": "Library",
276
+ "dialog.linkImage": "Link this image",
277
+ "error.tooLarge": "That file is too large.",
278
+ "error.uploadFailed": "Upload failed.",
279
+ "error.badHost": "That URL isn't from an allowed site.",
280
+ mediaLibrary: "Media library",
281
+ fullscreen: "Fullscreen",
282
+ fullscreenExit: "Exit fullscreen",
283
+ source: "View HTML source",
284
+ customHtml: "Custom HTML / CSS",
285
+ poweredBy: "powered by osamtech.com",
286
+ "source.apply": "Apply",
287
+ "source.title": "Edit HTML source"
288
+ };
289
+ function pruneLabels(input) {
290
+ const out = {};
291
+ for (const [k, v] of Object.entries(input)) if (typeof v === "string") out[k] = v;
292
+ return out;
293
+ }
294
+ function has(items, ...names) {
295
+ return names.some((n) => items.includes(n));
296
+ }
297
+ function resolveConfig(input = {}) {
298
+ const toolbarResolved = resolveToolbar(input.toolbar);
299
+ const items = toolbarResolved === false ? [] : toolbarResolved;
300
+ let headingLevels;
301
+ if (input.heading === false) {
302
+ headingLevels = false;
303
+ } else if (input.heading?.levels?.length) {
304
+ headingLevels = [...input.heading.levels].sort();
305
+ } else {
306
+ const fromToolbar = Object.entries(HEADING_ITEMS).filter(([k]) => items.includes(k)).map(([, v]) => v).sort();
307
+ if (fromToolbar.length) headingLevels = fromToolbar;
308
+ else if (has(items, "headings")) headingLevels = [1, 2, 3];
309
+ else headingLevels = [1, 2, 3];
310
+ }
311
+ let link = false;
312
+ if (input.link !== false && (has(items, "link", "unlink") || input.link)) {
313
+ const l = input.link || {};
314
+ link = {
315
+ allowTargetBlank: l.allowTargetBlank ?? true,
316
+ allowRelAttributes: l.allowRelAttributes ?? true,
317
+ defaultRel: l.defaultRel === void 0 ? "noopener noreferrer nofollow" : l.defaultRel,
318
+ protocols: l.protocols ?? ["http", "https", "mailto", "tel"],
319
+ autolink: l.autolink ?? true
320
+ };
321
+ }
322
+ const color = input.color === false || !has(items, "color") ? false : { colors: input.color?.colors ?? DEFAULT_COLORS };
323
+ const highlight = input.highlight === false || !has(items, "highlight") ? false : { colors: input.highlight?.highlights ?? input.highlight?.colors ?? DEFAULT_HIGHLIGHTS, multicolor: true };
324
+ let image = false;
325
+ const wantsImage = has(items, "image", "imageUrl", "mediaLibrary") || !!input.image;
326
+ if (input.image !== false && wantsImage) {
327
+ const im = input.image || {};
328
+ image = {
329
+ resizable: im.resizable ?? true,
330
+ caption: im.caption ?? true,
331
+ align: im.align ?? true,
332
+ link: im.link ?? true,
333
+ accept: im.accept ?? "image/*",
334
+ maxSize: im.maxSize ?? input.upload?.maxSize,
335
+ allowUrl: has(items, "imageUrl") || !input.upload?.handler,
336
+ allowUpload: has(items, "image") && !!input.upload?.handler
337
+ };
338
+ }
339
+ const hasHandler = !!input.upload?.handler;
340
+ const upload = {
341
+ hasHandler,
342
+ video: input.upload?.video ?? hasHandler,
343
+ pdf: input.upload?.pdf ?? hasHandler,
344
+ maxSize: input.upload?.maxSize
345
+ };
346
+ let embed = false;
347
+ const wantsEmbed = has(items, "embed", "youtube", "instagram", "video", "pdf") || !!input.embed;
348
+ if (input.embed !== false && wantsEmbed) {
349
+ const em = input.embed || {};
350
+ embed = {
351
+ allowedHosts: em.allowedHosts ?? DEFAULT_EMBED_HOSTS,
352
+ defaultRatio: em.defaultRatio || "16/9"
353
+ };
354
+ }
355
+ let textAlign = false;
356
+ const wantsAlign = has(items, "alignLeft", "alignCenter", "alignRight", "alignJustify") || !!input.textAlign;
357
+ if (input.textAlign !== false && wantsAlign) {
358
+ const t = input.textAlign && input.textAlign !== true ? input.textAlign : {};
359
+ textAlign = {
360
+ types: t.types ?? ["heading", "paragraph"],
361
+ alignments: t.alignments ?? ["left", "center", "right", "justify"],
362
+ defaultAlignment: t.defaultAlignment ?? "left"
363
+ };
364
+ }
365
+ let codeBlock = false;
366
+ if (input.codeBlock === true || has(items, "codeBlock")) codeBlock = { copyButton: true };
367
+ else if (input.codeBlock && typeof input.codeBlock === "object") {
368
+ codeBlock = {
369
+ defaultLanguage: input.codeBlock.defaultLanguage,
370
+ copyButton: input.codeBlock.copyButton ?? true
371
+ };
372
+ }
373
+ const mediaLibrary = input.mediaLibrary ?? false;
374
+ let html = false;
375
+ const wantsHtml = input.html === true || input.html && typeof input.html === "object" || has(items, "source", "customHtml");
376
+ if (input.html !== false && wantsHtml) {
377
+ const h = input.html && typeof input.html === "object" ? input.html : {};
378
+ html = {
379
+ sourceView: h.sourceView ?? true,
380
+ customBlock: h.customBlock ?? true,
381
+ styleAttributes: h.styleAttributes ?? true,
382
+ classAttributes: h.classAttributes ?? true,
383
+ allowStyleTags: h.allowStyleTags ?? false
384
+ };
385
+ }
386
+ return {
387
+ toolbar: toolbarResolved === false ? false : tidyToolbar(items),
388
+ placeholder: input.placeholder ?? "Write something\u2026",
389
+ dir: input.dir ?? "ltr",
390
+ labels: pruneLabels({ ...DEFAULT_LABELS, ...input.labels ?? {} }),
391
+ headingLevels,
392
+ link,
393
+ color,
394
+ highlight,
395
+ image,
396
+ upload,
397
+ embed,
398
+ mediaLibrary,
399
+ html,
400
+ theme: input.theme,
401
+ branding: input.branding ?? true,
402
+ taskList: input.taskList ?? has(items, "taskList"),
403
+ codeBlock,
404
+ blockquote: input.blockquote ?? has(items, "blockquote"),
405
+ horizontalRule: input.horizontalRule ?? has(items, "horizontalRule"),
406
+ textAlign,
407
+ starterKit: input.starterKit ?? {}
408
+ };
409
+ }
410
+ var ResizableImage = Image__default.default.extend({
411
+ name: "image",
412
+ addOptions() {
413
+ return {
414
+ ...this.parent?.(),
415
+ resizable: true,
416
+ caption: true,
417
+ align: true,
418
+ link: true,
419
+ inline: false,
420
+ HTMLAttributes: {}
421
+ };
422
+ },
423
+ inline: false,
424
+ group: "block",
425
+ draggable: true,
426
+ addAttributes() {
427
+ return {
428
+ src: { default: null },
429
+ alt: { default: null },
430
+ title: { default: null },
431
+ width: {
432
+ default: null,
433
+ parseHTML: (el) => el.getAttribute("data-width") || el.style.width || el.getAttribute("width") || null,
434
+ renderHTML: (attrs) => attrs.width ? { "data-width": attrs.width } : {}
435
+ },
436
+ align: {
437
+ default: null,
438
+ parseHTML: (el) => el.getAttribute("data-align") || el.closest("figure")?.getAttribute("data-align") || null,
439
+ renderHTML: () => ({})
440
+ },
441
+ href: {
442
+ default: null,
443
+ parseHTML: (el) => el.closest("a")?.getAttribute("href") || null,
444
+ renderHTML: () => ({})
445
+ },
446
+ target: {
447
+ default: null,
448
+ parseHTML: (el) => el.closest("a")?.getAttribute("target") || null,
449
+ renderHTML: () => ({})
450
+ },
451
+ rel: {
452
+ default: null,
453
+ parseHTML: (el) => el.closest("a")?.getAttribute("rel") || null,
454
+ renderHTML: () => ({})
455
+ },
456
+ caption: {
457
+ default: null,
458
+ parseHTML: (el) => el.closest("figure")?.querySelector("figcaption")?.textContent || null,
459
+ renderHTML: () => ({})
460
+ }
461
+ };
462
+ },
463
+ parseHTML() {
464
+ return [{ tag: "figure[data-osam-image] img" }, { tag: "img[src]" }];
465
+ },
466
+ renderHTML({ HTMLAttributes, node }) {
467
+ const a = node.attrs;
468
+ const figureAttrs = { "data-osam-image": "" };
469
+ if (a.align) figureAttrs["data-align"] = a.align;
470
+ if (a.width) figureAttrs.style = `width:${cssLen(a.width)}`;
471
+ const { align, width, caption, href, target, rel, ...rest } = HTMLAttributes;
472
+ const imgSpec = [
473
+ "img",
474
+ core.mergeAttributes(this.options.HTMLAttributes, rest, a.width ? { "data-width": a.width } : {})
475
+ ];
476
+ const media = a.href ? ["a", { href: a.href, ...a.target ? { target: a.target } : {}, ...a.rel ? { rel: a.rel } : {} }, imgSpec] : imgSpec;
477
+ return a.caption ? ["figure", figureAttrs, media, ["figcaption", {}, a.caption]] : ["figure", figureAttrs, media];
478
+ },
479
+ addNodeView() {
480
+ return react.ReactNodeViewRenderer(ImageNodeView);
481
+ }
482
+ });
483
+ function cssLen(v) {
484
+ if (typeof v === "number") return `${v}px`;
485
+ return /^\d+$/.test(v) ? `${v}px` : v;
486
+ }
487
+ function ImageNodeView({ node, updateAttributes, editor, selected }) {
488
+ const { src, alt, title, width, align, caption, href } = node.attrs;
489
+ const opts = editor.extensionManager.extensions.find((e) => e.name === "image")?.options;
490
+ const wrapRef = React10__namespace.useRef(null);
491
+ const [dragging, setDragging] = React10__namespace.useState(false);
492
+ const [linking, setLinking] = React10__namespace.useState(false);
493
+ const [hrefDraft, setHrefDraft] = React10__namespace.useState(href || "");
494
+ const startResize = (e, side) => {
495
+ if (!opts?.resizable) return;
496
+ e.preventDefault();
497
+ const startX = e.clientX;
498
+ const el = wrapRef.current;
499
+ if (!el) return;
500
+ const startW = el.getBoundingClientRect().width;
501
+ const parentW = el.closest(".osam-image")?.getBoundingClientRect().width || startW;
502
+ setDragging(true);
503
+ const move = (ev) => {
504
+ const delta = (ev.clientX - startX) * (side === "left" ? -1 : 1);
505
+ const next = Math.max(48, Math.min(parentW, startW + delta));
506
+ updateAttributes({ width: `${Math.round(next / parentW * 100)}%` });
507
+ };
508
+ const up = () => {
509
+ setDragging(false);
510
+ window.removeEventListener("pointermove", move);
511
+ window.removeEventListener("pointerup", up);
512
+ };
513
+ window.addEventListener("pointermove", move);
514
+ window.addEventListener("pointerup", up);
515
+ };
516
+ return /* @__PURE__ */ jsxRuntime.jsxs(
517
+ react.NodeViewWrapper,
518
+ {
519
+ className: "osam-image",
520
+ "data-align": align || void 0,
521
+ "data-selected": selected || void 0,
522
+ "data-has-link": href ? "" : void 0,
523
+ children: [
524
+ /* @__PURE__ */ jsxRuntime.jsxs(
525
+ "div",
526
+ {
527
+ ref: wrapRef,
528
+ className: "osam-image__frame",
529
+ style: { width: width ? cssLen(width) : void 0 },
530
+ "data-dragging": dragging || void 0,
531
+ contentEditable: false,
532
+ children: [
533
+ /* @__PURE__ */ jsxRuntime.jsx("img", { src, alt: alt || "", title: title || void 0, draggable: false }),
534
+ opts?.resizable && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
535
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "osam-image__handle osam-image__handle--left", onPointerDown: (e) => startResize(e, "left") }),
536
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "osam-image__handle osam-image__handle--right", onPointerDown: (e) => startResize(e, "right") })
537
+ ] }),
538
+ selected && editor.isEditable && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-image__bar", children: [
539
+ opts?.align && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
540
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-active": align === "left", title: "Float left, text wraps right", onClick: () => updateAttributes({ align: "left" }), children: "\u21E4" }),
541
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-active": align === "center" || !align, title: "Center block", onClick: () => updateAttributes({ align: "center" }), children: "\u21D4" }),
542
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-active": align === "right", title: "Float right, text wraps left", onClick: () => updateAttributes({ align: "right" }), children: "\u21E5" }),
543
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "osam-image__sep" })
544
+ ] }),
545
+ ["25%", "50%", "75%", "100%"].map((w) => /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-active": width === w, onClick: () => updateAttributes({ width: w }), children: w }, w)),
546
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => updateAttributes({ width: null }), title: "Reset size", children: "\u21BA" }),
547
+ opts?.link && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
548
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "osam-image__sep" }),
549
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-active": !!href, title: "Link image", onClick: () => {
550
+ setHrefDraft(href || "");
551
+ setLinking((v) => !v);
552
+ }, children: "\u{1F517}" })
553
+ ] })
554
+ ] }),
555
+ linking && selected && editor.isEditable && /* @__PURE__ */ jsxRuntime.jsxs(
556
+ "form",
557
+ {
558
+ className: "osam-image__linkform",
559
+ onSubmit: (e) => {
560
+ e.preventDefault();
561
+ const url = hrefDraft.trim();
562
+ updateAttributes(
563
+ url ? { href: url, target: "_blank", rel: "noopener noreferrer" } : { href: null, target: null, rel: null }
564
+ );
565
+ setLinking(false);
566
+ },
567
+ children: [
568
+ /* @__PURE__ */ jsxRuntime.jsx("input", { autoFocus: true, value: hrefDraft, placeholder: "https://\u2026 (empty to remove)", onChange: (e) => setHrefDraft(e.target.value) }),
569
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", children: "OK" })
570
+ ]
571
+ }
572
+ )
573
+ ]
574
+ }
575
+ ),
576
+ opts?.caption && /* @__PURE__ */ jsxRuntime.jsx(
577
+ "figcaption",
578
+ {
579
+ className: "osam-image__caption",
580
+ "data-placeholder": "Add a caption\u2026",
581
+ contentEditable: editor.isEditable,
582
+ suppressContentEditableWarning: true,
583
+ onBlur: (e) => updateAttributes({ caption: e.currentTarget.textContent?.trim() || null }),
584
+ children: caption
585
+ }
586
+ )
587
+ ]
588
+ }
589
+ );
590
+ }
591
+ var Embed = core.Node.create({
592
+ name: "embed",
593
+ group: "block",
594
+ atom: true,
595
+ draggable: true,
596
+ selectable: true,
597
+ addOptions() {
598
+ return { defaultRatio: "16/9", HTMLAttributes: {} };
599
+ },
600
+ addAttributes() {
601
+ return {
602
+ src: { default: null },
603
+ provider: { default: "iframe" },
604
+ ratio: { default: this.options.defaultRatio },
605
+ title: { default: null },
606
+ originalUrl: { default: null },
607
+ align: { default: null },
608
+ width: { default: null }
609
+ };
610
+ },
611
+ parseHTML() {
612
+ return [
613
+ {
614
+ tag: "div[data-osam-embed]",
615
+ getAttrs: (node) => {
616
+ const el = node;
617
+ const media = el.querySelector("iframe, video, object");
618
+ const src = media?.getAttribute("src") || media?.getAttribute("data") || null;
619
+ return {
620
+ src,
621
+ provider: el.getAttribute("data-osam-embed") || "iframe",
622
+ ratio: el.getAttribute("data-ratio") || "16/9",
623
+ align: el.getAttribute("data-align") || null,
624
+ title: media?.getAttribute("title") || null
625
+ };
626
+ }
627
+ },
628
+ {
629
+ tag: "iframe[src]",
630
+ getAttrs: (el) => ({
631
+ src: el.getAttribute("src"),
632
+ provider: "iframe"
633
+ })
634
+ }
635
+ ];
636
+ },
637
+ renderHTML({ HTMLAttributes, node }) {
638
+ const attrs = node.attrs;
639
+ const wrapper = core.mergeAttributes(this.options.HTMLAttributes, {
640
+ "data-osam-embed": attrs.provider,
641
+ "data-ratio": attrs.ratio,
642
+ ...attrs.align ? { "data-align": attrs.align } : {},
643
+ ...attrs.width ? { style: `width:${attrs.width}` } : {}
644
+ });
645
+ if (attrs.provider === "video") {
646
+ return [
647
+ "div",
648
+ wrapper,
649
+ ["video", { src: attrs.src, controls: "true", playsinline: "true", style: "width:100%" }]
650
+ ];
651
+ }
652
+ if (attrs.provider === "pdf") {
653
+ return [
654
+ "div",
655
+ wrapper,
656
+ [
657
+ "object",
658
+ { data: attrs.src, type: "application/pdf", style: "width:100%;height:100%" },
659
+ ["a", { href: attrs.src ?? "#" }, attrs.title || "Open PDF"]
660
+ ]
661
+ ];
662
+ }
663
+ return [
664
+ "div",
665
+ wrapper,
666
+ [
667
+ "iframe",
668
+ {
669
+ src: attrs.src,
670
+ title: attrs.title || attrs.provider,
671
+ loading: "lazy",
672
+ frameborder: "0",
673
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
674
+ allowfullscreen: "true",
675
+ style: "width:100%;height:100%"
676
+ }
677
+ ]
678
+ ];
679
+ },
680
+ addCommands() {
681
+ return {
682
+ setEmbed: (attrs) => ({ commands }) => commands.insertContent({ type: this.name, attrs }),
683
+ updateEmbed: (attrs) => ({ commands }) => commands.updateAttributes(this.name, attrs)
684
+ };
685
+ },
686
+ addNodeView() {
687
+ return react.ReactNodeViewRenderer(EmbedNodeView);
688
+ }
689
+ });
690
+ function EmbedNodeView({ node, selected, updateAttributes, editor }) {
691
+ const attrs = node.attrs;
692
+ const ratioStyle = attrs.provider === "pdf" ? void 0 : { aspectRatio: attrs.ratio.replace("/", " / ") };
693
+ return /* @__PURE__ */ jsxRuntime.jsxs(
694
+ react.NodeViewWrapper,
695
+ {
696
+ className: "osam-embed",
697
+ "data-provider": attrs.provider,
698
+ "data-align": attrs.align || void 0,
699
+ "data-selected": selected || void 0,
700
+ style: { width: attrs.width || void 0 },
701
+ contentEditable: false,
702
+ children: [
703
+ /* @__PURE__ */ jsxRuntime.jsx(
704
+ "div",
705
+ {
706
+ className: "osam-embed__frame",
707
+ "data-provider": attrs.provider,
708
+ style: {
709
+ ...ratioStyle,
710
+ ...attrs.provider === "pdf" ? { height: 620 } : {}
711
+ },
712
+ children: attrs.provider === "video" ? /* @__PURE__ */ jsxRuntime.jsx(VideoPlayer, { src: attrs.src || "" }) : attrs.provider === "pdf" ? /* @__PURE__ */ jsxRuntime.jsx("object", { data: attrs.src || "", type: "application/pdf", children: /* @__PURE__ */ jsxRuntime.jsx("a", { href: attrs.src || "#", target: "_blank", rel: "noreferrer", children: attrs.title || "Open PDF" }) }) : /* @__PURE__ */ jsxRuntime.jsx(
713
+ "iframe",
714
+ {
715
+ src: attrs.src || "",
716
+ title: attrs.title || attrs.provider,
717
+ loading: "lazy",
718
+ allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share",
719
+ allowFullScreen: true
720
+ }
721
+ )
722
+ }
723
+ ),
724
+ selected && editor.isEditable && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-embed__bar", contentEditable: false, children: [
725
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-active": attrs.align === "left", onClick: () => updateAttributes({ align: "left" }), children: "\u290E" }),
726
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-active": !attrs.align || attrs.align === "center", onClick: () => updateAttributes({ align: "center" }), children: "\u21D4" }),
727
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-active": attrs.align === "right", onClick: () => updateAttributes({ align: "right" }), children: "\u290F" }),
728
+ attrs.provider !== "pdf" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
729
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "osam-embed__sep" }),
730
+ ["16/9", "4/3", "1/1"].map((r) => /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-active": attrs.ratio === r, onClick: () => updateAttributes({ ratio: r }), children: r }, r))
731
+ ] })
732
+ ] })
733
+ ]
734
+ }
735
+ );
736
+ }
737
+ function VideoPlayer({ src }) {
738
+ const ref = React10__namespace.useRef(null);
739
+ const isHls = /\.m3u8(\?.*)?$/i.test(src);
740
+ React10__namespace.useEffect(() => {
741
+ const video = ref.current;
742
+ if (!video || !isHls) return;
743
+ if (video.canPlayType("application/vnd.apple.mpegurl")) {
744
+ video.src = src;
745
+ return;
746
+ }
747
+ let hls;
748
+ let cancelled = false;
749
+ import('hls.js').then((mod) => {
750
+ if (cancelled) return;
751
+ const Hls = mod.default ?? mod;
752
+ if (Hls?.isSupported?.()) {
753
+ hls = new Hls();
754
+ hls.loadSource(src);
755
+ hls.attachMedia(video);
756
+ } else {
757
+ video.src = src;
758
+ }
759
+ }).catch(() => {
760
+ video.src = src;
761
+ });
762
+ return () => {
763
+ cancelled = true;
764
+ hls?.destroy?.();
765
+ };
766
+ }, [src, isHls]);
767
+ return /* @__PURE__ */ jsxRuntime.jsx("video", { ref, src: isHls ? void 0 : src, controls: true, playsInline: true, preload: "metadata" });
768
+ }
769
+ var DEFAULT_LANGUAGES = [
770
+ "plaintext",
771
+ "bash",
772
+ "javascript",
773
+ "typescript",
774
+ "jsx",
775
+ "tsx",
776
+ "json",
777
+ "html",
778
+ "css",
779
+ "scss",
780
+ "python",
781
+ "java",
782
+ "c",
783
+ "cpp",
784
+ "csharp",
785
+ "go",
786
+ "rust",
787
+ "php",
788
+ "ruby",
789
+ "sql",
790
+ "yaml",
791
+ "markdown",
792
+ "diff"
793
+ ];
794
+ var CodeBlock = extensionCodeBlockLowlight.CodeBlockLowlight.extend({
795
+ addOptions() {
796
+ return {
797
+ ...this.parent?.(),
798
+ copyButton: true,
799
+ languages: DEFAULT_LANGUAGES
800
+ };
801
+ },
802
+ addNodeView() {
803
+ return react.ReactNodeViewRenderer(CodeBlockView);
804
+ }
805
+ });
806
+ function CodeBlockView({ node, updateAttributes, editor, extension }) {
807
+ const opts = extension.options;
808
+ const language = node.attrs.language || "plaintext";
809
+ const [copied, setCopied] = React10__namespace.useState(false);
810
+ const copy = async () => {
811
+ const text = node.textContent;
812
+ try {
813
+ await navigator.clipboard.writeText(text);
814
+ } catch {
815
+ const ta = document.createElement("textarea");
816
+ ta.value = text;
817
+ document.body.appendChild(ta);
818
+ ta.select();
819
+ document.execCommand("copy");
820
+ ta.remove();
821
+ }
822
+ setCopied(true);
823
+ setTimeout(() => setCopied(false), 1500);
824
+ };
825
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.NodeViewWrapper, { className: "osam-codeblock", children: [
826
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-codeblock__bar", contentEditable: false, children: [
827
+ editor.isEditable ? /* @__PURE__ */ jsxRuntime.jsx(
828
+ "select",
829
+ {
830
+ value: language,
831
+ onChange: (e) => updateAttributes({ language: e.target.value }),
832
+ children: opts.languages.map((l) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: l, children: l }, l))
833
+ }
834
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "osam-codeblock__lang", children: language }),
835
+ opts.copyButton && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "osam-codeblock__copy", onClick: copy, children: copied ? "Copied \u2713" : "Copy" })
836
+ ] }),
837
+ /* @__PURE__ */ jsxRuntime.jsx("pre", { children: /* @__PURE__ */ jsxRuntime.jsx(react.NodeViewContent, { as: "code", className: `language-${language}` }) })
838
+ ] });
839
+ }
840
+
841
+ // src/sanitize.ts
842
+ var BLOCKED_TAGS = ["script", "noscript", "template", "object", "embed", "base", "meta", "link"];
843
+ var URL_ATTRS = ["href", "src", "action", "formaction", "xlink:href", "data"];
844
+ function sanitizeHtml(input, opts = {}) {
845
+ if (!input) return "";
846
+ if (typeof window === "undefined" || typeof DOMParser === "undefined") {
847
+ return input.replace(/<\/?(script|iframe\s+srcdoc)[^>]*>/gi, "").replace(/\son\w+\s*=\s*(".*?"|'.*?'|[^\s>]+)/gi, "").replace(/(javascript|data):/gi, "blocked:");
848
+ }
849
+ const doc = new DOMParser().parseFromString(`<body>${input}</body>`, "text/html");
850
+ const blocked = opts.allowStyleTags ? BLOCKED_TAGS : [...BLOCKED_TAGS, "style"];
851
+ const walk = (node) => {
852
+ for (const child of Array.from(node.children)) {
853
+ const tag = child.tagName.toLowerCase();
854
+ if (blocked.includes(tag)) {
855
+ child.remove();
856
+ continue;
857
+ }
858
+ for (const attr of Array.from(child.attributes)) {
859
+ const name = attr.name.toLowerCase();
860
+ if (name.startsWith("on")) {
861
+ child.removeAttribute(attr.name);
862
+ continue;
863
+ }
864
+ if (URL_ATTRS.includes(name)) {
865
+ const v = attr.value.trim().toLowerCase();
866
+ if (v.startsWith("javascript:") || v.startsWith("vbscript:") || v.startsWith("data:text/html")) {
867
+ child.removeAttribute(attr.name);
868
+ }
869
+ }
870
+ if (name === "style" && /expression\s*\(|javascript:/i.test(attr.value)) {
871
+ child.removeAttribute("style");
872
+ }
873
+ }
874
+ walk(child);
875
+ }
876
+ };
877
+ walk(doc.body);
878
+ return doc.body.innerHTML;
879
+ }
880
+ var CustomHtml = core.Node.create({
881
+ name: "customHtml",
882
+ group: "block",
883
+ atom: true,
884
+ draggable: true,
885
+ selectable: true,
886
+ addOptions() {
887
+ return { allowStyleTags: false };
888
+ },
889
+ addAttributes() {
890
+ return {
891
+ html: {
892
+ default: "",
893
+ // stored on the doc node; never rendered as an attribute
894
+ renderHTML: () => ({}),
895
+ parseHTML: () => void 0
896
+ }
897
+ };
898
+ },
899
+ parseHTML() {
900
+ return [
901
+ {
902
+ tag: "div[data-osam-html]",
903
+ getAttrs: (el) => ({ html: el.innerHTML })
904
+ }
905
+ ];
906
+ },
907
+ renderHTML({ HTMLAttributes, node }) {
908
+ const clean = sanitizeHtml(node.attrs.html || "", {
909
+ allowStyleTags: this.options.allowStyleTags
910
+ });
911
+ const dom = document.createElement("div");
912
+ for (const [k, v] of Object.entries(core.mergeAttributes(HTMLAttributes, { "data-osam-html": "" }))) {
913
+ if (v != null) dom.setAttribute(k, String(v));
914
+ }
915
+ dom.innerHTML = clean;
916
+ return dom;
917
+ },
918
+ addCommands() {
919
+ return {
920
+ setCustomHtml: (html = "<div>\n \n</div>") => ({ commands }) => commands.insertContent({ type: this.name, attrs: { html } })
921
+ };
922
+ },
923
+ addNodeView() {
924
+ return react.ReactNodeViewRenderer(CustomHtmlView);
925
+ }
926
+ });
927
+ function CustomHtmlView({ node, updateAttributes, editor, selected }) {
928
+ const allowStyleTags = editor.extensionManager.extensions.find((e) => e.name === "customHtml")?.options?.allowStyleTags;
929
+ const [editing, setEditing] = React10__namespace.useState(!node.attrs.html);
930
+ const [draft, setDraft] = React10__namespace.useState(node.attrs.html || "");
931
+ const clean = React10__namespace.useMemo(
932
+ () => sanitizeHtml(node.attrs.html || "", { allowStyleTags }),
933
+ [node.attrs.html, allowStyleTags]
934
+ );
935
+ return /* @__PURE__ */ jsxRuntime.jsxs(react.NodeViewWrapper, { className: "osam-html", "data-selected": selected || void 0, contentEditable: false, children: [
936
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-html__bar", children: [
937
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "HTML" }),
938
+ editor.isEditable && /* @__PURE__ */ jsxRuntime.jsx(
939
+ "button",
940
+ {
941
+ type: "button",
942
+ onClick: () => {
943
+ if (editing) {
944
+ updateAttributes({ html: draft });
945
+ setEditing(false);
946
+ } else {
947
+ setDraft(node.attrs.html || "");
948
+ setEditing(true);
949
+ }
950
+ },
951
+ children: editing ? "Done" : "Edit"
952
+ }
953
+ )
954
+ ] }),
955
+ editing && editor.isEditable ? /* @__PURE__ */ jsxRuntime.jsx(
956
+ "textarea",
957
+ {
958
+ className: "osam-html__src",
959
+ value: draft,
960
+ spellCheck: false,
961
+ autoFocus: true,
962
+ onChange: (e) => setDraft(e.target.value),
963
+ onBlur: () => {
964
+ updateAttributes({ html: draft });
965
+ setEditing(false);
966
+ },
967
+ rows: Math.min(20, Math.max(4, draft.split("\n").length + 1))
968
+ }
969
+ ) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "osam-html__preview", dangerouslySetInnerHTML: { __html: clean } })
970
+ ] });
971
+ }
972
+ var PreserveAttributes = core.Extension.create({
973
+ name: "preserveAttributes",
974
+ addOptions() {
975
+ return {
976
+ types: ["paragraph", "heading", "blockquote", "listItem", "codeBlock"],
977
+ style: true,
978
+ classAttr: true
979
+ };
980
+ },
981
+ addGlobalAttributes() {
982
+ const attrs = {
983
+ id: {
984
+ default: null,
985
+ parseHTML: (el) => el.getAttribute("id"),
986
+ renderHTML: (a) => a.id ? { id: a.id } : {}
987
+ }
988
+ };
989
+ if (this.options.style) {
990
+ attrs.style = {
991
+ default: null,
992
+ parseHTML: (el) => el.getAttribute("style"),
993
+ renderHTML: (a) => a.style ? { style: a.style } : {}
994
+ };
995
+ }
996
+ if (this.options.classAttr) {
997
+ attrs.class = {
998
+ default: null,
999
+ parseHTML: (el) => el.getAttribute("class"),
1000
+ renderHTML: (a) => a.class ? { class: a.class } : {}
1001
+ };
1002
+ }
1003
+ return [{ types: this.options.types, attributes: attrs }];
1004
+ }
1005
+ });
1006
+
1007
+ // src/extensions/buildExtensions.ts
1008
+ function buildExtensions(config) {
1009
+ const wantCodeBlock = config.codeBlock !== false;
1010
+ const starterOptions = {
1011
+ heading: config.headingLevels === false ? false : { levels: config.headingLevels },
1012
+ codeBlock: false,
1013
+ // replaced by CodeBlock (or removed) below
1014
+ link: config.link === false ? false : {
1015
+ openOnClick: false,
1016
+ autolink: config.link.autolink,
1017
+ protocols: config.link.protocols,
1018
+ HTMLAttributes: {
1019
+ ...config.link.defaultRel ? { rel: config.link.defaultRel } : {}
1020
+ }
1021
+ }
1022
+ };
1023
+ if (!config.blockquote) starterOptions.blockquote = false;
1024
+ if (!config.horizontalRule) starterOptions.horizontalRule = false;
1025
+ Object.assign(starterOptions, config.starterKit);
1026
+ const starter = StarterKit__default.default.configure(starterOptions);
1027
+ const list = [
1028
+ starter,
1029
+ Placeholder__default.default.configure({
1030
+ placeholder: config.placeholder,
1031
+ showOnlyWhenEditable: true
1032
+ })
1033
+ ];
1034
+ if (config.color) {
1035
+ list.push(extensionTextStyle.TextStyle, extensionTextStyle.Color);
1036
+ } else if (config.highlight) {
1037
+ list.push(extensionTextStyle.TextStyle);
1038
+ }
1039
+ if (config.highlight) {
1040
+ list.push(Highlight__default.default.configure({ multicolor: config.highlight.multicolor }));
1041
+ }
1042
+ if (config.textAlign) {
1043
+ list.push(
1044
+ TextAlign__default.default.configure({
1045
+ types: config.textAlign.types,
1046
+ alignments: config.textAlign.alignments,
1047
+ defaultAlignment: config.textAlign.defaultAlignment
1048
+ })
1049
+ );
1050
+ }
1051
+ if (config.taskList) {
1052
+ list.push(extensionTaskList.TaskList, extensionTaskItem.TaskItem.configure({ nested: true }));
1053
+ }
1054
+ if (wantCodeBlock) {
1055
+ const lowlight$1 = lowlight.createLowlight(lowlight.common);
1056
+ list.push(
1057
+ CodeBlock.configure({
1058
+ lowlight: lowlight$1,
1059
+ defaultLanguage: config.codeBlock && typeof config.codeBlock === "object" ? config.codeBlock.defaultLanguage : void 0,
1060
+ copyButton: config.codeBlock ? config.codeBlock.copyButton : true
1061
+ })
1062
+ );
1063
+ }
1064
+ if (config.image) {
1065
+ list.push(
1066
+ ResizableImage.configure({
1067
+ resizable: config.image.resizable,
1068
+ caption: config.image.caption,
1069
+ align: config.image.align,
1070
+ link: config.image.link,
1071
+ HTMLAttributes: { class: "osam-img" }
1072
+ })
1073
+ );
1074
+ }
1075
+ if (config.embed) {
1076
+ list.push(Embed.configure({ defaultRatio: config.embed.defaultRatio }));
1077
+ }
1078
+ if (config.html) {
1079
+ if (config.html.styleAttributes || config.html.classAttributes) {
1080
+ list.push(
1081
+ PreserveAttributes.configure({
1082
+ style: config.html.styleAttributes,
1083
+ classAttr: config.html.classAttributes,
1084
+ types: ["paragraph", "heading", "blockquote", "listItem", "codeBlock"]
1085
+ })
1086
+ );
1087
+ }
1088
+ if (config.html.customBlock) {
1089
+ list.push(CustomHtml.configure({ allowStyleTags: config.html.allowStyleTags }));
1090
+ }
1091
+ }
1092
+ return list;
1093
+ }
1094
+ function applyUserExtensions(base, extra) {
1095
+ if (!extra) return base;
1096
+ if (typeof extra === "function") return extra(base);
1097
+ return [...base, ...extra];
1098
+ }
1099
+
1100
+ // src/useOsamEditor.ts
1101
+ function useOsamEditor(options = {}) {
1102
+ const {
1103
+ content = "",
1104
+ editable = true,
1105
+ autofocus = false,
1106
+ onUpdate,
1107
+ onCreate,
1108
+ extensions: userExtensions,
1109
+ ...rest
1110
+ } = options;
1111
+ const cfgKey = stableKey(rest);
1112
+ const config = React10__namespace.useMemo(() => resolveConfig(rest), [cfgKey]);
1113
+ const extensions = React10__namespace.useMemo(
1114
+ () => applyUserExtensions(buildExtensions(config), userExtensions),
1115
+ [config, userExtensions]
1116
+ );
1117
+ const cbRef = React10__namespace.useRef({ onUpdate, onCreate });
1118
+ cbRef.current = { onUpdate, onCreate };
1119
+ const editor = react.useEditor(
1120
+ {
1121
+ extensions,
1122
+ content: content ?? "",
1123
+ editable,
1124
+ autofocus,
1125
+ immediatelyRender: false,
1126
+ editorProps: {
1127
+ attributes: {
1128
+ class: "osam-content",
1129
+ dir: config.dir,
1130
+ spellcheck: "true"
1131
+ }
1132
+ },
1133
+ onCreate: ({ editor: editor2 }) => cbRef.current.onCreate?.(editor2),
1134
+ onUpdate: ({ editor: editor2 }) => {
1135
+ cbRef.current.onUpdate?.({ html: editor2.getHTML(), json: editor2.getJSON(), editor: editor2 });
1136
+ }
1137
+ },
1138
+ [extensions]
1139
+ );
1140
+ React10__namespace.useEffect(() => {
1141
+ editor?.setEditable(editable);
1142
+ }, [editor, editable]);
1143
+ return { editor, config };
1144
+ }
1145
+ function stableKey(cfg) {
1146
+ try {
1147
+ return JSON.stringify(cfg, (_k, v) => {
1148
+ if (typeof v === "function") return "[fn]";
1149
+ return v;
1150
+ });
1151
+ } catch {
1152
+ return Math.random().toString();
1153
+ }
1154
+ }
1155
+ var S = (props) => /* @__PURE__ */ jsxRuntime.jsx(
1156
+ "svg",
1157
+ {
1158
+ width: "16",
1159
+ height: "16",
1160
+ viewBox: "0 0 24 24",
1161
+ fill: "none",
1162
+ stroke: "currentColor",
1163
+ strokeWidth: "2",
1164
+ strokeLinecap: "round",
1165
+ strokeLinejoin: "round",
1166
+ "aria-hidden": "true",
1167
+ ...props
1168
+ }
1169
+ );
1170
+ function Icon({ name }) {
1171
+ switch (name) {
1172
+ case "bold":
1173
+ return /* @__PURE__ */ jsxRuntime.jsx(S, { children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 4h8a4 4 0 0 1 0 8H6zM6 12h9a4 4 0 0 1 0 8H6z" }) });
1174
+ case "italic":
1175
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1176
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "19", y1: "4", x2: "10", y2: "4" }),
1177
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "14", y1: "20", x2: "5", y2: "20" }),
1178
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "15", y1: "4", x2: "9", y2: "20" })
1179
+ ] });
1180
+ case "underline":
1181
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1182
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 3v7a6 6 0 0 0 12 0V3" }),
1183
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "21", x2: "20", y2: "21" })
1184
+ ] });
1185
+ case "strike":
1186
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1187
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 4H9a3 3 0 0 0-2.83 4" }),
1188
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 12a4 4 0 0 1 0 8H6" }),
1189
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" })
1190
+ ] });
1191
+ case "code":
1192
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1193
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "16 18 22 12 16 6" }),
1194
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "8 6 2 12 8 18" })
1195
+ ] });
1196
+ case "color":
1197
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1198
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 20h16" }),
1199
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M7 16 12 4l5 12" }),
1200
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 12h6" })
1201
+ ] });
1202
+ case "highlight":
1203
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1204
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m9 11-6 6v3h3l6-6" }),
1205
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m17 3 4 4-9 9-4-4z" })
1206
+ ] });
1207
+ case "clear":
1208
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1209
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 7h16" }),
1210
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m6 7 1 13h10l1-13" }),
1211
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 7V4h6v3" }),
1212
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "14", y1: "4", x2: "20", y2: "20" })
1213
+ ] });
1214
+ case "align-left":
1215
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1216
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
1217
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "14", y2: "12" }),
1218
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "18", y2: "18" })
1219
+ ] });
1220
+ case "align-center":
1221
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1222
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
1223
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "7", y1: "12", x2: "17", y2: "12" }),
1224
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "5", y1: "18", x2: "19", y2: "18" })
1225
+ ] });
1226
+ case "align-right":
1227
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1228
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
1229
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "10", y1: "12", x2: "20", y2: "12" }),
1230
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "18", x2: "20", y2: "18" })
1231
+ ] });
1232
+ case "align-justify":
1233
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1234
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
1235
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
1236
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
1237
+ ] });
1238
+ case "return":
1239
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1240
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "9 10 4 15 9 20" }),
1241
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 4v7a4 4 0 0 1-4 4H4" })
1242
+ ] });
1243
+ case "list-bullet":
1244
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1245
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "6", x2: "20", y2: "6" }),
1246
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "12", x2: "20", y2: "12" }),
1247
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "9", y1: "18", x2: "20", y2: "18" }),
1248
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "6", r: "1" }),
1249
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "12", r: "1" }),
1250
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "18", r: "1" })
1251
+ ] });
1252
+ case "list-ordered":
1253
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1254
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "10", y1: "6", x2: "21", y2: "6" }),
1255
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "10", y1: "12", x2: "21", y2: "12" }),
1256
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "10", y1: "18", x2: "21", y2: "18" }),
1257
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 6h1v4" }),
1258
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 10h2" }),
1259
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 18H4c0-1 2-2 2-3s-1-1.5-2-1" })
1260
+ ] });
1261
+ case "list-check":
1262
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1263
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m3 7 2 2 3-3" }),
1264
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m3 17 2 2 3-3" }),
1265
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "6", x2: "21", y2: "6" }),
1266
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "18", x2: "21", y2: "18" })
1267
+ ] });
1268
+ case "indent":
1269
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1270
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "4 8 8 12 4 16" }),
1271
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "6", x2: "20", y2: "6" }),
1272
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "12", x2: "20", y2: "12" }),
1273
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "18", x2: "20", y2: "18" })
1274
+ ] });
1275
+ case "outdent":
1276
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1277
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "8 8 4 12 8 16" }),
1278
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "6", x2: "20", y2: "6" }),
1279
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "12", x2: "20", y2: "12" }),
1280
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "18", x2: "20", y2: "18" })
1281
+ ] });
1282
+ case "link":
1283
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1284
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M10 13a5 5 0 0 0 7 0l3-3a5 5 0 0 0-7-7l-1 1" }),
1285
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 11a5 5 0 0 0-7 0l-3 3a5 5 0 0 0 7 7l1-1" })
1286
+ ] });
1287
+ case "unlink":
1288
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1289
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18.84 12.25 20 11a5 5 0 0 0-7-7l-1.5 1.34" }),
1290
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5.17 11.75 4 13a5 5 0 0 0 7 7l1.5-1.34" }),
1291
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2", y1: "2", x2: "22", y2: "22" })
1292
+ ] });
1293
+ case "quote":
1294
+ return /* @__PURE__ */ jsxRuntime.jsx(S, { children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 17h3l2-4V7H5v6h3zM14 17h3l2-4V7h-6v6h3z" }) });
1295
+ case "code-block":
1296
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1297
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }),
1298
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "9 9 7 12 9 15" }),
1299
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "15 9 17 12 15 15" })
1300
+ ] });
1301
+ case "hr":
1302
+ return /* @__PURE__ */ jsxRuntime.jsx(S, { children: /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "12", x2: "21", y2: "12" }) });
1303
+ case "image":
1304
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1305
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
1306
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "9", cy: "9", r: "2" }),
1307
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m21 15-5-5L5 21" })
1308
+ ] });
1309
+ case "image-url":
1310
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1311
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "14", rx: "2" }),
1312
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8", cy: "8", r: "1.5" }),
1313
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m21 13-5-4-6 6" }),
1314
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 21h8" })
1315
+ ] });
1316
+ case "youtube":
1317
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1318
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "5", width: "20", height: "14", rx: "4" }),
1319
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "10 9 16 12 10 15" })
1320
+ ] });
1321
+ case "instagram":
1322
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1323
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "5" }),
1324
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "4" }),
1325
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "17.5", cy: "6.5", r: "1" })
1326
+ ] });
1327
+ case "video":
1328
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1329
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "2", y: "4", width: "14", height: "16", rx: "2" }),
1330
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m22 8-6 4 6 4z" })
1331
+ ] });
1332
+ case "embed":
1333
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1334
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "8 6 3 12 8 18" }),
1335
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "16 6 21 12 16 18" }),
1336
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "13", y1: "4", x2: "11", y2: "20" })
1337
+ ] });
1338
+ case "pdf":
1339
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1340
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
1341
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "14 2 14 8 20 8" }),
1342
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 13h1.5a1.5 1.5 0 0 1 0 3H9zM9 13v6" })
1343
+ ] });
1344
+ case "undo":
1345
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1346
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 14 4 9l5-5" }),
1347
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 9h11a5 5 0 0 1 0 10h-3" })
1348
+ ] });
1349
+ case "redo":
1350
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1351
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m15 14 5-5-5-5" }),
1352
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 9H9a5 5 0 0 0 0 10h3" })
1353
+ ] });
1354
+ case "chevron":
1355
+ return /* @__PURE__ */ jsxRuntime.jsx(S, { width: "12", height: "12", children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "6 9 12 15 18 9" }) });
1356
+ case "library":
1357
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1358
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
1359
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "8.5", cy: "8.5", r: "1.5" }),
1360
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "m21 15-4.5-4.5L9 18l-3-3-3 3" })
1361
+ ] });
1362
+ case "maximize":
1363
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1364
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 3H5a2 2 0 0 0-2 2v3" }),
1365
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 3h3a2 2 0 0 1 2 2v3" }),
1366
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 16v3a2 2 0 0 1-2 2h-3" }),
1367
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 21H5a2 2 0 0 1-2-2v-3" })
1368
+ ] });
1369
+ case "minimize":
1370
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1371
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 3v3a2 2 0 0 1-2 2H3" }),
1372
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 8h-3a2 2 0 0 1-2-2V3" }),
1373
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 16h3a2 2 0 0 1 2 2v3" }),
1374
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M16 21v-3a2 2 0 0 1 2-2h3" })
1375
+ ] });
1376
+ case "source":
1377
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1378
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "16 18 22 12 16 6" }),
1379
+ /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "8 6 2 12 8 18" }),
1380
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "4", x2: "10", y2: "20" })
1381
+ ] });
1382
+ case "html-block":
1383
+ return /* @__PURE__ */ jsxRuntime.jsxs(S, { children: [
1384
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { x: "3", y: "4", width: "18", height: "16", rx: "2" }),
1385
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 9v6" }),
1386
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M8 12h3" }),
1387
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M11 9v6" }),
1388
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15 9v6l2-2 2 2V9" })
1389
+ ] });
1390
+ default:
1391
+ return null;
1392
+ }
1393
+ }
1394
+ function useEditorSync(editor) {
1395
+ const [, force] = React10__namespace.useReducer((n) => n + 1, 0);
1396
+ React10__namespace.useEffect(() => {
1397
+ if (!editor) return;
1398
+ const update = () => force();
1399
+ editor.on("transaction", update);
1400
+ editor.on("selectionUpdate", update);
1401
+ editor.on("focus", update);
1402
+ editor.on("blur", update);
1403
+ return () => {
1404
+ editor.off("transaction", update);
1405
+ editor.off("selectionUpdate", update);
1406
+ editor.off("focus", update);
1407
+ editor.off("blur", update);
1408
+ };
1409
+ }, [editor]);
1410
+ }
1411
+ function useOnClickOutside(ref, handler, active = true) {
1412
+ React10__namespace.useEffect(() => {
1413
+ if (!active) return;
1414
+ const listener = (e) => {
1415
+ const el = ref.current;
1416
+ if (!el || el.contains(e.target)) return;
1417
+ handler();
1418
+ };
1419
+ document.addEventListener("mousedown", listener);
1420
+ document.addEventListener("touchstart", listener);
1421
+ const onKey = (e) => e.key === "Escape" && handler();
1422
+ document.addEventListener("keydown", onKey);
1423
+ return () => {
1424
+ document.removeEventListener("mousedown", listener);
1425
+ document.removeEventListener("touchstart", listener);
1426
+ document.removeEventListener("keydown", onKey);
1427
+ };
1428
+ }, [ref, handler, active]);
1429
+ }
1430
+ function Popover({ button, children, align = "start" }) {
1431
+ const [open, setOpen] = React10__namespace.useState(false);
1432
+ const ref = React10__namespace.useRef(null);
1433
+ const close = React10__namespace.useCallback(() => setOpen(false), []);
1434
+ useOnClickOutside(ref, close, open);
1435
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-popover", ref, children: [
1436
+ button({ open, toggle: () => setOpen((o) => !o) }),
1437
+ open && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "osam-popover__panel", "data-align": align, role: "dialog", children: children({ close }) })
1438
+ ] });
1439
+ }
1440
+ function HeadingSelect({ editor, config }) {
1441
+ const levels = config.headingLevels === false ? [] : config.headingLevels;
1442
+ const L = config.labels;
1443
+ const current = (() => {
1444
+ for (const l of levels) if (editor.isActive("heading", { level: l })) return `H${l}`;
1445
+ if (editor.isActive("paragraph")) return L.paragraph;
1446
+ return L.headings;
1447
+ })();
1448
+ return /* @__PURE__ */ jsxRuntime.jsx(
1449
+ Popover,
1450
+ {
1451
+ button: ({ open, toggle }) => /* @__PURE__ */ jsxRuntime.jsxs(
1452
+ "button",
1453
+ {
1454
+ type: "button",
1455
+ className: "osam-btn osam-btn--select",
1456
+ "data-open": open || void 0,
1457
+ onClick: toggle,
1458
+ title: L.headings,
1459
+ children: [
1460
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: current }),
1461
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "chevron" })
1462
+ ]
1463
+ }
1464
+ ),
1465
+ children: ({ close }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-menu", children: [
1466
+ /* @__PURE__ */ jsxRuntime.jsx(
1467
+ "button",
1468
+ {
1469
+ type: "button",
1470
+ "data-active": editor.isActive("paragraph") || void 0,
1471
+ onClick: () => {
1472
+ editor.chain().focus().setParagraph().run();
1473
+ close();
1474
+ },
1475
+ children: L.paragraph
1476
+ }
1477
+ ),
1478
+ levels.map((l) => /* @__PURE__ */ jsxRuntime.jsx(
1479
+ "button",
1480
+ {
1481
+ type: "button",
1482
+ className: `osam-menu__h${l}`,
1483
+ "data-active": editor.isActive("heading", { level: l }) || void 0,
1484
+ onClick: () => {
1485
+ editor.chain().focus().toggleHeading({ level: l }).run();
1486
+ close();
1487
+ },
1488
+ children: L[`h${l}`] ?? `Heading ${l}`
1489
+ },
1490
+ l
1491
+ ))
1492
+ ] })
1493
+ }
1494
+ );
1495
+ }
1496
+ function ColorButton({ editor, config, variant }) {
1497
+ const settings = variant === "color" ? config.color : config.highlight;
1498
+ if (!settings) return null;
1499
+ const L = config.labels;
1500
+ const label = variant === "color" ? L.color : L.highlight;
1501
+ const active = variant === "color" ? editor.getAttributes("textStyle").color : editor.getAttributes("highlight").color;
1502
+ return /* @__PURE__ */ jsxRuntime.jsx(
1503
+ Popover,
1504
+ {
1505
+ button: ({ open, toggle }) => /* @__PURE__ */ jsxRuntime.jsxs(
1506
+ "button",
1507
+ {
1508
+ type: "button",
1509
+ className: "osam-btn osam-btn--color",
1510
+ "data-open": open || void 0,
1511
+ title: label,
1512
+ "aria-label": label,
1513
+ onClick: toggle,
1514
+ children: [
1515
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: variant === "color" ? "color" : "highlight" }),
1516
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "osam-btn__swatch", style: { background: active || "transparent" } })
1517
+ ]
1518
+ }
1519
+ ),
1520
+ children: ({ close }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-swatches", children: [
1521
+ settings.colors.map((c) => /* @__PURE__ */ jsxRuntime.jsx(
1522
+ "button",
1523
+ {
1524
+ type: "button",
1525
+ className: "osam-swatch",
1526
+ style: { background: c },
1527
+ title: c,
1528
+ "data-active": active === c || void 0,
1529
+ onClick: () => {
1530
+ if (variant === "color") editor.chain().focus().setColor(c).run();
1531
+ else editor.chain().focus().toggleHighlight({ color: c }).run();
1532
+ close();
1533
+ }
1534
+ },
1535
+ c
1536
+ )),
1537
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "osam-swatch osam-swatch--custom", title: "Custom", children: [
1538
+ /* @__PURE__ */ jsxRuntime.jsx(
1539
+ "input",
1540
+ {
1541
+ type: "color",
1542
+ onChange: (e) => {
1543
+ const c = e.target.value;
1544
+ if (variant === "color") editor.chain().focus().setColor(c).run();
1545
+ else editor.chain().focus().setHighlight({ color: c }).run();
1546
+ }
1547
+ }
1548
+ ),
1549
+ "+"
1550
+ ] }),
1551
+ /* @__PURE__ */ jsxRuntime.jsx(
1552
+ "button",
1553
+ {
1554
+ type: "button",
1555
+ className: "osam-btn-text osam-swatches__clear",
1556
+ onClick: () => {
1557
+ if (variant === "color") editor.chain().focus().unsetColor().run();
1558
+ else editor.chain().focus().unsetHighlight().run();
1559
+ close();
1560
+ },
1561
+ children: L.clearFormatting
1562
+ }
1563
+ )
1564
+ ] })
1565
+ }
1566
+ );
1567
+ }
1568
+ function buildRel(base, nofollow, sponsored) {
1569
+ const parts = new Set((base ?? "").split(/\s+/).filter(Boolean));
1570
+ parts.delete("nofollow");
1571
+ parts.delete("sponsored");
1572
+ if (nofollow) parts.add("nofollow");
1573
+ if (sponsored) parts.add("sponsored");
1574
+ const out = [...parts].join(" ");
1575
+ return out || null;
1576
+ }
1577
+ function LinkButton({ editor, config }) {
1578
+ if (config.link === false) return null;
1579
+ const link = config.link;
1580
+ const L = config.labels;
1581
+ return /* @__PURE__ */ jsxRuntime.jsx(
1582
+ Popover,
1583
+ {
1584
+ button: ({ open, toggle }) => /* @__PURE__ */ jsxRuntime.jsx(
1585
+ "button",
1586
+ {
1587
+ type: "button",
1588
+ className: "osam-btn",
1589
+ "data-active": editor.isActive("link") || void 0,
1590
+ "data-open": open || void 0,
1591
+ title: L.link,
1592
+ "aria-label": L.link,
1593
+ onClick: toggle,
1594
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "link" })
1595
+ }
1596
+ ),
1597
+ children: ({ close }) => /* @__PURE__ */ jsxRuntime.jsx(LinkForm, { editor, config, link, onDone: close })
1598
+ }
1599
+ );
1600
+ }
1601
+ function LinkForm({
1602
+ editor,
1603
+ config,
1604
+ link,
1605
+ onDone
1606
+ }) {
1607
+ const L = config.labels;
1608
+ const prev = editor.getAttributes("link");
1609
+ const selectionEmpty = editor.state.selection.empty && !editor.isActive("link");
1610
+ const relInit = prev.rel ?? link.defaultRel;
1611
+ const [href, setHref] = React10__namespace.useState(prev.href ?? "");
1612
+ const [text, setText] = React10__namespace.useState("");
1613
+ const [newTab, setNewTab] = React10__namespace.useState(
1614
+ prev.target ? prev.target === "_blank" : false
1615
+ );
1616
+ const [nofollow, setNofollow] = React10__namespace.useState(/(^|\s)nofollow(\s|$)/.test(relInit ?? ""));
1617
+ const [sponsored, setSponsored] = React10__namespace.useState(/(^|\s)sponsored(\s|$)/.test(relInit ?? ""));
1618
+ function apply(e) {
1619
+ e.preventDefault();
1620
+ const url = href.trim();
1621
+ if (!url) return;
1622
+ const rel = link.allowRelAttributes ? buildRel(link.defaultRel, nofollow, sponsored) : link.defaultRel;
1623
+ const attrs = { href: url };
1624
+ if (link.allowTargetBlank) attrs.target = newTab ? "_blank" : null;
1625
+ if (rel !== void 0) attrs.rel = rel;
1626
+ let chain = editor.chain().focus();
1627
+ if (selectionEmpty) {
1628
+ const label = text.trim() || url;
1629
+ chain = chain.insertContent({
1630
+ type: "text",
1631
+ text: label,
1632
+ marks: [{ type: "link", attrs }]
1633
+ });
1634
+ } else {
1635
+ chain = chain.extendMarkRange("link").setLink(attrs);
1636
+ }
1637
+ chain.run();
1638
+ onDone();
1639
+ }
1640
+ function remove() {
1641
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
1642
+ onDone();
1643
+ }
1644
+ return /* @__PURE__ */ jsxRuntime.jsxs("form", { className: "osam-linkform", onSubmit: apply, children: [
1645
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "osam-field", children: [
1646
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: L["dialog.url"] }),
1647
+ /* @__PURE__ */ jsxRuntime.jsx(
1648
+ "input",
1649
+ {
1650
+ type: "url",
1651
+ autoFocus: true,
1652
+ value: href,
1653
+ placeholder: "https://\u2026",
1654
+ onChange: (e) => setHref(e.target.value)
1655
+ }
1656
+ )
1657
+ ] }),
1658
+ selectionEmpty && /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "osam-field", children: [
1659
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: L["dialog.linkText"] }),
1660
+ /* @__PURE__ */ jsxRuntime.jsx("input", { value: text, placeholder: href, onChange: (e) => setText(e.target.value) })
1661
+ ] }),
1662
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-checks", children: [
1663
+ link.allowTargetBlank && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1664
+ /* @__PURE__ */ jsxRuntime.jsx("input", { type: "checkbox", checked: newTab, onChange: (e) => setNewTab(e.target.checked) }),
1665
+ L["dialog.openInNewTab"]
1666
+ ] }),
1667
+ link.allowRelAttributes && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1668
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1669
+ /* @__PURE__ */ jsxRuntime.jsx(
1670
+ "input",
1671
+ {
1672
+ type: "checkbox",
1673
+ checked: nofollow,
1674
+ onChange: (e) => setNofollow(e.target.checked)
1675
+ }
1676
+ ),
1677
+ 'rel="',
1678
+ L["dialog.nofollow"],
1679
+ '"'
1680
+ ] }),
1681
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
1682
+ /* @__PURE__ */ jsxRuntime.jsx(
1683
+ "input",
1684
+ {
1685
+ type: "checkbox",
1686
+ checked: sponsored,
1687
+ onChange: (e) => setSponsored(e.target.checked)
1688
+ }
1689
+ ),
1690
+ 'rel="',
1691
+ L["dialog.sponsored"],
1692
+ '"'
1693
+ ] })
1694
+ ] })
1695
+ ] }),
1696
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-media__actions", children: [
1697
+ editor.isActive("link") && /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "osam-btn-text", onClick: remove, children: L.unlink }),
1698
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "osam-btn-text", onClick: onDone, children: L["dialog.cancel"] }),
1699
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "osam-btn-primary", children: L["dialog.insert"] })
1700
+ ] })
1701
+ ] });
1702
+ }
1703
+
1704
+ // src/storage/types.ts
1705
+ function detectKind(file) {
1706
+ if (file.type.startsWith("image/")) return "image";
1707
+ if (file.type.startsWith("video/")) return "video";
1708
+ if (file.type === "application/pdf") return "pdf";
1709
+ const ext = file.name.split(".").pop()?.toLowerCase();
1710
+ if (ext && ["png", "jpg", "jpeg", "gif", "webp", "avif", "svg", "bmp"].includes(ext)) return "image";
1711
+ if (ext && ["mp4", "webm", "mov", "m4v", "mkv", "avi"].includes(ext)) return "video";
1712
+ if (ext === "pdf") return "pdf";
1713
+ throw new Error(
1714
+ `osameditor: unsupported file type "${file.type || file.name}". Only images, video and PDF are allowed.`
1715
+ );
1716
+ }
1717
+
1718
+ // src/ui/useUpload.ts
1719
+ function useUpload(handler, config) {
1720
+ const [state, setState] = React10__namespace.useState({ busy: false, progress: 0, error: null });
1721
+ const abortRef = React10__namespace.useRef(null);
1722
+ const reset = React10__namespace.useCallback(() => setState({ busy: false, progress: 0, error: null }), []);
1723
+ const cancel = React10__namespace.useCallback(() => {
1724
+ abortRef.current?.abort();
1725
+ abortRef.current = null;
1726
+ reset();
1727
+ }, [reset]);
1728
+ const upload = React10__namespace.useCallback(
1729
+ async (file) => {
1730
+ if (!handler) {
1731
+ setState((s) => ({ ...s, error: "No upload handler configured." }));
1732
+ return null;
1733
+ }
1734
+ let kind;
1735
+ try {
1736
+ kind = detectKind(file);
1737
+ } catch (e) {
1738
+ setState({ busy: false, progress: 0, error: e.message });
1739
+ return null;
1740
+ }
1741
+ const max = (kind === "image" && config.image ? config.image.maxSize : void 0) ?? config.upload.maxSize;
1742
+ if (max && file.size > max) {
1743
+ setState({
1744
+ busy: false,
1745
+ progress: 0,
1746
+ error: `${config.labels["error.tooLarge"]} (${(file.size / 1e6).toFixed(1)} MB > ${(max / 1e6).toFixed(1)} MB)`
1747
+ });
1748
+ return null;
1749
+ }
1750
+ const controller = new AbortController();
1751
+ abortRef.current = controller;
1752
+ setState({ busy: true, progress: 0, error: null });
1753
+ try {
1754
+ const result = await handler(file, {
1755
+ kind,
1756
+ signal: controller.signal,
1757
+ onProgress: (p) => setState((s) => ({ ...s, progress: p }))
1758
+ });
1759
+ setState({ busy: false, progress: 100, error: null });
1760
+ abortRef.current = null;
1761
+ return { ...result, kind: result.kind ?? kind };
1762
+ } catch (e) {
1763
+ if (e?.name === "AbortError") {
1764
+ reset();
1765
+ return null;
1766
+ }
1767
+ setState({
1768
+ busy: false,
1769
+ progress: 0,
1770
+ error: e?.message || config.labels["error.uploadFailed"]
1771
+ });
1772
+ abortRef.current = null;
1773
+ return null;
1774
+ }
1775
+ },
1776
+ [handler, config, reset]
1777
+ );
1778
+ return { ...state, upload, cancel, reset };
1779
+ }
1780
+
1781
+ // src/extensions/embed-utils.ts
1782
+ var VIDEO_FILE = /\.(mp4|webm|ogg|ogv|mov|m4v|m3u8)(\?.*)?$/i;
1783
+ function hostOf(url) {
1784
+ try {
1785
+ return new URL(url).hostname.replace(/^www\./, "");
1786
+ } catch {
1787
+ return "";
1788
+ }
1789
+ }
1790
+ function isHostAllowed(url, allowed) {
1791
+ if (allowed === "*") return true;
1792
+ const host = hostOf(url);
1793
+ if (!host) return false;
1794
+ return allowed.some((h) => host === h || host.endsWith(`.${h}`));
1795
+ }
1796
+ function youtubeId(url) {
1797
+ try {
1798
+ const u = new URL(url);
1799
+ if (u.hostname.includes("youtu.be")) return u.pathname.slice(1) || null;
1800
+ if (u.pathname.startsWith("/shorts/")) return u.pathname.split("/")[2] || null;
1801
+ if (u.pathname.startsWith("/embed/")) return u.pathname.split("/")[2] || null;
1802
+ return u.searchParams.get("v");
1803
+ } catch {
1804
+ return null;
1805
+ }
1806
+ }
1807
+ function vimeoId(url) {
1808
+ const m = url.match(/vimeo\.com\/(?:video\/)?(\d+)/);
1809
+ return m ? m[1] : null;
1810
+ }
1811
+ function instagramEmbedUrl(url) {
1812
+ const m = url.match(/instagram\.com\/(p|reel|tv)\/([A-Za-z0-9_-]+)/);
1813
+ if (!m) return null;
1814
+ return `https://www.instagram.com/${m[1]}/${m[2]}/embed`;
1815
+ }
1816
+ function resolveEmbed(raw, opts) {
1817
+ const url = raw.trim();
1818
+ if (!url) return null;
1819
+ if (opts.treatAsPdf || /\.pdf(\?.*)?$/i.test(url)) {
1820
+ return { provider: "pdf", src: url, originalUrl: url };
1821
+ }
1822
+ const yt = youtubeId(url);
1823
+ if (yt) {
1824
+ return {
1825
+ provider: "youtube",
1826
+ src: `https://www.youtube-nocookie.com/embed/${yt}`,
1827
+ originalUrl: url
1828
+ };
1829
+ }
1830
+ const vim = vimeoId(url);
1831
+ if (vim) {
1832
+ return { provider: "vimeo", src: `https://player.vimeo.com/video/${vim}`, originalUrl: url };
1833
+ }
1834
+ const insta = instagramEmbedUrl(url);
1835
+ if (insta) return { provider: "instagram", src: insta, originalUrl: url };
1836
+ if (/(twitter\.com|x\.com)\/\w+\/status\/\d+/.test(url)) {
1837
+ return { provider: "twitter", src: url, originalUrl: url };
1838
+ }
1839
+ if (opts.treatAsVideo || VIDEO_FILE.test(url)) {
1840
+ return { provider: "video", src: url, originalUrl: url };
1841
+ }
1842
+ if (isHostAllowed(url, opts.allowedHosts)) {
1843
+ return { provider: "iframe", src: url, originalUrl: url };
1844
+ }
1845
+ return null;
1846
+ }
1847
+
1848
+ // src/mediaLibrary.ts
1849
+ async function resolveHeaders(h) {
1850
+ if (!h) return {};
1851
+ return typeof h === "function" ? await h() : h;
1852
+ }
1853
+ function normalize(list) {
1854
+ return list.map((it) => typeof it === "string" ? { url: it } : it).filter((it) => it && typeof it.url === "string");
1855
+ }
1856
+ var DEFAULT_RESOLVE = (res) => {
1857
+ if (Array.isArray(res)) return res;
1858
+ return res?.items ?? res?.data ?? res?.images ?? res?.urls ?? res?.results ?? [];
1859
+ };
1860
+ async function loadMediaLibrary(cfg) {
1861
+ if (typeof cfg.list === "function") {
1862
+ return normalize(await cfg.list());
1863
+ }
1864
+ const res = await fetch(cfg.list, {
1865
+ headers: await resolveHeaders(cfg.headers),
1866
+ credentials: cfg.credentials ?? "same-origin"
1867
+ });
1868
+ if (!res.ok) throw new Error(`osameditor: media library GET failed (${res.status})`);
1869
+ const json = await res.json();
1870
+ return normalize((cfg.resolveList ?? DEFAULT_RESOLVE)(json));
1871
+ }
1872
+ async function saveToMediaLibrary(cfg, url, file) {
1873
+ if (!cfg.save) return;
1874
+ try {
1875
+ if (typeof cfg.save === "function") {
1876
+ await cfg.save(url, file);
1877
+ return;
1878
+ }
1879
+ await fetch(cfg.save, {
1880
+ method: "POST",
1881
+ headers: { "Content-Type": "application/json", ...await resolveHeaders(cfg.headers) },
1882
+ credentials: cfg.credentials ?? "same-origin",
1883
+ body: JSON.stringify({ url, name: file?.name })
1884
+ });
1885
+ } catch (e) {
1886
+ console.warn("osameditor: could not save to media library", e);
1887
+ }
1888
+ }
1889
+ var META = {
1890
+ image: { icon: "image", labelKey: "image" },
1891
+ imageUrl: { icon: "image-url", labelKey: "imageUrl" },
1892
+ mediaLibrary: { icon: "library", labelKey: "mediaLibrary" },
1893
+ youtube: { icon: "youtube", labelKey: "youtube" },
1894
+ instagram: { icon: "instagram", labelKey: "instagram" },
1895
+ video: { icon: "video", labelKey: "video" },
1896
+ embed: { icon: "embed", labelKey: "embed" },
1897
+ pdf: { icon: "pdf", labelKey: "pdf" }
1898
+ };
1899
+ function MediaButton({ kind, editor, config, handler }) {
1900
+ const meta = META[kind];
1901
+ const label = config.labels[meta.labelKey] ?? kind;
1902
+ return /* @__PURE__ */ jsxRuntime.jsx(
1903
+ Popover,
1904
+ {
1905
+ button: ({ open, toggle }) => /* @__PURE__ */ jsxRuntime.jsx(
1906
+ "button",
1907
+ {
1908
+ type: "button",
1909
+ className: "osam-btn",
1910
+ "data-open": open || void 0,
1911
+ title: label,
1912
+ "aria-label": label,
1913
+ onClick: toggle,
1914
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: meta.icon })
1915
+ }
1916
+ ),
1917
+ children: ({ close }) => /* @__PURE__ */ jsxRuntime.jsx(MediaPanel, { kind, editor, config, handler, onDone: close })
1918
+ }
1919
+ );
1920
+ }
1921
+ function MediaPanel({ kind, editor, config, handler, onDone }) {
1922
+ const L = config.labels;
1923
+ const up = useUpload(handler, config);
1924
+ const isImageKind = kind === "image" || kind === "imageUrl" || kind === "mediaLibrary";
1925
+ const canUpload = isImageKind && config.image && config.image.allowUpload || kind === "video" && config.upload.hasHandler && config.upload.video || kind === "pdf" && config.upload.hasHandler && config.upload.pdf;
1926
+ const canUrl = !isImageKind ? true : kind !== "mediaLibrary" && !!(config.image && config.image.allowUrl);
1927
+ const canLibrary = isImageKind && !!config.mediaLibrary;
1928
+ const initialTab = kind === "mediaLibrary" && canLibrary ? "library" : canUpload ? "upload" : canUrl ? "url" : "library";
1929
+ const [tab, setTab] = React10__namespace.useState(initialTab);
1930
+ const [url, setUrl] = React10__namespace.useState("");
1931
+ const [alt, setAlt] = React10__namespace.useState("");
1932
+ const [title, setTitle] = React10__namespace.useState("");
1933
+ const [caption, setCaption] = React10__namespace.useState("");
1934
+ const [ratio, setRatio] = React10__namespace.useState(config.embed ? config.embed.defaultRatio : "16/9");
1935
+ const [err, setErr] = React10__namespace.useState(null);
1936
+ const fileRef = React10__namespace.useRef(null);
1937
+ const [lib, setLib] = React10__namespace.useState(null);
1938
+ const [libLoading, setLibLoading] = React10__namespace.useState(false);
1939
+ React10__namespace.useEffect(() => {
1940
+ if (tab !== "library" || !config.mediaLibrary || lib) return;
1941
+ setLibLoading(true);
1942
+ loadMediaLibrary(config.mediaLibrary).then(setLib).catch((e) => setErr(e?.message || "Could not load media library")).finally(() => setLibLoading(false));
1943
+ }, [tab, config.mediaLibrary, lib]);
1944
+ const acceptFor = isImageKind ? config.image && config.image.accept || "image/*" : kind === "video" ? "video/*" : kind === "pdf" ? "application/pdf" : "image/*,video/*,application/pdf";
1945
+ function insertImage(src) {
1946
+ editor.chain().focus().insertContent({
1947
+ type: "image",
1948
+ attrs: { src, alt: alt || null, title: title || null, caption: caption || null }
1949
+ }).run();
1950
+ onDone();
1951
+ }
1952
+ function insertMediaUrl(raw, forceKind) {
1953
+ const resolved = resolveEmbed(raw, {
1954
+ allowedHosts: config.embed ? config.embed.allowedHosts : "*",
1955
+ treatAsPdf: forceKind === "pdf" || kind === "pdf",
1956
+ treatAsVideo: forceKind === "video" || kind === "video"
1957
+ });
1958
+ if (kind === "youtube" && (!resolved || resolved.provider !== "youtube")) {
1959
+ setErr("That doesn't look like a YouTube URL.");
1960
+ return;
1961
+ }
1962
+ if (kind === "instagram" && (!resolved || resolved.provider !== "instagram")) {
1963
+ setErr("That doesn't look like an Instagram post/reel URL.");
1964
+ return;
1965
+ }
1966
+ if (!resolved) {
1967
+ setErr(L["error.badHost"]);
1968
+ return;
1969
+ }
1970
+ editor.chain().focus().insertContent({
1971
+ type: "embed",
1972
+ attrs: {
1973
+ src: resolved.src,
1974
+ provider: resolved.provider,
1975
+ originalUrl: resolved.originalUrl,
1976
+ ratio,
1977
+ title: title || null
1978
+ }
1979
+ }).run();
1980
+ onDone();
1981
+ }
1982
+ async function onFile(file) {
1983
+ if (!file) return;
1984
+ setErr(null);
1985
+ const result = await up.upload(file);
1986
+ if (!result) return;
1987
+ if (result.kind === "image") {
1988
+ if (config.mediaLibrary) void saveToMediaLibrary(config.mediaLibrary, result.url, file);
1989
+ insertImage(result.url);
1990
+ } else {
1991
+ insertMediaUrl(result.url, result.kind);
1992
+ }
1993
+ }
1994
+ const showMeta = isImageKind;
1995
+ const tabs = [];
1996
+ if (canUpload) tabs.push(["upload", L["dialog.upload"]]);
1997
+ if (canUrl) tabs.push(["url", "URL"]);
1998
+ if (canLibrary) tabs.push(["library", L["dialog.library"]]);
1999
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-media", children: [
2000
+ tabs.length > 1 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "osam-media__tabs", children: tabs.map(([id, lbl]) => /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", "data-active": tab === id, onClick: () => setTab(id), children: lbl }, id)) }),
2001
+ tab === "library" && canLibrary && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-media__lib", children: [
2002
+ libLoading && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "osam-hint", children: "Loading\u2026" }),
2003
+ lib && lib.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "osam-hint", children: "No images yet." }),
2004
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "osam-media__grid", children: lib?.map((it) => /* @__PURE__ */ jsxRuntime.jsx(
2005
+ "button",
2006
+ {
2007
+ type: "button",
2008
+ className: "osam-media__thumb",
2009
+ title: it.name || it.url,
2010
+ onClick: () => {
2011
+ editor.chain().focus().insertContent({ type: "image", attrs: { src: it.url } }).run();
2012
+ onDone();
2013
+ },
2014
+ children: /* @__PURE__ */ jsxRuntime.jsx("img", { src: it.thumbnail || it.url, alt: it.name || "" })
2015
+ },
2016
+ it.url
2017
+ )) })
2018
+ ] }),
2019
+ tab === "upload" && canUpload && /* @__PURE__ */ jsxRuntime.jsxs(
2020
+ "div",
2021
+ {
2022
+ className: "osam-media__drop",
2023
+ onDragOver: (e) => e.preventDefault(),
2024
+ onDrop: (e) => {
2025
+ e.preventDefault();
2026
+ onFile(e.dataTransfer.files?.[0]);
2027
+ },
2028
+ onClick: () => fileRef.current?.click(),
2029
+ "data-busy": up.busy || void 0,
2030
+ children: [
2031
+ /* @__PURE__ */ jsxRuntime.jsx(
2032
+ "input",
2033
+ {
2034
+ ref: fileRef,
2035
+ type: "file",
2036
+ accept: acceptFor,
2037
+ hidden: true,
2038
+ onChange: (e) => onFile(e.target.files?.[0] || void 0)
2039
+ }
2040
+ ),
2041
+ up.busy ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2042
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "osam-media__progress", children: /* @__PURE__ */ jsxRuntime.jsx("span", { style: { width: `${up.progress}%` } }) }),
2043
+ /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
2044
+ L["dialog.uploading"],
2045
+ " ",
2046
+ up.progress,
2047
+ "%",
2048
+ " ",
2049
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "osam-link", onClick: up.cancel, children: L["dialog.cancel"] })
2050
+ ] })
2051
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("p", { children: [
2052
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "image" }),
2053
+ " ",
2054
+ L["dialog.upload"],
2055
+ " ",
2056
+ /* @__PURE__ */ jsxRuntime.jsx("br", {}),
2057
+ /* @__PURE__ */ jsxRuntime.jsxs("small", { children: [
2058
+ L["dialog.or"],
2059
+ " drag & drop"
2060
+ ] })
2061
+ ] })
2062
+ ]
2063
+ }
2064
+ ),
2065
+ tab === "url" && canUrl && /* @__PURE__ */ jsxRuntime.jsxs(
2066
+ "form",
2067
+ {
2068
+ onSubmit: (e) => {
2069
+ e.preventDefault();
2070
+ setErr(null);
2071
+ if (!url.trim()) return;
2072
+ if (kind === "imageUrl" || kind === "image") insertImage(url.trim());
2073
+ else insertMediaUrl(url.trim());
2074
+ },
2075
+ children: [
2076
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "osam-field", children: [
2077
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: L["dialog.url"] }),
2078
+ /* @__PURE__ */ jsxRuntime.jsx(
2079
+ "input",
2080
+ {
2081
+ type: "url",
2082
+ value: url,
2083
+ autoFocus: true,
2084
+ placeholder: "https://\u2026",
2085
+ onChange: (e) => setUrl(e.target.value)
2086
+ }
2087
+ )
2088
+ ] }),
2089
+ kind === "embed" && config.embed && config.embed.allowedHosts !== "*" && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "osam-hint", children: [
2090
+ "Allowed: ",
2091
+ config.embed.allowedHosts.slice(0, 6).join(", "),
2092
+ config.embed.allowedHosts.length > 6 ? "\u2026" : ""
2093
+ ] }),
2094
+ showMeta && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2095
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "osam-field", children: [
2096
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: L["dialog.alt"] }),
2097
+ /* @__PURE__ */ jsxRuntime.jsx("input", { value: alt, onChange: (e) => setAlt(e.target.value) })
2098
+ ] }),
2099
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "osam-field", children: [
2100
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: L["dialog.title"] }),
2101
+ /* @__PURE__ */ jsxRuntime.jsx("input", { value: title, onChange: (e) => setTitle(e.target.value) })
2102
+ ] }),
2103
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "osam-field", children: [
2104
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: L["dialog.caption"] }),
2105
+ /* @__PURE__ */ jsxRuntime.jsx("input", { value: caption, onChange: (e) => setCaption(e.target.value) })
2106
+ ] })
2107
+ ] }),
2108
+ (kind === "embed" || kind === "youtube" || kind === "instagram" || kind === "video") && /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "osam-field", children: [
2109
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: L["dialog.ratio"] }),
2110
+ /* @__PURE__ */ jsxRuntime.jsxs("select", { value: ratio, onChange: (e) => setRatio(e.target.value), children: [
2111
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "16/9", children: "16 : 9" }),
2112
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "4/3", children: "4 : 3" }),
2113
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "1/1", children: "1 : 1" }),
2114
+ /* @__PURE__ */ jsxRuntime.jsx("option", { value: "9/16", children: "9 : 16" })
2115
+ ] })
2116
+ ] }),
2117
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-media__actions", children: [
2118
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "osam-btn-text", onClick: onDone, children: L["dialog.cancel"] }),
2119
+ /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", className: "osam-btn-primary", children: L["dialog.insert"] })
2120
+ ] })
2121
+ ]
2122
+ }
2123
+ ),
2124
+ (err || up.error) && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "osam-error", children: err || up.error })
2125
+ ] });
2126
+ }
2127
+ function Btn({
2128
+ icon,
2129
+ label,
2130
+ active,
2131
+ disabled,
2132
+ onClick
2133
+ }) {
2134
+ return /* @__PURE__ */ jsxRuntime.jsx(
2135
+ "button",
2136
+ {
2137
+ type: "button",
2138
+ className: "osam-btn",
2139
+ "data-active": active || void 0,
2140
+ disabled,
2141
+ title: label,
2142
+ "aria-label": label,
2143
+ "aria-pressed": active || void 0,
2144
+ onMouseDown: (e) => e.preventDefault(),
2145
+ onClick,
2146
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon })
2147
+ }
2148
+ );
2149
+ }
2150
+ function Toolbar({ editor, config, uploadHandler, view }) {
2151
+ useEditorSync(editor);
2152
+ if (config.toolbar === false) return null;
2153
+ const L = config.labels;
2154
+ const can = editor.can();
2155
+ const locked = !!view?.sourceMode;
2156
+ const render = (item, i) => {
2157
+ const key = `${item}-${i}`;
2158
+ if (item === "fullscreen") {
2159
+ if (!view?.onToggleFullscreen) return null;
2160
+ return /* @__PURE__ */ jsxRuntime.jsx(
2161
+ Btn,
2162
+ {
2163
+ icon: view.fullscreen ? "minimize" : "maximize",
2164
+ label: view.fullscreen ? L.fullscreenExit : L.fullscreen,
2165
+ active: view.fullscreen,
2166
+ onClick: view.onToggleFullscreen
2167
+ },
2168
+ key
2169
+ );
2170
+ }
2171
+ if (item === "source") {
2172
+ if (!config.html || !config.html.sourceView || !view?.onToggleSource) return null;
2173
+ return /* @__PURE__ */ jsxRuntime.jsx(
2174
+ Btn,
2175
+ {
2176
+ icon: "source",
2177
+ label: L.source,
2178
+ active: view.sourceMode,
2179
+ onClick: view.onToggleSource
2180
+ },
2181
+ key
2182
+ );
2183
+ }
2184
+ if (item === "|") return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "osam-sep", "aria-hidden": "true" }, key);
2185
+ if (item === "spacer") return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "osam-spacer" }, key);
2186
+ if (locked) {
2187
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "osam-btn osam-btn--ghost", "aria-hidden": "true" }, key);
2188
+ }
2189
+ switch (item) {
2190
+ case "bold":
2191
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "bold", label: L.bold, active: editor.isActive("bold"), disabled: !can.toggleBold?.(), onClick: () => editor.chain().focus().toggleBold().run() }, key);
2192
+ case "italic":
2193
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "italic", label: L.italic, active: editor.isActive("italic"), onClick: () => editor.chain().focus().toggleItalic().run() }, key);
2194
+ case "underline":
2195
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "underline", label: L.underline, active: editor.isActive("underline"), onClick: () => editor.chain().focus().toggleUnderline().run() }, key);
2196
+ case "strike":
2197
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "strike", label: L.strike, active: editor.isActive("strike"), onClick: () => editor.chain().focus().toggleStrike().run() }, key);
2198
+ case "code":
2199
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "code", label: L.code, active: editor.isActive("code"), onClick: () => editor.chain().focus().toggleCode().run() }, key);
2200
+ case "clearFormatting":
2201
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "clear", label: L.clearFormatting, onClick: () => editor.chain().focus().unsetAllMarks().clearNodes().run() }, key);
2202
+ case "color":
2203
+ return /* @__PURE__ */ jsxRuntime.jsx(ColorButton, { editor, config, variant: "color" }, key);
2204
+ case "highlight":
2205
+ return /* @__PURE__ */ jsxRuntime.jsx(ColorButton, { editor, config, variant: "highlight" }, key);
2206
+ case "headings":
2207
+ return config.headingLevels === false ? null : /* @__PURE__ */ jsxRuntime.jsx(HeadingSelect, { editor, config }, key);
2208
+ case "paragraph":
2209
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "align-left", label: L.paragraph, active: editor.isActive("paragraph"), onClick: () => editor.chain().focus().setParagraph().run() }, key);
2210
+ case "h1":
2211
+ case "h2":
2212
+ case "h3":
2213
+ case "h4":
2214
+ case "h5":
2215
+ case "h6": {
2216
+ const level = Number(item[1]);
2217
+ if (config.headingLevels === false || !config.headingLevels.includes(level)) return null;
2218
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2219
+ "button",
2220
+ {
2221
+ type: "button",
2222
+ className: "osam-btn osam-btn--text",
2223
+ "data-active": editor.isActive("heading", { level }) || void 0,
2224
+ title: L[`h${level}`],
2225
+ onMouseDown: (e) => e.preventDefault(),
2226
+ onClick: () => editor.chain().focus().toggleHeading({ level }).run(),
2227
+ children: [
2228
+ "H",
2229
+ level
2230
+ ]
2231
+ },
2232
+ key
2233
+ );
2234
+ }
2235
+ case "alignLeft":
2236
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "align-left", label: L.alignLeft, active: editor.isActive({ textAlign: "left" }), disabled: !config.textAlign, onClick: () => editor.chain().focus().setTextAlign("left").run() }, key);
2237
+ case "alignCenter":
2238
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "align-center", label: L.alignCenter, active: editor.isActive({ textAlign: "center" }), disabled: !config.textAlign, onClick: () => editor.chain().focus().setTextAlign("center").run() }, key);
2239
+ case "alignRight":
2240
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "align-right", label: L.alignRight, active: editor.isActive({ textAlign: "right" }), disabled: !config.textAlign, onClick: () => editor.chain().focus().setTextAlign("right").run() }, key);
2241
+ case "alignJustify":
2242
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "align-justify", label: L.alignJustify, active: editor.isActive({ textAlign: "justify" }), disabled: !config.textAlign, onClick: () => editor.chain().focus().setTextAlign("justify").run() }, key);
2243
+ case "hardBreak":
2244
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "return", label: L.hardBreak, onClick: () => editor.chain().focus().setHardBreak().run() }, key);
2245
+ case "bulletList":
2246
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "list-bullet", label: L.bulletList, active: editor.isActive("bulletList"), onClick: () => editor.chain().focus().toggleBulletList().run() }, key);
2247
+ case "orderedList":
2248
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "list-ordered", label: L.orderedList, active: editor.isActive("orderedList"), onClick: () => editor.chain().focus().toggleOrderedList().run() }, key);
2249
+ case "taskList":
2250
+ return config.taskList ? /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "list-check", label: L.taskList, active: editor.isActive("taskList"), onClick: () => editor.chain().focus().toggleTaskList().run() }, key) : null;
2251
+ case "indent":
2252
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "indent", label: L.indent, disabled: !can.sinkListItem?.("listItem"), onClick: () => editor.chain().focus().sinkListItem("listItem").run() }, key);
2253
+ case "outdent":
2254
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "outdent", label: L.outdent, disabled: !can.liftListItem?.("listItem"), onClick: () => editor.chain().focus().liftListItem("listItem").run() }, key);
2255
+ case "link":
2256
+ return /* @__PURE__ */ jsxRuntime.jsx(LinkButton, { editor, config }, key);
2257
+ case "unlink":
2258
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "unlink", label: L.unlink, disabled: !editor.isActive("link"), onClick: () => editor.chain().focus().extendMarkRange("link").unsetLink().run() }, key);
2259
+ case "blockquote":
2260
+ return config.blockquote ? /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "quote", label: L.blockquote, active: editor.isActive("blockquote"), onClick: () => editor.chain().focus().toggleBlockquote().run() }, key) : null;
2261
+ case "codeBlock":
2262
+ return config.codeBlock !== false ? /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "code-block", label: L.codeBlock, active: editor.isActive("codeBlock"), onClick: () => editor.chain().focus().toggleCodeBlock().run() }, key) : null;
2263
+ case "horizontalRule":
2264
+ return config.horizontalRule ? /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "hr", label: L.horizontalRule, onClick: () => editor.chain().focus().setHorizontalRule().run() }, key) : null;
2265
+ case "mediaLibrary":
2266
+ return config.image && config.mediaLibrary ? /* @__PURE__ */ jsxRuntime.jsx(MediaButton, { kind: "mediaLibrary", editor, config, handler: uploadHandler }, key) : null;
2267
+ case "customHtml":
2268
+ return config.html && config.html.customBlock ? /* @__PURE__ */ jsxRuntime.jsx(
2269
+ Btn,
2270
+ {
2271
+ icon: "html-block",
2272
+ label: L.customHtml,
2273
+ onClick: () => editor.chain().focus().setCustomHtml().run()
2274
+ },
2275
+ key
2276
+ ) : null;
2277
+ case "image":
2278
+ case "imageUrl":
2279
+ case "youtube":
2280
+ case "instagram":
2281
+ case "video":
2282
+ case "embed":
2283
+ case "pdf": {
2284
+ if ((item === "image" || item === "imageUrl") && !config.image) return null;
2285
+ if (item !== "image" && item !== "imageUrl" && !config.embed) return null;
2286
+ if (item === "image" && !(config.image && config.image.allowUpload)) {
2287
+ return config.image && config.image.allowUrl ? /* @__PURE__ */ jsxRuntime.jsx(MediaButton, { kind: "imageUrl", editor, config, handler: uploadHandler }, key) : null;
2288
+ }
2289
+ if (item === "video" && !config.embed) return null;
2290
+ if (item === "pdf" && !config.embed) return null;
2291
+ return /* @__PURE__ */ jsxRuntime.jsx(MediaButton, { kind: item, editor, config, handler: uploadHandler }, key);
2292
+ }
2293
+ case "undo":
2294
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "undo", label: L.undo, disabled: !can.undo?.(), onClick: () => editor.chain().focus().undo().run() }, key);
2295
+ case "redo":
2296
+ return /* @__PURE__ */ jsxRuntime.jsx(Btn, { icon: "redo", label: L.redo, disabled: !can.redo?.(), onClick: () => editor.chain().focus().redo().run() }, key);
2297
+ default:
2298
+ return null;
2299
+ }
2300
+ };
2301
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "osam-toolbar", role: "toolbar", "aria-label": "Formatting", dir: config.dir, children: config.toolbar.map(render) });
2302
+ }
2303
+
2304
+ // src/theme.ts
2305
+ var MAP = {
2306
+ background: "--osam-bg",
2307
+ foreground: "--osam-fg",
2308
+ muted: "--osam-muted",
2309
+ border: "--osam-border",
2310
+ accent: "--osam-accent",
2311
+ accentText: "--osam-accent-fg",
2312
+ radius: "--osam-radius",
2313
+ fontFamily: "--osam-font",
2314
+ monoFontFamily: "--osam-font-mono",
2315
+ toolbarBackground: "--osam-toolbar-bg",
2316
+ buttonColor: "--osam-btn-fg",
2317
+ buttonHoverBackground: "--osam-hover",
2318
+ buttonActiveBackground: "--osam-active",
2319
+ buttonActiveColor: "--osam-active-fg",
2320
+ contentBackground: "--osam-content-bg",
2321
+ codeBackground: "--osam-code-bg"
2322
+ };
2323
+ function themeToStyle(theme) {
2324
+ if (!theme) return {};
2325
+ const style = {};
2326
+ for (const [key, cssVar] of Object.entries(MAP)) {
2327
+ const value = theme[key];
2328
+ if (value === void 0 || value === null) continue;
2329
+ style[cssVar] = key === "radius" && typeof value === "number" ? `${value}px` : String(value);
2330
+ }
2331
+ return style;
2332
+ }
2333
+ var isJson = (v) => !!v && typeof v === "object";
2334
+ function OsamEditor(props) {
2335
+ const {
2336
+ defaultValue,
2337
+ value,
2338
+ onChange,
2339
+ onReady,
2340
+ editable = true,
2341
+ autofocus,
2342
+ className,
2343
+ style,
2344
+ minHeight = 260,
2345
+ toolbarPosition = "top",
2346
+ stickyToolbar = false,
2347
+ defaultFullscreen = false,
2348
+ fullscreen: fullscreenProp,
2349
+ onFullscreenChange,
2350
+ ...config
2351
+ } = props;
2352
+ const controlled = value !== void 0;
2353
+ const lastEmitted = React10__namespace.useRef(null);
2354
+ const { editor, config: resolved } = useOsamEditor({
2355
+ ...config,
2356
+ content: (controlled ? value : defaultValue) ?? "",
2357
+ editable,
2358
+ autofocus,
2359
+ onCreate: (ed) => onReady?.(ed),
2360
+ onUpdate: ({ html, json, editor: editor2 }) => {
2361
+ lastEmitted.current = html;
2362
+ onChange?.(html, { json, editor: editor2 });
2363
+ }
2364
+ });
2365
+ const [fsState, setFsState] = React10__namespace.useState(defaultFullscreen);
2366
+ const fullscreen = fullscreenProp ?? fsState;
2367
+ const toggleFullscreen = React10__namespace.useCallback(() => {
2368
+ const next = !fullscreen;
2369
+ if (fullscreenProp === void 0) setFsState(next);
2370
+ onFullscreenChange?.(next);
2371
+ }, [fullscreen, fullscreenProp, onFullscreenChange]);
2372
+ React10__namespace.useEffect(() => {
2373
+ if (!fullscreen) return;
2374
+ const prev = document.body.style.overflow;
2375
+ document.body.style.overflow = "hidden";
2376
+ const onKey = (e) => e.key === "Escape" && toggleFullscreen();
2377
+ document.addEventListener("keydown", onKey);
2378
+ return () => {
2379
+ document.body.style.overflow = prev;
2380
+ document.removeEventListener("keydown", onKey);
2381
+ };
2382
+ }, [fullscreen, toggleFullscreen]);
2383
+ const canSource = !!resolved.html && resolved.html.sourceView;
2384
+ const [sourceMode, setSourceMode] = React10__namespace.useState(false);
2385
+ const [draft, setDraft] = React10__namespace.useState("");
2386
+ const allowStyleTags = !!resolved.html && resolved.html.allowStyleTags;
2387
+ const toggleSource = React10__namespace.useCallback(() => {
2388
+ if (!editor) return;
2389
+ if (!sourceMode) {
2390
+ setDraft(editor.getHTML());
2391
+ setSourceMode(true);
2392
+ } else {
2393
+ const clean = sanitizeHtml(draft, { allowStyleTags });
2394
+ editor.commands.setContent(clean, { emitUpdate: true });
2395
+ setSourceMode(false);
2396
+ }
2397
+ }, [editor, sourceMode, draft, allowStyleTags]);
2398
+ React10__namespace.useEffect(() => {
2399
+ if (!editor || !controlled || sourceMode) return;
2400
+ const incoming = isJson(value) ? value : String(value ?? "");
2401
+ const current = editor.getHTML();
2402
+ const incomingHtml = isJson(value) ? null : String(value ?? "");
2403
+ if (incomingHtml !== null && (incomingHtml === current || incomingHtml === lastEmitted.current)) return;
2404
+ const { from, to } = editor.state.selection;
2405
+ editor.commands.setContent(incoming, { emitUpdate: false });
2406
+ try {
2407
+ editor.commands.setTextSelection({ from, to });
2408
+ } catch {
2409
+ }
2410
+ }, [editor, controlled, value, sourceMode]);
2411
+ const uploadHandler = config.upload?.handler;
2412
+ const bar = toolbarPosition !== "none" && editor ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "osam-toolbar-wrap", "data-sticky": stickyToolbar && !fullscreen ? "" : void 0, children: /* @__PURE__ */ jsxRuntime.jsx(
2413
+ Toolbar,
2414
+ {
2415
+ editor,
2416
+ config: resolved,
2417
+ uploadHandler,
2418
+ view: {
2419
+ fullscreen,
2420
+ onToggleFullscreen: toggleFullscreen,
2421
+ sourceMode,
2422
+ onToggleSource: canSource ? toggleSource : void 0
2423
+ }
2424
+ }
2425
+ ) }) : null;
2426
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2427
+ "div",
2428
+ {
2429
+ className: `osam-editor${className ? ` ${className}` : ""}`,
2430
+ "data-editable": editable || void 0,
2431
+ "data-fullscreen": fullscreen || void 0,
2432
+ "data-source": sourceMode || void 0,
2433
+ style: { ...themeToStyle(resolved.theme), ...style },
2434
+ dir: resolved.dir,
2435
+ children: [
2436
+ toolbarPosition === "top" && bar,
2437
+ sourceMode ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-editor__body osam-source", style: { minHeight }, children: [
2438
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "osam-source__hint", children: [
2439
+ resolved.labels["source.title"],
2440
+ allowStyleTags ? "" : " \u2014 <script> and <style> are removed"
2441
+ ] }),
2442
+ /* @__PURE__ */ jsxRuntime.jsx(
2443
+ "textarea",
2444
+ {
2445
+ className: "osam-source__area",
2446
+ value: draft,
2447
+ spellCheck: false,
2448
+ onChange: (e) => setDraft(e.target.value)
2449
+ }
2450
+ ),
2451
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "osam-source__actions", children: /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "osam-btn-primary", onClick: toggleSource, children: resolved.labels["source.apply"] }) })
2452
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(react.EditorContent, { editor, className: "osam-editor__body", style: { minHeight } }),
2453
+ toolbarPosition === "bottom" && bar,
2454
+ resolved.branding && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "osam-branding", children: /* @__PURE__ */ jsxRuntime.jsx("a", { href: "https://osamtech.com", target: "_blank", rel: "noopener noreferrer", children: resolved.labels.poweredBy }) })
2455
+ ]
2456
+ }
2457
+ );
2458
+ }
2459
+ function OsamContent({ html, hls = true, className, ...rest }) {
2460
+ const ref = React10__namespace.useRef(null);
2461
+ React10__namespace.useEffect(() => {
2462
+ if (!hls || !ref.current) return;
2463
+ const videos = Array.from(ref.current.querySelectorAll("video")).filter(
2464
+ (v) => /\.m3u8(\?.*)?$/i.test(v.getAttribute("src") || v.currentSrc || "")
2465
+ );
2466
+ if (!videos.length) return;
2467
+ const instances = [];
2468
+ let cancelled = false;
2469
+ import('hls.js').then((mod) => {
2470
+ if (cancelled) return;
2471
+ const Hls = mod.default ?? mod;
2472
+ for (const video of videos) {
2473
+ const src = video.getAttribute("src") || "";
2474
+ if (video.canPlayType("application/vnd.apple.mpegurl")) continue;
2475
+ if (Hls?.isSupported?.()) {
2476
+ video.removeAttribute("src");
2477
+ const inst = new Hls();
2478
+ inst.loadSource(src);
2479
+ inst.attachMedia(video);
2480
+ instances.push(inst);
2481
+ }
2482
+ }
2483
+ }).catch(() => {
2484
+ });
2485
+ return () => {
2486
+ cancelled = true;
2487
+ instances.forEach((i) => i.destroy?.());
2488
+ };
2489
+ }, [html, hls]);
2490
+ return /* @__PURE__ */ jsxRuntime.jsx(
2491
+ "div",
2492
+ {
2493
+ ref,
2494
+ className: `osam-content${className ? ` ${className}` : ""}`,
2495
+ dangerouslySetInnerHTML: { __html: html },
2496
+ ...rest
2497
+ }
2498
+ );
2499
+ }
2500
+
2501
+ // src/storage/osam.ts
2502
+ function defaultResolveUrl(data) {
2503
+ if (!data) return void 0;
2504
+ return data.url ?? data.fileUrl ?? data.location ?? data.Location ?? data.src ?? data.playbackUrl ?? data.hlsUrl ?? data?.data?.url ?? data?.file?.url ?? data?.result?.url ?? void 0;
2505
+ }
2506
+ function createOsamStorageUploader(options) {
2507
+ const {
2508
+ getToken,
2509
+ baseUrl,
2510
+ chunkSize,
2511
+ concurrency,
2512
+ maxAttempts,
2513
+ resolveUrl = defaultResolveUrl,
2514
+ resolvePoster,
2515
+ client
2516
+ } = options;
2517
+ let clientPromise = client ? Promise.resolve(client) : null;
2518
+ async function getClient() {
2519
+ if (!clientPromise) {
2520
+ clientPromise = import('osamstorage').then((mod) => {
2521
+ const OsamStorage = mod.OsamStorage ?? mod.default?.OsamStorage ?? mod.default;
2522
+ if (!OsamStorage) {
2523
+ throw new Error("osameditor: could not find the OsamStorage export in 'osamstorage'.");
2524
+ }
2525
+ return new OsamStorage({ getToken, baseUrl, chunkSize, concurrency, maxAttempts });
2526
+ }).catch((err) => {
2527
+ throw new Error(
2528
+ "osameditor: the 'osamstorage' package is not installed. Run `npm i osamstorage`, or pass your own `client` / use `createVpsUploader()` instead.\n" + String(err)
2529
+ );
2530
+ });
2531
+ }
2532
+ return clientPromise;
2533
+ }
2534
+ return async (file, ctx) => {
2535
+ const kind = ctx.kind ?? detectKind(file);
2536
+ const storage = await getClient();
2537
+ const data = await new Promise((resolve, reject) => {
2538
+ storage.upload(file, {
2539
+ signal: ctx.signal,
2540
+ onProgress: (p) => ctx.onProgress?.(Math.round(p)),
2541
+ onSuccess: (d) => resolve(d),
2542
+ onError: (e) => reject(e instanceof Error ? e : new Error(String(e)))
2543
+ }).catch(reject);
2544
+ });
2545
+ const url = resolveUrl(data, file);
2546
+ if (!url) {
2547
+ throw new Error(
2548
+ "osameditor: OsamStorage upload finished but no URL could be resolved from the response. Pass a `resolveUrl` to createOsamStorageUploader(). Response was: " + safeJson(data)
2549
+ );
2550
+ }
2551
+ const result = {
2552
+ url,
2553
+ kind,
2554
+ name: file.name,
2555
+ meta: typeof data === "object" && data ? data : void 0
2556
+ };
2557
+ const poster = resolvePoster?.(data, file);
2558
+ if (poster) result.poster = poster;
2559
+ return result;
2560
+ };
2561
+ }
2562
+ function safeJson(v) {
2563
+ try {
2564
+ return JSON.stringify(v);
2565
+ } catch {
2566
+ return String(v);
2567
+ }
2568
+ }
2569
+
2570
+ // src/storage/vps.ts
2571
+ function defaultResolveUrl2(data) {
2572
+ if (typeof data === "string") return data;
2573
+ if (!data) return void 0;
2574
+ return data.url ?? data.fileUrl ?? data.location ?? data.src ?? data?.data?.url ?? void 0;
2575
+ }
2576
+ function createVpsUploader(options) {
2577
+ const {
2578
+ endpoint,
2579
+ fieldName = "file",
2580
+ fields,
2581
+ headers,
2582
+ credentials = "same-origin",
2583
+ resolveUrl = defaultResolveUrl2,
2584
+ resolvePoster
2585
+ } = options;
2586
+ return (file, ctx) => new Promise((resolve, reject) => {
2587
+ const kind = ctx.kind ?? detectKind(file);
2588
+ const url = typeof endpoint === "function" ? endpoint(file, kind) : endpoint;
2589
+ const form = new FormData();
2590
+ form.append(fieldName, file, file.name);
2591
+ const extra = typeof fields === "function" ? fields(file) : fields;
2592
+ if (extra) for (const [k, v] of Object.entries(extra)) form.append(k, v);
2593
+ Promise.resolve(typeof headers === "function" ? headers() : headers).then((resolvedHeaders) => {
2594
+ const xhr = new XMLHttpRequest();
2595
+ xhr.open("POST", url, true);
2596
+ xhr.withCredentials = credentials === "include";
2597
+ if (resolvedHeaders) {
2598
+ for (const [k, v] of Object.entries(resolvedHeaders)) xhr.setRequestHeader(k, v);
2599
+ }
2600
+ xhr.upload.onprogress = (e) => {
2601
+ if (e.lengthComputable) ctx.onProgress?.(Math.round(e.loaded / e.total * 100));
2602
+ };
2603
+ xhr.onload = () => {
2604
+ if (xhr.status < 200 || xhr.status >= 300) {
2605
+ reject(new Error(`osameditor: upload failed (${xhr.status} ${xhr.statusText}). ${xhr.responseText}`));
2606
+ return;
2607
+ }
2608
+ let data = xhr.responseText;
2609
+ try {
2610
+ data = JSON.parse(xhr.responseText);
2611
+ } catch {
2612
+ }
2613
+ const resolved = resolveUrl(data, file);
2614
+ if (!resolved) {
2615
+ reject(
2616
+ new Error(
2617
+ "osameditor: upload succeeded but no URL resolved from the response. Pass a `resolveUrl` to createVpsUploader()."
2618
+ )
2619
+ );
2620
+ return;
2621
+ }
2622
+ const result = { url: resolved, kind, name: file.name };
2623
+ if (data && typeof data === "object") result.meta = data;
2624
+ const poster = resolvePoster?.(data, file);
2625
+ if (poster) result.poster = poster;
2626
+ resolve(result);
2627
+ };
2628
+ xhr.onerror = () => reject(new Error("osameditor: network error during upload."));
2629
+ xhr.onabort = () => reject(new DOMException("Upload aborted", "AbortError"));
2630
+ if (ctx.signal) {
2631
+ if (ctx.signal.aborted) {
2632
+ xhr.abort();
2633
+ return;
2634
+ }
2635
+ ctx.signal.addEventListener("abort", () => xhr.abort(), { once: true });
2636
+ }
2637
+ xhr.send(form);
2638
+ }).catch(reject);
2639
+ });
2640
+ }
2641
+
2642
+ exports.CodeBlock = CodeBlock;
2643
+ exports.CustomHtml = CustomHtml;
2644
+ exports.Embed = Embed;
2645
+ exports.OsamContent = OsamContent;
2646
+ exports.OsamEditor = OsamEditor;
2647
+ exports.PreserveAttributes = PreserveAttributes;
2648
+ exports.ResizableImage = ResizableImage;
2649
+ exports.TOOLBAR_PRESETS = TOOLBAR_PRESETS;
2650
+ exports.Toolbar = Toolbar;
2651
+ exports.applyUserExtensions = applyUserExtensions;
2652
+ exports.buildExtensions = buildExtensions;
2653
+ exports.createOsamStorageUploader = createOsamStorageUploader;
2654
+ exports.createVpsUploader = createVpsUploader;
2655
+ exports.detectKind = detectKind;
2656
+ exports.loadMediaLibrary = loadMediaLibrary;
2657
+ exports.resolveConfig = resolveConfig;
2658
+ exports.resolveEmbed = resolveEmbed;
2659
+ exports.resolveToolbar = resolveToolbar;
2660
+ exports.sanitizeHtml = sanitizeHtml;
2661
+ exports.saveToMediaLibrary = saveToMediaLibrary;
2662
+ exports.themeToStyle = themeToStyle;
2663
+ exports.useOsamEditor = useOsamEditor;
2664
+ //# sourceMappingURL=index.cjs.map
2665
+ //# sourceMappingURL=index.cjs.map