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