@openeditor/react 0.0.30 → 0.0.32

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/README.md CHANGED
@@ -2,7 +2,36 @@
2
2
 
3
3
  Headless React bindings for OpenEditor's web editor runtime.
4
4
 
5
- Pass `attachmentRuntime` to `useOpenEditorController` for editable attachments and to `OpenEditorViewer` for identity-based resolution/opening. Progress, cancellation, retry input, and errors remain transient node-view state and are never emitted through document JSON.
5
+ Pass `imageRuntime` and `attachmentRuntime` to `useOpenEditorController` for
6
+ host-backed media and to `OpenEditorViewer` for identity-based URL resolution.
7
+ Progress, cancellation, retry input, and errors remain transient node-view state
8
+ and are never emitted through document JSON.
9
+
10
+ An inserted image starts as an empty image node—never a fake placeholder URL.
11
+ The built-in node view offers upload, drag/drop, URL embedding, replacement,
12
+ alternative-text editing, progress, retry, and removal. The upload button is
13
+ shown only when the host supplies `selectImage` and `uploadImage`:
14
+
15
+ ```tsx
16
+ const imageRuntime = {
17
+ selectImage: () => chooseImageFromYourPicker(),
18
+ validateImage: (input) => input.size && input.size > 15_000_000
19
+ ? { accepted: false, message: "Image is too large." }
20
+ : { accepted: true },
21
+ uploadImage: (input, { signal, onProgress } = {}) =>
22
+ uploadToYourStorage(input.source, { signal, onProgress }),
23
+ resolveImage: (imageId, { signal } = {}) =>
24
+ resolveImageFromYourStorage(imageId, { signal }),
25
+ replaceImage: (imageId, input, options) =>
26
+ replaceImageInYourStorage(imageId, input.source, options),
27
+ };
28
+
29
+ const controller = useOpenEditorController({ imageRuntime });
30
+ ```
31
+
32
+ `uploadImage` and `replaceImage` return an `OpenEditorImageSnapshot`. Store a
33
+ durable `imageId`, public `src`, or both; `resolveImage` may return a temporary
34
+ owner-preview URL such as a `blob:` URL without serializing it.
6
35
 
7
36
  ## Public surface
8
37
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, CSSProperties, RefObject, ComponentType } from 'react';
3
- import { OpenEditorBlockRef, OpenEditorDocument, BlockSpec, ProseMirrorNode, OpenEditorUrlPolicy, OpenEditorUrlContext, OpenEditorPageRuntime, OpenEditorAttachmentRuntime, OpenEditorPageSnapshot, OpenEditorAuthoringCapabilities } from '@openeditor/core';
4
- export { OpenEditorBlockRef, OpenEditorPageRuntime, OpenEditorPageSnapshot, OpenEditorUrlContext, OpenEditorUrlPolicy, openEditorPublicUrlPolicy, openEditorUnsafeUrlPolicy } from '@openeditor/core';
3
+ import { OpenEditorBlockRef, OpenEditorDocument, BlockSpec, ProseMirrorNode, OpenEditorUrlPolicy, OpenEditorUrlContext, OpenEditorPageRuntime, OpenEditorAttachmentRuntime, OpenEditorImageRuntime, OpenEditorPageSnapshot, OpenEditorAuthoringCapabilities } from '@openeditor/core';
4
+ export { OpenEditorBlockRef, OpenEditorImageRuntime, OpenEditorImageSnapshot, OpenEditorImageUploadInput, OpenEditorPageRuntime, OpenEditorPageSnapshot, OpenEditorUrlContext, OpenEditorUrlPolicy, openEditorPublicUrlPolicy, openEditorUnsafeUrlPolicy } from '@openeditor/core';
5
5
  import { ExportFormat, OpenEditorExporters } from '@openeditor/exporters';
6
6
  export { seedDocument } from '@openeditor/extensions';
7
7
  import { Editor, AnyExtension, NodeConfig } from '@tiptap/core';
@@ -19,7 +19,9 @@ type OpenEditorBlockInteractionStore = {
19
19
 
20
20
  declare const openEditorThemeTokenNames: readonly ["surface", "surfaceRaised", "surfaceMuted", "blockSurface", "text", "textSoft", "heading", "muted", "placeholder", "border", "borderStrong", "structuralLine", "accent", "accentText", "accentStrong", "buttonBackground", "buttonText", "codeBackground", "codeText", "link", "linkHover", "shadow", "fontSans", "fontMono", "radiusSmall", "radiusMedium", "radiusLarge", "spaceBlock", "spaceInline", "bodyFontSize", "bodyLineHeight", "headingFont", "headingLineHeight", "headingWeight", "heading1Size", "heading2Size", "heading3Size", "heading4Size", "heading5Size", "heading6Size"];
21
21
  type OpenEditorThemeToken = (typeof openEditorThemeTokenNames)[number];
22
- type OpenEditorTheme = Partial<Record<OpenEditorThemeToken, string>>;
22
+ type OpenEditorTheme = {
23
+ surfaceRaised: string;
24
+ } & Partial<Record<Exclude<OpenEditorThemeToken, "surfaceRaised">, string>>;
23
25
  type OpenEditorThemeStyle = CSSProperties & Record<`--oe-${string}`, string | undefined>;
24
26
  declare const createOpenEditorThemeStyle: (theme: OpenEditorTheme) => OpenEditorThemeStyle;
25
27
  declare const useOpenEditorThemeStyle: () => OpenEditorThemeStyle | undefined;
@@ -58,6 +60,7 @@ type OpenEditorReactProps = OpenEditorAuthoringCapabilities & {
58
60
  onFilePasteDrop?: (file: File) => boolean;
59
61
  pageRuntime?: OpenEditorPageRuntime;
60
62
  attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
63
+ imageRuntime?: OpenEditorImageRuntime<any>;
61
64
  slashMenuItems?: readonly OpenEditorSlashMenuItem[];
62
65
  /** Consumer-owned blocks. Extension names and node types must be unique. */
63
66
  extensions?: readonly OpenEditorReactExtension[];
@@ -122,6 +125,7 @@ type OpenEditorController = {
122
125
  extensions: readonly OpenEditorReactExtension[];
123
126
  pageRuntime?: OpenEditorPageRuntime;
124
127
  attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
128
+ imageRuntime?: OpenEditorImageRuntime<any>;
125
129
  enabledBlocks?: readonly string[];
126
130
  slashState: SlashState | null;
127
131
  dismissSlashMenu: () => void;
@@ -232,6 +236,7 @@ type OpenEditorViewerProps = {
232
236
  extensions?: readonly OpenEditorReactExtension[];
233
237
  pageRuntime?: OpenEditorPageRuntime;
234
238
  attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
239
+ imageRuntime?: OpenEditorImageRuntime<any>;
235
240
  /** Safe by default. Supply an explicit host policy for trusted preview URLs such as `blob:`. */
236
241
  urlPolicy?: OpenEditorUrlPolicy;
237
242
  };
@@ -251,7 +256,7 @@ type OpenEditorPageHeaderProps = {
251
256
  };
252
257
  /** Canonical Notion-style page metadata editor shared by host page surfaces. */
253
258
  declare const OpenEditorPageHeader: ({ page, runtime, className, onPageChange, }: OpenEditorPageHeaderProps) => react.JSX.Element;
254
- declare const useOpenEditorController: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, enabledBlocks, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
259
+ declare const useOpenEditorController: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, imageRuntime, enabledBlocks, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
255
260
  type OpenEditorBubbleMenuProps = {
256
261
  controller: OpenEditorController;
257
262
  children: ReactNode;
@@ -293,7 +298,7 @@ declare const sortBlockPickerItems: (items: readonly OpenEditorBlockPickerItem[]
293
298
  declare const getDefaultBlockPickerItems: (controller: OpenEditorController) => OpenEditorBlockPickerItem[];
294
299
  declare const getDefaultSlashMenuItems: (controller: OpenEditorController) => OpenEditorSlashMenuItem[];
295
300
  declare const OpenEditorContent: ({ controller, className, }: OpenEditorContentProps) => react.JSX.Element;
296
- declare const OpenEditorViewer: ({ document, className, renderers, extensions, pageRuntime, attachmentRuntime, urlPolicy, }: OpenEditorViewerProps) => react.JSX.Element;
297
- declare const useOpenEditor: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, enabledBlocks, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
301
+ declare const OpenEditorViewer: ({ document, className, renderers, extensions, pageRuntime, attachmentRuntime, imageRuntime, urlPolicy, }: OpenEditorViewerProps) => react.JSX.Element;
302
+ declare const useOpenEditor: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, imageRuntime, enabledBlocks, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
298
303
 
299
304
  export { type InsertRange, type OpenEditorBlockAction, type OpenEditorBlockActionContext, OpenEditorBlockDragHandle, type OpenEditorBlockDragHandleProps, type OpenEditorBlockPickerItem, OpenEditorBubbleMenu, type OpenEditorBubbleMenuProps, OpenEditorContent, type OpenEditorContentProps, type OpenEditorController, type OpenEditorExtensionMenu, type OpenEditorOverlayRect, OpenEditorPageHeader, type OpenEditorPageHeaderProps, type OpenEditorReactConfig, type OpenEditorReactExtension, type OpenEditorReactNodeExtension, type OpenEditorReactProps, type OpenEditorSlashMenuIcon, type OpenEditorSlashMenuIconProps, type OpenEditorSlashMenuItem, OpenEditorTableBubbleMenu, type OpenEditorTableBubbleMenuProps, type OpenEditorTableCommand, type OpenEditorTableController, type OpenEditorTableState, type OpenEditorTheme, OpenEditorThemeProvider, type OpenEditorThemeStyle, type OpenEditorThemeToken, OpenEditorViewer, type OpenEditorViewerProps, type OpenEditorViewerRenderer, type SlashState, canRunOpenEditorTableCommand, createOpenEditorThemeStyle, defineOpenEditorReactExtension, defineOpenEditorReactNode, formatAttachmentSize, getDefaultBlockPickerItems, getDefaultSlashMenuItems, getOpenEditorTableState, openEditorTableCommands, openEditorThemeTokenNames, runOpenEditorTableCommand, sortBlockPickerItems, sortSlashMenuItems, useOpenEditor, useOpenEditorBlockInteraction, useOpenEditorController, useOpenEditorThemeStyle };
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export { openEditorPublicUrlPolicy, openEditorUnsafeUrlPolicy } from '@openedito
3
3
  import { Checkbox } from '@base-ui/react/checkbox';
4
4
  import { shift as shift$1, flip, offset, arrow as arrow$1, size, autoPlacement, hide, inline, computePosition, autoUpdate } from '@floating-ui/dom';
5
5
  import { OpenEditorEmojiPicker } from '@openeditor/emoji';
6
+ import { RefreshCw, Link2, Pencil, Trash2, Image as Image$1, Upload, X } from 'lucide-react';
6
7
  import React, { createContext, forwardRef, useRef, useState, useEffect, useContext, useMemo, useLayoutEffect, createRef, memo, createElement, version, useSyncExternalStore as useSyncExternalStore$1, useCallback, use, useDebugValue } from 'react';
7
8
  import { jsxs, Fragment as Fragment$1, jsx } from 'react/jsx-runtime';
8
9
  import { renderMermaidSVG } from 'beautiful-mermaid';
@@ -33094,6 +33095,7 @@ var createOpenEditorTableController = (getEditor) => ({
33094
33095
  });
33095
33096
  var OpenEditorPageRuntimeContext = createContext(void 0);
33096
33097
  var OpenEditorAttachmentRuntimeContext = createContext(void 0);
33098
+ var OpenEditorImageRuntimeContext = createContext(void 0);
33097
33099
  var defineOpenEditorReactExtension = (extension) => extension;
33098
33100
  var defineOpenEditorReactNode = ({
33099
33101
  block,
@@ -33421,6 +33423,262 @@ var Callout = Node3.create({
33421
33423
  return ReactNodeViewRenderer(OpenEditorCalloutNodeView);
33422
33424
  }
33423
33425
  });
33426
+ var imageSnapshotFromNode = (node) => ({
33427
+ imageId: typeof node.attrs?.imageId === "string" ? node.attrs.imageId : null,
33428
+ src: typeof node.attrs?.src === "string" && node.attrs.src.trim() ? node.attrs.src : null,
33429
+ alt: typeof node.attrs?.alt === "string" ? node.attrs.alt : "",
33430
+ width: typeof node.attrs?.width === "number" ? node.attrs.width : null,
33431
+ height: typeof node.attrs?.height === "number" ? node.attrs.height : null
33432
+ });
33433
+ var resolvedImageSrc = (snapshot, runtimeResolved = false) => {
33434
+ if (!snapshot?.src) return null;
33435
+ const safe = openEditorPublicUrlPolicy(snapshot.src, "image");
33436
+ if (safe) return safe;
33437
+ const normalized = snapshot.src.trim();
33438
+ return runtimeResolved && normalized.startsWith("blob:") ? normalized : null;
33439
+ };
33440
+ var webImageInput = (file) => ({
33441
+ name: file.name,
33442
+ mimeType: file.type || null,
33443
+ size: file.size,
33444
+ source: file
33445
+ });
33446
+ var OpenEditorImageNodeView = ({ editor, node, updateAttributes: updateAttributes2, deleteNode: deleteNode2, selected }) => {
33447
+ const runtime = use(OpenEditorImageRuntimeContext);
33448
+ const durable = imageSnapshotFromNode(node);
33449
+ const hasImage = Boolean(durable.imageId || durable.src);
33450
+ const [phase, setPhase] = useState(hasImage ? "ready" : "idle");
33451
+ const [progress, setProgress] = useState(0);
33452
+ const [error, setError] = useState(null);
33453
+ const [input, setInput] = useState(null);
33454
+ const [resolved, setResolved] = useState(null);
33455
+ const [showUrl, setShowUrl] = useState(false);
33456
+ const [url, setUrl] = useState("");
33457
+ const [editingAlt, setEditingAlt] = useState(false);
33458
+ const [alt, setAlt] = useState(durable.alt);
33459
+ const operationRef = useRef(0);
33460
+ const abortRef = useRef(null);
33461
+ const stopCurrent = () => {
33462
+ operationRef.current += 1;
33463
+ abortRef.current?.abort();
33464
+ abortRef.current = null;
33465
+ };
33466
+ const isCurrent = (operation) => operationRef.current === operation;
33467
+ useEffect(() => () => stopCurrent(), []);
33468
+ useEffect(() => {
33469
+ setAlt(durable.alt);
33470
+ }, [durable.alt]);
33471
+ useEffect(() => {
33472
+ if (!durable.imageId || !runtime?.resolveImage) {
33473
+ setResolved(null);
33474
+ return;
33475
+ }
33476
+ const controller = new AbortController();
33477
+ void runtime.resolveImage(durable.imageId, { signal: controller.signal }).then((snapshot) => {
33478
+ if (!controller.signal.aborted) setResolved(snapshot);
33479
+ }).catch(() => {
33480
+ if (!controller.signal.aborted) setResolved(null);
33481
+ });
33482
+ return () => controller.abort();
33483
+ }, [durable.imageId, runtime]);
33484
+ const upload = async (selectedInput, replacing) => {
33485
+ if (!runtime?.uploadImage) return;
33486
+ stopCurrent();
33487
+ const operation = operationRef.current;
33488
+ const controller = new AbortController();
33489
+ abortRef.current = controller;
33490
+ setInput(selectedInput);
33491
+ setProgress(0);
33492
+ setError(null);
33493
+ setShowUrl(false);
33494
+ setPhase("uploading");
33495
+ try {
33496
+ const validation = await runtime.validateImage?.(selectedInput);
33497
+ if (!isCurrent(operation)) return;
33498
+ if (validation && !validation.accepted) throw new Error(validation.message);
33499
+ const callbacks = {
33500
+ signal: controller.signal,
33501
+ onProgress: (value) => {
33502
+ if (isCurrent(operation)) setProgress(Math.max(0, Math.min(1, value)));
33503
+ }
33504
+ };
33505
+ const uploaded = replacing && durable.imageId && runtime.replaceImage ? await runtime.replaceImage(durable.imageId, selectedInput, callbacks) : await runtime.uploadImage(selectedInput, callbacks);
33506
+ if (!isCurrent(operation)) return;
33507
+ updateAttributes2({ ...uploaded, alt: uploaded.alt || durable.alt });
33508
+ if (uploaded.imageId && runtime.resolveImage) {
33509
+ const nextResolved = await runtime.resolveImage(uploaded.imageId, { signal: controller.signal });
33510
+ if (!isCurrent(operation)) return;
33511
+ setResolved(nextResolved);
33512
+ } else {
33513
+ setResolved(uploaded);
33514
+ }
33515
+ setInput(null);
33516
+ setProgress(1);
33517
+ setPhase("ready");
33518
+ } catch (cause) {
33519
+ if (!isCurrent(operation) || controller.signal.aborted) return;
33520
+ setError(cause instanceof Error ? cause.message : "Image upload failed.");
33521
+ setPhase("error");
33522
+ }
33523
+ };
33524
+ const selectAndUpload = async (replacing = false) => {
33525
+ if (!runtime?.selectImage || !runtime.uploadImage) return;
33526
+ stopCurrent();
33527
+ const operation = operationRef.current;
33528
+ const controller = new AbortController();
33529
+ abortRef.current = controller;
33530
+ setError(null);
33531
+ setPhase("selecting");
33532
+ try {
33533
+ const selectedInput = await runtime.selectImage({ signal: controller.signal });
33534
+ if (!isCurrent(operation)) return;
33535
+ if (!selectedInput) {
33536
+ setPhase(hasImage ? "ready" : "idle");
33537
+ return;
33538
+ }
33539
+ await upload(selectedInput, replacing);
33540
+ } catch (cause) {
33541
+ if (!isCurrent(operation) || controller.signal.aborted) return;
33542
+ setError(cause instanceof Error ? cause.message : "Could not select an image.");
33543
+ setPhase("error");
33544
+ }
33545
+ };
33546
+ const submitUrl = (event) => {
33547
+ event.preventDefault();
33548
+ const safe = openEditorPublicUrlPolicy(url.trim(), "image");
33549
+ if (!safe) {
33550
+ setError("Enter a valid http or https image URL.");
33551
+ return;
33552
+ }
33553
+ stopCurrent();
33554
+ updateAttributes2({ imageId: null, src: safe, width: null, height: null });
33555
+ setResolved(null);
33556
+ setError(null);
33557
+ setShowUrl(false);
33558
+ setUrl("");
33559
+ setPhase("ready");
33560
+ };
33561
+ const dropImage = (event) => {
33562
+ if (!editor.isEditable || !runtime?.uploadImage) return;
33563
+ const file = event.dataTransfer.files?.[0];
33564
+ if (!file) return;
33565
+ event.preventDefault();
33566
+ event.stopPropagation();
33567
+ void upload(webImageInput(file), hasImage);
33568
+ };
33569
+ const cancel = () => {
33570
+ stopCurrent();
33571
+ setInput(null);
33572
+ setError(null);
33573
+ setPhase(hasImage ? "ready" : "idle");
33574
+ };
33575
+ const preview = resolvedImageSrc(resolved, true) ?? resolvedImageSrc(durable);
33576
+ const canUpload = Boolean(runtime?.selectImage && runtime.uploadImage);
33577
+ const busy = phase === "selecting" || phase === "uploading";
33578
+ return /* @__PURE__ */ jsxs(
33579
+ NodeViewWrapper,
33580
+ {
33581
+ as: "figure",
33582
+ className: "oe-image",
33583
+ contentEditable: false,
33584
+ "data-selected": selected ? "true" : void 0,
33585
+ "data-state": phase,
33586
+ onDragOver: (event) => {
33587
+ if (canUpload) event.preventDefault();
33588
+ },
33589
+ onDrop: dropImage,
33590
+ children: [
33591
+ preview ? /* @__PURE__ */ jsxs("div", { className: "oe-image-preview", children: [
33592
+ /* @__PURE__ */ jsx("img", { alt: durable.alt, draggable: false, height: durable.height ?? void 0, src: preview, width: durable.width ?? void 0 }),
33593
+ editor.isEditable ? /* @__PURE__ */ jsxs("div", { "aria-label": "Image actions", className: "oe-image-actions", role: "toolbar", children: [
33594
+ /* @__PURE__ */ jsxs("button", { onClick: () => {
33595
+ if (canUpload) void selectAndUpload(true);
33596
+ else setShowUrl(true);
33597
+ }, type: "button", children: [
33598
+ /* @__PURE__ */ jsx(RefreshCw, { "aria-hidden": "true" }),
33599
+ " Replace"
33600
+ ] }),
33601
+ /* @__PURE__ */ jsx("button", { "aria-label": "Use image URL", onClick: () => {
33602
+ setError(null);
33603
+ setShowUrl(true);
33604
+ }, title: "Replace from a link", type: "button", children: /* @__PURE__ */ jsx(Link2, { "aria-hidden": "true" }) }),
33605
+ /* @__PURE__ */ jsxs("button", { onClick: () => setEditingAlt(true), type: "button", children: [
33606
+ /* @__PURE__ */ jsx(Pencil, { "aria-hidden": "true" }),
33607
+ " Alt text"
33608
+ ] }),
33609
+ /* @__PURE__ */ jsx("button", { "aria-label": "Remove image", className: "oe-image-remove", onClick: deleteNode2, title: "Remove image", type: "button", children: /* @__PURE__ */ jsx(Trash2, { "aria-hidden": "true" }) })
33610
+ ] }) : null
33611
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "oe-image-empty", children: [
33612
+ /* @__PURE__ */ jsx("span", { className: "oe-image-empty-icon", children: /* @__PURE__ */ jsx(Image$1, { "aria-hidden": "true" }) }),
33613
+ /* @__PURE__ */ jsxs("div", { className: "oe-image-empty-copy", children: [
33614
+ /* @__PURE__ */ jsx("strong", { children: "Add an image" }),
33615
+ /* @__PURE__ */ jsx("span", { children: canUpload ? "Upload, drop, or embed from a link" : "Embed from a link" })
33616
+ ] }),
33617
+ canUpload ? /* @__PURE__ */ jsx("button", { "aria-busy": phase === "selecting", "aria-label": "Upload image", className: "oe-image-empty-action", disabled: busy, onClick: () => void selectAndUpload(false), title: "Upload image", type: "button", children: /* @__PURE__ */ jsx(Upload, { "aria-hidden": "true" }) }) : null,
33618
+ /* @__PURE__ */ jsx("button", { "aria-label": "Use image URL", className: "oe-image-empty-action", disabled: busy, onClick: () => {
33619
+ setError(null);
33620
+ setShowUrl(true);
33621
+ }, title: "Use image URL", type: "button", children: /* @__PURE__ */ jsx(Link2, { "aria-hidden": "true" }) }),
33622
+ editor.isEditable ? /* @__PURE__ */ jsx("button", { "aria-label": "Remove empty image block", className: "oe-image-empty-action oe-image-remove", onClick: deleteNode2, title: "Remove image block", type: "button", children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" }) }) : null
33623
+ ] }),
33624
+ phase === "uploading" ? /* @__PURE__ */ jsxs("div", { className: "oe-image-upload-status", children: [
33625
+ /* @__PURE__ */ jsx("div", { className: "oe-image-progress", role: "progressbar", "aria-label": `Uploading ${input?.name ?? "image"}`, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(progress * 100), children: /* @__PURE__ */ jsx("span", { style: { width: `${progress * 100}%` } }) }),
33626
+ /* @__PURE__ */ jsxs("span", { children: [
33627
+ Math.round(progress * 100),
33628
+ "%"
33629
+ ] }),
33630
+ /* @__PURE__ */ jsx("button", { onClick: cancel, type: "button", children: "Cancel" })
33631
+ ] }) : null,
33632
+ showUrl ? /* @__PURE__ */ jsxs("form", { className: "oe-image-panel", onSubmit: submitUrl, children: [
33633
+ /* @__PURE__ */ jsxs("label", { children: [
33634
+ /* @__PURE__ */ jsx("span", { children: "Image URL" }),
33635
+ /* @__PURE__ */ jsx("input", { autoFocus: true, "aria-label": "Image URL", inputMode: "url", onChange: (event) => setUrl(event.target.value), placeholder: "https://example.com/image.jpg", value: url })
33636
+ ] }),
33637
+ /* @__PURE__ */ jsx("button", { className: "oe-image-primary", type: "submit", children: "Use image" }),
33638
+ /* @__PURE__ */ jsx("button", { onClick: () => {
33639
+ setShowUrl(false);
33640
+ setError(null);
33641
+ }, type: "button", children: "Cancel" })
33642
+ ] }) : null,
33643
+ editingAlt ? /* @__PURE__ */ jsxs("form", { className: "oe-image-panel", onSubmit: (event) => {
33644
+ event.preventDefault();
33645
+ updateAttributes2({ alt });
33646
+ setEditingAlt(false);
33647
+ }, children: [
33648
+ /* @__PURE__ */ jsxs("label", { children: [
33649
+ /* @__PURE__ */ jsx("span", { children: "Alternative text" }),
33650
+ /* @__PURE__ */ jsx("input", { autoFocus: true, "aria-label": "Alternative text", onChange: (event) => setAlt(event.target.value), placeholder: "Describe this image", value: alt })
33651
+ ] }),
33652
+ /* @__PURE__ */ jsx("button", { className: "oe-image-primary", type: "submit", children: "Save" }),
33653
+ /* @__PURE__ */ jsx("button", { onClick: () => {
33654
+ setAlt(durable.alt);
33655
+ setEditingAlt(false);
33656
+ }, type: "button", children: "Cancel" })
33657
+ ] }) : null,
33658
+ error ? /* @__PURE__ */ jsxs("div", { className: "oe-image-error", role: "alert", children: [
33659
+ /* @__PURE__ */ jsx("span", { children: error }),
33660
+ input ? /* @__PURE__ */ jsx("button", { onClick: () => void upload(input, hasImage), type: "button", children: "Retry" }) : null
33661
+ ] }) : null
33662
+ ]
33663
+ }
33664
+ );
33665
+ };
33666
+ var OpenEditorImage = index_default2.extend({
33667
+ addAttributes() {
33668
+ return {
33669
+ ...this.parent?.(),
33670
+ imageId: {
33671
+ default: null,
33672
+ parseHTML: (element) => element.getAttribute("data-openeditor-image-id"),
33673
+ renderHTML: (attributes) => attributes.imageId ? { "data-openeditor-image-id": attributes.imageId } : {}
33674
+ },
33675
+ src: { default: null }
33676
+ };
33677
+ },
33678
+ addNodeView() {
33679
+ return ReactNodeViewRenderer(OpenEditorImageNodeView);
33680
+ }
33681
+ });
33424
33682
  var attachmentSnapshotFromNode = (node) => ({
33425
33683
  attachmentId: typeof node.attrs?.attachmentId === "string" ? node.attrs.attachmentId : null,
33426
33684
  name: typeof node.attrs?.name === "string" ? node.attrs.name : "",
@@ -33440,12 +33698,19 @@ var formatAttachmentSize = (size3) => {
33440
33698
  }
33441
33699
  return `${value >= 10 ? value.toFixed(0) : value.toFixed(1)} ${units[unit]}`;
33442
33700
  };
33443
- var attachmentTypeLabel = (snapshot) => {
33444
- if (snapshot.mimeType) return snapshot.mimeType.split("/").at(-1)?.toUpperCase() ?? snapshot.mimeType;
33445
- const extension = snapshot.name.includes(".") ? snapshot.name.split(".").at(-1) : null;
33446
- return extension?.toUpperCase() ?? "FILE";
33701
+ var IMAGE_ATTACHMENT_EXTENSION = /\.(?:avif|gif|jpe?g|png|svg|webp)$/i;
33702
+ var AttachmentPreview = ({ snapshot, url }) => {
33703
+ const isImage = snapshot.mimeType?.toLowerCase().startsWith("image/") || IMAGE_ATTACHMENT_EXTENSION.test(snapshot.name);
33704
+ if (!url || !isImage) return null;
33705
+ return /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "oe-attachment-thumbnail", children: /* @__PURE__ */ jsx("img", { alt: "", src: url }) });
33706
+ };
33707
+ var attachmentPreviewUrl = (url, allowBlob = false) => {
33708
+ if (!url) return null;
33709
+ const safeUrl = openEditorPublicUrlPolicy(url, "attachment");
33710
+ if (safeUrl) return safeUrl;
33711
+ const normalized = url.trim();
33712
+ return allowBlob && normalized.startsWith("blob:") ? normalized : null;
33447
33713
  };
33448
- var AttachmentIcon = ({ snapshot }) => /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "oe-attachment-icon", children: /* @__PURE__ */ jsx("span", { children: attachmentTypeLabel(snapshot).slice(0, 4) }) });
33449
33714
  var OpenEditorAttachmentNodeView = ({ editor, node, updateAttributes: updateAttributes2, deleteNode: deleteNode2 }) => {
33450
33715
  const runtime = use(OpenEditorAttachmentRuntimeContext);
33451
33716
  const durable = attachmentSnapshotFromNode(node);
@@ -33453,6 +33718,9 @@ var OpenEditorAttachmentNodeView = ({ editor, node, updateAttributes: updateAttr
33453
33718
  const [progress, setProgress] = useState(0);
33454
33719
  const [error, setError] = useState(null);
33455
33720
  const [input, setInput] = useState(null);
33721
+ const [resolved, setResolved] = useState(null);
33722
+ const [renaming, setRenaming] = useState(false);
33723
+ const [draftName, setDraftName] = useState("");
33456
33724
  const operationRef = useRef(0);
33457
33725
  const abortRef = useRef(null);
33458
33726
  const isCurrent = (operation) => operationRef.current === operation;
@@ -33462,6 +33730,19 @@ var OpenEditorAttachmentNodeView = ({ editor, node, updateAttributes: updateAttr
33462
33730
  abortRef.current = null;
33463
33731
  };
33464
33732
  useEffect(() => () => stopCurrent(), []);
33733
+ useEffect(() => {
33734
+ if (!durable.attachmentId || !runtime?.resolveAttachment) {
33735
+ setResolved(null);
33736
+ return;
33737
+ }
33738
+ const controller = new AbortController();
33739
+ void runtime.resolveAttachment(durable.attachmentId, { signal: controller.signal }).then((snapshot) => {
33740
+ if (!controller.signal.aborted) setResolved(snapshot);
33741
+ }).catch(() => {
33742
+ if (!controller.signal.aborted) setResolved(null);
33743
+ });
33744
+ return () => controller.abort();
33745
+ }, [durable.attachmentId, runtime]);
33465
33746
  const upload = async (selected, replacing) => {
33466
33747
  if (!runtime?.uploadAttachment) return;
33467
33748
  stopCurrent();
@@ -33471,6 +33752,8 @@ var OpenEditorAttachmentNodeView = ({ editor, node, updateAttributes: updateAttr
33471
33752
  setInput(selected);
33472
33753
  setProgress(0);
33473
33754
  setError(null);
33755
+ setResolved(null);
33756
+ setRenaming(false);
33474
33757
  setPhase("uploading");
33475
33758
  try {
33476
33759
  const validation = await runtime.validateAttachment?.(selected);
@@ -33485,6 +33768,11 @@ var OpenEditorAttachmentNodeView = ({ editor, node, updateAttributes: updateAttr
33485
33768
  setInput(null);
33486
33769
  setProgress(1);
33487
33770
  setPhase("ready");
33771
+ if (uploaded.attachmentId && runtime.resolveAttachment) {
33772
+ void runtime.resolveAttachment(uploaded.attachmentId, { signal: controller.signal }).then((snapshot) => {
33773
+ if (isCurrent(operation) && !controller.signal.aborted) setResolved(snapshot);
33774
+ }).catch(() => void 0);
33775
+ }
33488
33776
  } catch (cause) {
33489
33777
  if (!isCurrent(operation) || controller.signal.aborted) return;
33490
33778
  setError(cause instanceof Error ? cause.message : "Upload failed.");
@@ -33528,26 +33816,46 @@ var OpenEditorAttachmentNodeView = ({ editor, node, updateAttributes: updateAttr
33528
33816
  };
33529
33817
  const name = durable.name || input?.name || "Choose a file";
33530
33818
  const display = durable.name ? durable : { ...durable, name, mimeType: input?.mimeType ?? null, size: input?.size ?? null };
33819
+ const previewSnapshot = resolved ? { ...resolved, name } : display;
33820
+ const previewUrl = attachmentPreviewUrl(previewSnapshot.url, Boolean(resolved));
33821
+ const commitRename = () => {
33822
+ const nextName = draftName.trim() || name;
33823
+ setRenaming(false);
33824
+ if (nextName === name) return;
33825
+ updateAttributes2({ name: nextName });
33826
+ if (durable.attachmentId) void runtime?.renameAttachment?.(durable.attachmentId, nextName);
33827
+ };
33531
33828
  const open = () => {
33532
- if (runtime?.openAttachment) void runtime.openAttachment(durable);
33533
- else if (durable.url) window.open(durable.url, "_blank", "noopener,noreferrer");
33829
+ const attachment = resolved ? { ...resolved, name } : durable;
33830
+ if (runtime?.openAttachment) void runtime.openAttachment(attachment);
33831
+ else if (attachment.url) window.open(attachment.url, "_blank", "noopener,noreferrer");
33534
33832
  };
33535
33833
  return /* @__PURE__ */ jsxs(NodeViewWrapper, { className: "oe-attachment", "data-state": phase, children: [
33536
- /* @__PURE__ */ jsx(AttachmentIcon, { snapshot: display }),
33834
+ (durable.attachmentId || durable.url) && phase !== "uploading" ? /* @__PURE__ */ jsx("button", { "aria-label": `Open ${name}`, className: "oe-attachment-open-control", contentEditable: false, onClick: open, type: "button" }) : null,
33835
+ /* @__PURE__ */ jsx(AttachmentPreview, { snapshot: previewSnapshot, url: previewUrl }),
33537
33836
  /* @__PURE__ */ jsxs("div", { className: "oe-attachment-body", children: [
33538
- /* @__PURE__ */ jsx("input", { "aria-label": "File name", className: "oe-attachment-name", disabled: !editor.isEditable || phase === "uploading", value: name, onChange: (event) => updateAttributes2({ name: event.target.value }), onBlur: () => {
33539
- if (durable.attachmentId && durable.name) void runtime?.renameAttachment?.(durable.attachmentId, durable.name);
33540
- } }),
33541
- /* @__PURE__ */ jsx("div", { className: "oe-attachment-meta", children: [formatAttachmentSize(display.size), attachmentTypeLabel(display)].filter(Boolean).join(" \xB7 ") }),
33837
+ renaming ? /* @__PURE__ */ jsx("input", { autoFocus: true, "aria-label": "Rename file", className: "oe-attachment-name oe-attachment-name-input", value: draftName, onBlur: commitRename, onChange: (event) => setDraftName(event.target.value), onKeyDown: (event) => {
33838
+ if (event.key === "Enter") {
33839
+ event.preventDefault();
33840
+ event.currentTarget.blur();
33841
+ } else if (event.key === "Escape") {
33842
+ event.preventDefault();
33843
+ setDraftName(name);
33844
+ setRenaming(false);
33845
+ }
33846
+ } }) : /* @__PURE__ */ jsx("span", { className: "oe-attachment-name", children: name }),
33542
33847
  phase === "uploading" ? /* @__PURE__ */ jsx("div", { className: "oe-attachment-progress", role: "progressbar", "aria-label": `Uploading ${name}`, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(progress * 100), children: /* @__PURE__ */ jsx("span", { style: { width: `${progress * 100}%` } }) }) : null,
33543
33848
  error ? /* @__PURE__ */ jsx("div", { className: "oe-attachment-error", role: "alert", children: error }) : null
33544
33849
  ] }),
33545
33850
  /* @__PURE__ */ jsxs("div", { className: "oe-attachment-actions", contentEditable: false, children: [
33546
33851
  phase === "uploading" || phase === "selecting" ? /* @__PURE__ */ jsx("button", { onClick: cancel, type: "button", children: "Cancel" }) : null,
33547
33852
  phase === "error" && input ? /* @__PURE__ */ jsx("button", { onClick: () => void upload(input, Boolean(durable.attachmentId)), type: "button", children: "Retry" }) : null,
33548
- (durable.attachmentId || durable.url) && phase !== "uploading" ? /* @__PURE__ */ jsx("button", { onClick: open, type: "button", children: "Open" }) : null,
33549
- editor.isEditable && runtime?.selectAttachment && runtime.uploadAttachment && phase !== "uploading" ? /* @__PURE__ */ jsx("button", { onClick: () => void selectAndUpload(Boolean(durable.attachmentId)), type: "button", children: "Replace" }) : null,
33550
- editor.isEditable ? /* @__PURE__ */ jsx("button", { "aria-label": `Remove ${name}`, onClick: remove, type: "button", children: "Remove" }) : null
33853
+ editor.isEditable && durable.name && phase !== "uploading" && phase !== "selecting" ? /* @__PURE__ */ jsx("button", { "aria-label": `Rename ${name}`, onClick: () => {
33854
+ setDraftName(name);
33855
+ setRenaming(true);
33856
+ }, title: "Rename file", type: "button", children: /* @__PURE__ */ jsx(Pencil, { "aria-hidden": "true" }) }) : null,
33857
+ editor.isEditable && runtime?.selectAttachment && runtime.uploadAttachment && phase !== "uploading" ? /* @__PURE__ */ jsx("button", { "aria-label": `Replace ${name}`, onClick: () => void selectAndUpload(Boolean(durable.attachmentId)), title: "Replace file", type: "button", children: /* @__PURE__ */ jsx(RefreshCw, { "aria-hidden": "true" }) }) : null,
33858
+ editor.isEditable ? /* @__PURE__ */ jsx("button", { "aria-label": `Remove ${name}`, className: "oe-attachment-remove", onClick: remove, title: "Remove file", type: "button", children: /* @__PURE__ */ jsx(Trash2, { "aria-hidden": "true" }) }) : null
33551
33859
  ] })
33552
33860
  ] });
33553
33861
  };
@@ -33583,12 +33891,10 @@ var OpenEditorAttachmentViewer = ({
33583
33891
  return () => controller.abort();
33584
33892
  }, [cached.attachmentId, cached.url, runtime]);
33585
33893
  const snapshot = state.snapshot ?? cached;
33894
+ const previewUrl = snapshot.url ? urlPolicy(snapshot.url, "attachment") : null;
33586
33895
  const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
33587
- /* @__PURE__ */ jsx(AttachmentIcon, { snapshot }),
33588
- /* @__PURE__ */ jsxs("span", { className: "oe-attachment-body", children: [
33589
- /* @__PURE__ */ jsx("strong", { className: "oe-attachment-name", children: snapshot.name || "Attachment unavailable" }),
33590
- /* @__PURE__ */ jsx("span", { className: "oe-attachment-meta", children: state.phase === "resolving" ? "Loading\u2026" : state.phase === "missing" ? "Missing or inaccessible" : [formatAttachmentSize(snapshot.size), attachmentTypeLabel(snapshot)].filter(Boolean).join(" \xB7 ") })
33591
- ] })
33896
+ /* @__PURE__ */ jsx(AttachmentPreview, { snapshot, url: previewUrl }),
33897
+ /* @__PURE__ */ jsx("span", { className: "oe-attachment-body", children: /* @__PURE__ */ jsx("strong", { className: "oe-attachment-name", children: state.phase === "resolving" ? "Loading\u2026" : snapshot.name || "Attachment unavailable" }) })
33592
33898
  ] });
33593
33899
  if (state.phase === "ready" && runtime?.openAttachment) return /* @__PURE__ */ jsx("button", { className: "oe-attachment oe-attachment-viewer", "data-openeditor-attachment": true, onClick: () => void runtime.openAttachment?.(snapshot), type: "button", children: content });
33594
33900
  const href = snapshot.url ? urlPolicy(snapshot.url, "attachment") : null;
@@ -33846,7 +34152,7 @@ var ToggleListViewerItem = ({ children, initiallyOpen }) => {
33846
34152
  className: "oe-toggle-list-trigger",
33847
34153
  onClick: () => setOpen((current) => !current),
33848
34154
  type: "button",
33849
- children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "oe-toggle-list-chevron", children: "\u203A" })
34155
+ children: /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "oe-toggle-list-chevron" })
33850
34156
  }
33851
34157
  ),
33852
34158
  /* @__PURE__ */ jsx("div", { className: "oe-toggle-list-content", children })
@@ -33929,7 +34235,7 @@ var createEditorExtensions = (getPlaceholder, customExtensions, slashSuggestion)
33929
34235
  }),
33930
34236
  index_default10.configure({ horizontalRule: false, link: false, underline: false }),
33931
34237
  Divider,
33932
- index_default2.configure({ allowBase64: true }),
34238
+ OpenEditorImage.configure({ allowBase64: true }),
33933
34239
  index_default3.configure({ openOnClick: false }),
33934
34240
  index_default8,
33935
34241
  index_default6,
@@ -33998,6 +34304,7 @@ var useOpenEditorController = ({
33998
34304
  onFilePasteDrop,
33999
34305
  pageRuntime,
34000
34306
  attachmentRuntime,
34307
+ imageRuntime,
34001
34308
  enabledBlocks,
34002
34309
  slashMenuItems = [],
34003
34310
  extensions = EMPTY_EXTENSIONS,
@@ -34345,6 +34652,7 @@ var useOpenEditorController = ({
34345
34652
  extensions,
34346
34653
  pageRuntime,
34347
34654
  attachmentRuntime,
34655
+ imageRuntime,
34348
34656
  enabledBlocks,
34349
34657
  slashState,
34350
34658
  dismissSlashMenu,
@@ -34790,6 +35098,27 @@ var renderHeadingViewer = (node, children) => {
34790
35098
  return /* @__PURE__ */ jsx("h6", { ...props, children });
34791
35099
  }
34792
35100
  };
35101
+ var OpenEditorImageViewer = ({ node, resolveUrl }) => {
35102
+ const runtime = use(OpenEditorImageRuntimeContext);
35103
+ const durable = imageSnapshotFromNode(node);
35104
+ const [resolved, setResolved] = useState(null);
35105
+ useEffect(() => {
35106
+ if (!durable.imageId || !runtime?.resolveImage) {
35107
+ setResolved(null);
35108
+ return;
35109
+ }
35110
+ const controller = new AbortController();
35111
+ void runtime.resolveImage(durable.imageId, { signal: controller.signal }).then((snapshot) => {
35112
+ if (!controller.signal.aborted) setResolved(snapshot);
35113
+ }).catch(() => {
35114
+ if (!controller.signal.aborted) setResolved(null);
35115
+ });
35116
+ return () => controller.abort();
35117
+ }, [durable.imageId, runtime]);
35118
+ const src = resolvedImageSrc(resolved, true) ?? resolveUrl(durable.src, "image");
35119
+ if (!src) return null;
35120
+ return /* @__PURE__ */ jsx("img", { alt: durable.alt, height: durable.height ?? void 0, src, width: durable.width ?? void 0 });
35121
+ };
34793
35122
  var BUILT_IN_VIEWER_RENDERERS = {
34794
35123
  paragraph: ({ node, children }) => /* @__PURE__ */ jsx("p", { style: viewerTextAlignStyle(node), children: node.content?.length ? children : /* @__PURE__ */ jsx("br", {}) }),
34795
35124
  heading: ({ node, children }) => renderHeadingViewer(node, children),
@@ -34802,12 +35131,7 @@ var BUILT_IN_VIEWER_RENDERERS = {
34802
35131
  return /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { className: language ? `language-${language}` : void 0, children: getNodeText(node) }) });
34803
35132
  },
34804
35133
  divider: () => /* @__PURE__ */ jsx("hr", {}),
34805
- image: ({ node, resolveUrl }) => {
34806
- const src = resolveUrl(node.attrs?.src, "image");
34807
- if (!src) return null;
34808
- const dimension = (value) => typeof value === "string" || typeof value === "number" ? value : void 0;
34809
- return /* @__PURE__ */ jsx("img", { alt: typeof node.attrs?.alt === "string" ? node.attrs.alt : "", height: dimension(node.attrs?.height), src, title: typeof node.attrs?.title === "string" ? node.attrs.title : void 0, width: dimension(node.attrs?.width) });
34810
- },
35134
+ image: ({ node, resolveUrl }) => /* @__PURE__ */ jsx(OpenEditorImageViewer, { node, resolveUrl }),
34811
35135
  columns: ({ node, children }) => {
34812
35136
  const columnCount = Math.min(Math.max(node.content?.length ?? 0, 2), 4);
34813
35137
  return /* @__PURE__ */ jsx("section", { className: "oe-columns", "data-openeditor-columns": node.content?.length ?? 0, style: { "--oe-column-count": columnCount }, children });
@@ -34864,7 +35188,7 @@ var OpenEditorContent = ({
34864
35188
  className = "oe-canvas"
34865
35189
  }) => {
34866
35190
  const internalController = controller;
34867
- return /* @__PURE__ */ jsx(OpenEditorAttachmentRuntimeContext.Provider, { value: internalController.attachmentRuntime, children: /* @__PURE__ */ jsx(OpenEditorPageRuntimeContext.Provider, { value: internalController.pageRuntime, children: /* @__PURE__ */ jsx(EditorContent, { editor: internalController.editor, className }) }) });
35191
+ return /* @__PURE__ */ jsx(OpenEditorImageRuntimeContext.Provider, { value: internalController.imageRuntime, children: /* @__PURE__ */ jsx(OpenEditorAttachmentRuntimeContext.Provider, { value: internalController.attachmentRuntime, children: /* @__PURE__ */ jsx(OpenEditorPageRuntimeContext.Provider, { value: internalController.pageRuntime, children: /* @__PURE__ */ jsx(EditorContent, { editor: internalController.editor, className }) }) }) });
34868
35192
  };
34869
35193
  var OpenEditorViewer = ({
34870
35194
  document: document2,
@@ -34873,6 +35197,7 @@ var OpenEditorViewer = ({
34873
35197
  extensions = EMPTY_EXTENSIONS,
34874
35198
  pageRuntime,
34875
35199
  attachmentRuntime,
35200
+ imageRuntime,
34876
35201
  urlPolicy = openEditorPublicUrlPolicy
34877
35202
  }) => {
34878
35203
  const registry = useMemo(() => createReactBlockRegistry(extensions), [extensions]);
@@ -34880,9 +35205,9 @@ var OpenEditorViewer = ({
34880
35205
  ...Object.fromEntries(extensions.map((extension) => [extension.block.name, extension.viewer])),
34881
35206
  ...renderers
34882
35207
  }), [extensions, renderers]);
34883
- return /* @__PURE__ */ jsx(OpenEditorAttachmentRuntimeContext.Provider, { value: attachmentRuntime, children: /* @__PURE__ */ jsx(OpenEditorPageRuntimeContext.Provider, { value: pageRuntime, children: /* @__PURE__ */ jsx("div", { className, children: normalizeDocument(document2).content.map(
35208
+ return /* @__PURE__ */ jsx(OpenEditorImageRuntimeContext.Provider, { value: imageRuntime, children: /* @__PURE__ */ jsx(OpenEditorAttachmentRuntimeContext.Provider, { value: attachmentRuntime, children: /* @__PURE__ */ jsx(OpenEditorPageRuntimeContext.Provider, { value: pageRuntime, children: /* @__PURE__ */ jsx("div", { className, children: normalizeDocument(document2).content.map(
34884
35209
  (node, index) => renderViewerNode(node, `viewer-${index}`, mergedRenderers, registry, urlPolicy)
34885
- ) }) }) });
35210
+ ) }) }) }) });
34886
35211
  };
34887
35212
  var useOpenEditor = useOpenEditorController;
34888
35213