@jini-ai/ui 0.3.9 → 0.3.11

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.
Files changed (42) hide show
  1. package/dist/browser/useFileDropTarget.js +1 -1
  2. package/dist/features/execution/react/components/ByokProviderForm.js +3 -3
  3. package/dist/features/html-editor/canvas-embed-placeholders.d.ts +4 -0
  4. package/dist/features/html-editor/canvas-embed-placeholders.js +51 -1
  5. package/dist/features/html-editor/canvas-style.d.ts +11 -0
  6. package/dist/features/html-editor/canvas-style.js +16 -1
  7. package/dist/features/html-editor/index.d.ts +1 -1
  8. package/dist/features/html-editor/react/components/InteractiveHtmlEditor.d.ts +23 -11
  9. package/dist/features/html-editor/react/components/InteractiveHtmlEditor.js +5 -3
  10. package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.d.ts +11 -1
  11. package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +356 -30
  12. package/dist/features/html-editor/source-splice.d.ts +56 -0
  13. package/dist/features/html-editor/source-splice.js +89 -0
  14. package/dist/features/i18n/dictionaries/settings-dialog.ar.js +2 -0
  15. package/dist/features/i18n/dictionaries/settings-dialog.bn.js +2 -0
  16. package/dist/features/i18n/dictionaries/settings-dialog.de.js +2 -0
  17. package/dist/features/i18n/dictionaries/settings-dialog.en.js +2 -0
  18. package/dist/features/i18n/dictionaries/settings-dialog.es.js +2 -0
  19. package/dist/features/i18n/dictionaries/settings-dialog.fa.js +2 -0
  20. package/dist/features/i18n/dictionaries/settings-dialog.fr.js +2 -0
  21. package/dist/features/i18n/dictionaries/settings-dialog.hi.js +2 -0
  22. package/dist/features/i18n/dictionaries/settings-dialog.hu.js +2 -0
  23. package/dist/features/i18n/dictionaries/settings-dialog.id.js +2 -0
  24. package/dist/features/i18n/dictionaries/settings-dialog.it.js +2 -0
  25. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +2 -0
  26. package/dist/features/i18n/dictionaries/settings-dialog.ko.js +2 -0
  27. package/dist/features/i18n/dictionaries/settings-dialog.pl.js +2 -0
  28. package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +2 -0
  29. package/dist/features/i18n/dictionaries/settings-dialog.ru.js +2 -0
  30. package/dist/features/i18n/dictionaries/settings-dialog.th.js +2 -0
  31. package/dist/features/i18n/dictionaries/settings-dialog.tr.js +2 -0
  32. package/dist/features/i18n/dictionaries/settings-dialog.uk.js +2 -0
  33. package/dist/features/i18n/dictionaries/settings-dialog.ur.js +2 -0
  34. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +2 -0
  35. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +2 -0
  36. package/dist/features/mcp-ui/surfaces/bridge.js +1 -0
  37. package/dist/features/mcp-ui/surfaces/confirmation.d.ts +8 -0
  38. package/dist/features/mcp-ui/surfaces/confirmation.js +65 -6
  39. package/dist/features/mcp-ui/surfaces/document.d.ts +11 -1
  40. package/dist/features/mcp-ui/surfaces/document.js +22 -2
  41. package/dist/features/mcp-ui/surfaces/form.js +12 -11
  42. package/package.json +4 -3
@@ -22,7 +22,7 @@ export function useFileDropTarget(onUploadFiles) {
22
22
  onUploadFilesRef.current = onUploadFiles;
23
23
  const clearDropReadError = useCallback(() => setDropReadError(null), []);
24
24
  // This target's own `onDrop`/`onDragLeave` are not guaranteed to run when a drag ends: a host that
25
- // handles a drop itself in the capture phase and stops propagation (Tovu's folder-drop-to-path) or a
25
+ // handles a drop itself in the capture phase and stops propagation (a host's folder-drop-to-path) or a
26
26
  // drag that ends elsewhere leaves the drag-over state stuck on. Window-capture listeners run before
27
27
  // any element handler, so no `stopPropagation` can skip them. Registered only while dragging.
28
28
  useEffect(() => {
@@ -101,8 +101,8 @@ export function ByokProviderForm({ config, onConfigChange, preset, presets = DEF
101
101
  // child it grew the field and pushed the bottom-aligned button off the input's line every
102
102
  // time discovery failed. Same element, same `.jini-field-hint.is-error[role="status"]`
103
103
  // identity host suites locate it by — only its parent changed.
104
- _jsx("span", { className: "jini-field-hint is-error", role: "status", children: t('Could not load live models: {message}', { message: modelDiscovery.message }) })) : null, canTestConnection && (connectionTest.status === 'ok' || connectionTest.status === 'error') ? (_jsx("div", { className: "jini-byok-test-row", children: _jsx("span", { className: `jini-byok-test-status is-${connectionTest.status}`, role: connectionTest.status === 'error' ? 'alert' : 'status', children: connectionTest.status === 'ok'
105
- ? (connectionTest.message ?? t('Connection succeeded'))
106
- : connectionTest.message }) })) : null, formFooter ? _jsx("div", { className: "jini-byok-form-footer", children: formFooter }) : null] }));
104
+ _jsx("span", { className: "jini-field-hint is-error", role: "status", children: t('Could not load live models: {message}', { message: t(modelDiscovery.message) }) })) : null, canTestConnection && (connectionTest.status === 'ok' || connectionTest.status === 'error') ? (_jsx("div", { className: "jini-byok-test-row", children: _jsx("span", { className: `jini-byok-test-status is-${connectionTest.status}`, role: connectionTest.status === 'error' ? 'alert' : 'status', children: connectionTest.status === 'ok'
105
+ ? (connectionTest.message !== undefined ? t(connectionTest.message) : t('Connection succeeded'))
106
+ : t(connectionTest.message) }) })) : null, formFooter ? _jsx("div", { className: "jini-byok-form-footer", children: formFooter }) : null] }));
107
107
  }
108
108
  //# sourceMappingURL=ByokProviderForm.js.map
@@ -29,6 +29,10 @@
29
29
  * (untouched, still literally empty) marker means there is nothing inside the marker for that re-parse
30
30
  * to ever pick up, regardless of whether that `isProtectedElement` gap is ever closed.
31
31
  *
32
+ * **Safe to call again after every structural edit** (the hook does, on the tick after any component
33
+ * add or remove): each pass first drops cards whose marker has gone (see `removeStaleCards`), then
34
+ * wraps any marker not already inside a card.
35
+ *
32
36
  * Re-entrancy (defensive, mirroring `canvas-content-wrapper.ts`'s own `WRAPPER_ROOT_MARKER` guard): a
33
37
  * marker already sitting inside a placeholder card (`el.closest('[' + CARD_ROOT_MARKER + ']')`) is
34
38
  * skipped, so a second call — or a caller's own `load` handler firing twice — cannot nest a second card
@@ -29,6 +29,10 @@
29
29
  * (untouched, still literally empty) marker means there is nothing inside the marker for that re-parse
30
30
  * to ever pick up, regardless of whether that `isProtectedElement` gap is ever closed.
31
31
  *
32
+ * **Safe to call again after every structural edit** (the hook does, on the tick after any component
33
+ * add or remove): each pass first drops cards whose marker has gone (see `removeStaleCards`), then
34
+ * wraps any marker not already inside a card.
35
+ *
32
36
  * Re-entrancy (defensive, mirroring `canvas-content-wrapper.ts`'s own `WRAPPER_ROOT_MARKER` guard): a
33
37
  * marker already sitting inside a placeholder card (`el.closest('[' + CARD_ROOT_MARKER + ']')`) is
34
38
  * skipped, so a second call — or a caller's own `load` handler firing twice — cannot nest a second card
@@ -38,12 +42,31 @@
38
42
  * `load`) can recognize an already-decorated marker via `el.closest` and skip it — see this file's
39
43
  * header. */
40
44
  const CARD_ROOT_MARKER = 'data-tovu-embed-placeholder-root';
45
+ /** Flags every child the card itself builds (icon, text), so the one unflagged child is always the
46
+ * marker. {@link OVERLAY_RULE} and the stale-card check in `removeStaleCards` both rely on it. */
47
+ const CARD_CHROME_MARKER = 'data-tovu-embed-placeholder-chrome';
48
+ /** Identifies the one `<style>` this module adds to the canvas document's `<head>`. */
49
+ const OVERLAY_STYLE_MARKER = 'data-tovu-embed-placeholder-style';
50
+ /**
51
+ * Stretches the (empty) marker over its card as a transparent overlay, so a click anywhere on the card
52
+ * lands on the marker element itself. GrapesJS resolves a click by walking up from the event target to
53
+ * the nearest element it rendered; the card is not one of those, so without this a click on the card
54
+ * selected whatever block wraps the embed, and Delete then removed that block together with the embed.
55
+ * With it, the editor selects (and outlines) the embed. A stylesheet rule rather than an inline style,
56
+ * so the marker's own attributes stay untouched. `!important` because the host's theme may style the
57
+ * marker element.
58
+ */
59
+ const OVERLAY_RULE = `[${CARD_ROOT_MARKER}] > :not([${CARD_CHROME_MARKER}])` +
60
+ ' { position: absolute !important; inset: 0 !important; margin: 0 !important; width: auto !important;' +
61
+ ' height: auto !important; min-width: 0 !important; max-width: none !important; min-height: 0 !important;' +
62
+ ' display: block !important; background: transparent !important; }';
41
63
  /** One CSS declaration list, as `property:value` pairs, for the card's own opaque chrome. Deliberately
42
64
  * NOT derived from the host's theme in any way — themes are arbitrary and downloadable, and this card
43
65
  * must stay legible whether the canvas underneath is a light theme, a dark theme, or (mid-edit) no
44
66
  * theme at all. An opaque background plus a fixed, high-contrast foreground guarantees that
45
67
  * regardless of what renders behind it, unlike a translucent or theme-token-based treatment would. */
46
68
  const CARD_STYLE = [
69
+ 'position:relative',
47
70
  'display:flex',
48
71
  'align-items:flex-start',
49
72
  'gap:8px',
@@ -91,6 +114,7 @@ function buildPlaceholderCard(doc, descriptor) {
91
114
  card.setAttribute(CARD_ROOT_MARKER, '');
92
115
  card.setAttribute('style', CARD_STYLE);
93
116
  const text = doc.createElement('div');
117
+ text.setAttribute(CARD_CHROME_MARKER, '');
94
118
  const heading = doc.createElement('div');
95
119
  heading.setAttribute('style', 'font-weight:600;');
96
120
  heading.textContent = `Placeholder — ${descriptor.kindLabel}`;
@@ -101,7 +125,9 @@ function buildPlaceholderCard(doc, descriptor) {
101
125
  note.setAttribute('style', 'opacity:.6;margin-top:4px;font-size:11px;');
102
126
  note.textContent = 'Shown here for editing only — the real content renders on the published page.';
103
127
  text.append(heading, identity, note);
104
- card.append(buildPlaceholderIcon(doc), text);
128
+ const icon = buildPlaceholderIcon(doc);
129
+ icon.setAttribute(CARD_CHROME_MARKER, '');
130
+ card.append(icon, text);
105
131
  return card;
106
132
  }
107
133
  /**
@@ -136,6 +162,28 @@ function applyResolvedMaxWidth(marker, card) {
136
162
  if (maxWidth && maxWidth !== 'none')
137
163
  card.style.setProperty('max-width', maxWidth);
138
164
  }
165
+ /** Adds {@link OVERLAY_RULE} to `doc`'s `<head>` once. Idempotent. */
166
+ function ensureOverlayRule(doc) {
167
+ if (doc.head.querySelector(`style[${OVERLAY_STYLE_MARKER}]`))
168
+ return;
169
+ const style = doc.createElement('style');
170
+ style.setAttribute(OVERLAY_STYLE_MARKER, '');
171
+ style.textContent = OVERLAY_RULE;
172
+ doc.head.appendChild(style);
173
+ }
174
+ /**
175
+ * Removes every card whose marker is no longer inside it. The card lives only in the canvas DOM, so the
176
+ * editor knows nothing about it: deleting the embed removes just the marker element (leaving a ghost
177
+ * card that still reads "Placeholder"), and moving it takes the marker out (leaving an empty card
178
+ * behind). The next pass of `applyCanvasEmbedPlaceholders` then wraps the moved marker again.
179
+ */
180
+ function removeStaleCards(body) {
181
+ for (const card of Array.from(body.querySelectorAll(`[${CARD_ROOT_MARKER}]`))) {
182
+ const hasMarker = Array.from(card.children).some((child) => !child.hasAttribute(CARD_CHROME_MARKER));
183
+ if (!hasMarker)
184
+ card.remove();
185
+ }
186
+ }
139
187
  /**
140
188
  * Scans every element under `body` once and, for each one `describe` recognizes (returns a descriptor
141
189
  * for), replaces its visible slot with an explicit placeholder card — see this file's header for the
@@ -155,6 +203,7 @@ function applyResolvedMaxWidth(marker, card) {
155
203
  */
156
204
  export function applyCanvasEmbedPlaceholders(body, describe) {
157
205
  const doc = body.ownerDocument;
206
+ removeStaleCards(body);
158
207
  // Snapshotted into an array before any mutation starts: this function inserts new ancestors as it
159
208
  // goes, and a live `querySelectorAll` result must never be iterated while the tree it was taken over
160
209
  // is being restructured underneath it — same discipline `applyCanvasContentWrapper` applies to
@@ -185,6 +234,7 @@ export function applyCanvasEmbedPlaceholders(body, describe) {
185
234
  applyResolvedMaxWidth(el, card);
186
235
  parent.insertBefore(card, el);
187
236
  card.appendChild(el); // Moves the SAME node (auto-detaches from `parent`) — never cloned.
237
+ ensureOverlayRule(doc);
188
238
  }
189
239
  }
190
240
  //# sourceMappingURL=canvas-embed-placeholders.js.map
@@ -51,6 +51,17 @@ export interface CanvasStyling {
51
51
  */
52
52
  contentWrapper?: readonly CanvasContentWrapperNode[];
53
53
  }
54
+ /**
55
+ * CSS class the canvas body carries while `react/hooks/useInteractiveHtmlEditor.ts`'s
56
+ * `revealCanvasWhenStylesheetsSettle` is still waiting on `canvas.styles`' `<link>` elements to settle
57
+ * (Bug B, Slice B1, this repo's `pages-redo` plan): every Interactive mount re-fetches the theme
58
+ * stylesheet (no browser cache short-circuit is guaranteed), and GrapesJS renders the canvas body
59
+ * without waiting for it, so a slow or restarting API left the canvas showing raw browser-default
60
+ * styling (Times New Roman, blue links) until the stylesheet arrived. Exported so the hook that
61
+ * toggles this class on/off and this module's CSS rule for it never drift onto two different literal
62
+ * strings.
63
+ */
64
+ export declare const CANVAS_STYLES_PENDING_CLASS = "gjs-canvas-styles-pending";
54
65
  /** The `canvas` slice of a GrapesJS editor config — structurally the subset of its `CanvasConfig`
55
66
  * this module decides, kept as its own type so the builder is assertable without a live editor. */
56
67
  export interface CanvasStyleConfig {
@@ -25,6 +25,21 @@ const CANVAS_SCROLLBAR_STYLE = [
25
25
  /** The half of GrapesJS's default `canvas.frameStyle` this module conditionally drops — see the file
26
26
  * header for why it cannot simply be left in place. */
27
27
  const CANVAS_DEFAULT_BODY_BACKGROUND = "body { background-color: #fff }";
28
+ /**
29
+ * CSS class the canvas body carries while `react/hooks/useInteractiveHtmlEditor.ts`'s
30
+ * `revealCanvasWhenStylesheetsSettle` is still waiting on `canvas.styles`' `<link>` elements to settle
31
+ * (Bug B, Slice B1, this repo's `pages-redo` plan): every Interactive mount re-fetches the theme
32
+ * stylesheet (no browser cache short-circuit is guaranteed), and GrapesJS renders the canvas body
33
+ * without waiting for it, so a slow or restarting API left the canvas showing raw browser-default
34
+ * styling (Times New Roman, blue links) until the stylesheet arrived. Exported so the hook that
35
+ * toggles this class on/off and this module's CSS rule for it never drift onto two different literal
36
+ * strings.
37
+ */
38
+ export const CANVAS_STYLES_PENDING_CLASS = "gjs-canvas-styles-pending";
39
+ /** The rule that makes {@link CANVAS_STYLES_PENDING_CLASS} actually hide the canvas. Unconditional —
40
+ * unlike {@link CANVAS_DEFAULT_BODY_BACKGROUND}, it doesn't depend on whether the host supplies its
41
+ * own styling, since the class is only ever present while a fetch is genuinely in flight. */
42
+ const CANVAS_STYLES_PENDING_RULE = `.${CANVAS_STYLES_PENDING_CLASS} { visibility: hidden }`;
28
43
  /**
29
44
  * Whether the host has taken over the canvas document's appearance. Either input is enough: a
30
45
  * stylesheet alone is the common case (a real theme's CSS), and raw `css` alone is enough for a host
@@ -39,7 +54,7 @@ function hostStylesTheCanvas(styling) {
39
54
  * and followed by the host's raw `css` when supplied.
40
55
  */
41
56
  function buildCanvasFrameStyle(styling) {
42
- const rules = [CANVAS_SCROLLBAR_STYLE];
57
+ const rules = [CANVAS_SCROLLBAR_STYLE, CANVAS_STYLES_PENDING_RULE];
43
58
  if (!hostStylesTheCanvas(styling))
44
59
  rules.unshift(CANVAS_DEFAULT_BODY_BACKGROUND);
45
60
  if (styling.css)
@@ -1,5 +1,5 @@
1
1
  export { InteractiveHtmlEditor } from './react/components/InteractiveHtmlEditor.js';
2
- export type { InteractiveHtmlEditorProps } from './react/components/InteractiveHtmlEditor.js';
2
+ export type { InteractiveHtmlEditorHandle, InteractiveHtmlEditorProps } from './react/components/InteractiveHtmlEditor.js';
3
3
  export { useInteractiveHtmlEditor } from './react/hooks/useInteractiveHtmlEditor.js';
4
4
  export type { CanvasContentWrapperNode, CanvasStyling } from './canvas-style.js';
5
5
  export type { CanvasEmbedPlaceholderDescriptor } from './canvas-embed-placeholders.js';
@@ -9,10 +9,12 @@ import type { CanvasStyling } from '../../canvas-style.js';
9
9
  * `<textarea>`. See `react/hooks/useInteractiveHtmlEditor.ts` for why this is uncontrolled
10
10
  * (mount-once) rather than reacting to `html`/`isProtectedElement` prop changes after construction.
11
11
  *
12
- * **Scope, this pass: text editing + basic formatting only.** GrapesJS also does drag/drop block
13
- * manipulation, a Style Manager, and a Layers panel; none of that chrome is enabled here (`panels:
14
- * { defaults: [] }`, `blockManager: { blocks: [] }` in the hook) — Gutenberg-style block
15
- * move/duplicate/lock/group is explicitly deferred, not merely unbuilt.
12
+ * **Scope: text editing, basic formatting, and GrapesJS's own block move/copy/delete.** Selecting a
13
+ * block shows GrapesJS's selection toolbar (move, copy, delete) and Delete removes it; the owner kept
14
+ * these on purpose (2026-09-23). A text edit is saved as a byte-minimal splice of the original
15
+ * `html`; a block move/copy/delete falls back to a cleaned full re-serialization, with a dev warning
16
+ * naming the cause (see the hook's `serializeWithSplice`). No Style Manager, Layers panel or block
17
+ * library is enabled (`panels: { defaults: [] }`, `blockManager: { blocks: [] }` in the hook).
16
18
  *
17
19
  * **`GJS_STYLE_VARS` below is a consequence of that same choice, not a separate styling decision.**
18
20
  * GrapesJS's own vendor CSS unconditionally reserves layout space for the default chrome this
@@ -27,9 +29,10 @@ import type { CanvasStyling } from '../../canvas-style.js';
27
29
  * its own stylesheet.
28
30
  *
29
31
  * **This component has zero knowledge of any host's embed/placeholder conventions.** `isProtectedElement`
30
- * is an optional predicate the caller supplies to lock specific elements (identified however the
31
- * caller likes) out of every GrapesJS interaction that could edit, move, or remove them — not
32
- * editable, draggable, droppable, removable, or selectable. When omitted, no elements are protected.
32
+ * is an optional predicate the caller supplies to turn specific elements (identified however the
33
+ * caller likes) into atomic blocks: their content can never be edited, dropped into or styled, but a
34
+ * click selects the element itself (never the block around it), and it can be deliberately moved,
35
+ * copied or deleted. When omitted, no elements are protected.
33
36
  * `describeEmbedPlaceholder` is the same idea for a second, independent concern: recognizing an
34
37
  * unresolved embed marker and labeling it for `../../canvas-embed-placeholders.ts`'s explicit
35
38
  * placeholder card. See `@jini-ai/admin/react`'s `InteractiveHtmlEditor` for a worked example of both:
@@ -69,11 +72,12 @@ export interface InteractiveHtmlEditorProps {
69
72
  html: string;
70
73
  /** Fires with the editor's current serialized HTML on every content edit. Does not persist
71
74
  * anything itself — the caller decides when/whether to save, same as an HTML source textarea's
72
- * `onChange`. */
75
+ * `onChange`. A host that saves or unmounts should `await ref.flush()` first, to sync an RTE
76
+ * session that is still open — see `InteractiveHtmlEditorHandle`. */
73
77
  onChange: (html: string) => void;
74
78
  className?: string;
75
- /** Identifies elements that must never become editable, draggable, removable, or droppable inside
76
- * the editor. Read once, at mount, same as `html`. Predicates that check attributes should use
79
+ /** Identifies elements whose content must never be edited or dropped into inside the editor. Each
80
+ * one is selectable as a single block that can be deliberately moved, copied or deleted. Read once, at mount, same as `html`. Predicates that check attributes should use
77
81
  * `hasAttributeOnAnyNodeShape` (exported alongside this component) rather than `el.hasAttribute`
78
82
  * directly — see that helper's own doc for why. Omit to protect nothing. */
79
83
  isProtectedElement?: (el: Element) => boolean;
@@ -90,5 +94,13 @@ export interface InteractiveHtmlEditorProps {
90
94
  * of any host's marker convention that `isProtectedElement` documents above. */
91
95
  describeEmbedPlaceholder?: (el: Element) => CanvasEmbedPlaceholderDescriptor | undefined;
92
96
  }
93
- export declare function InteractiveHtmlEditor({ html, onChange, className, isProtectedElement, canvasStyling, describeEmbedPlaceholder, }: InteractiveHtmlEditorProps): import("react").JSX.Element;
97
+ /** Imperative handle exposed via `ref`. A host calls `flush()` right before it saves or unmounts, to
98
+ * sync an open RTE session's still-pending edit into `onChange` first — see `onChange`'s own doc and
99
+ * `useInteractiveHtmlEditor`'s `flush` for the full rationale (GrapesJS only syncs an RTE session's
100
+ * text on close, never on every keystroke). Resolves the flushed HTML, or `undefined` when nothing
101
+ * had changed since mount. */
102
+ export interface InteractiveHtmlEditorHandle {
103
+ flush(): Promise<string | undefined>;
104
+ }
105
+ export declare const InteractiveHtmlEditor: import("react").ForwardRefExoticComponent<InteractiveHtmlEditorProps & import("react").RefAttributes<InteractiveHtmlEditorHandle>>;
94
106
  //# sourceMappingURL=InteractiveHtmlEditor.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { forwardRef, useImperativeHandle } from 'react';
2
3
  import 'grapesjs/dist/css/grapes.min.css';
3
4
  import { useInteractiveHtmlEditor } from '../hooks/useInteractiveHtmlEditor.js';
4
5
  /** See this file's header for why these exist: they zero out layout space GrapesJS's default CSS
@@ -7,9 +8,10 @@ const GJS_STYLE_VARS = {
7
8
  '--gjs-left-width': '0px',
8
9
  '--gjs-canvas-top': '0px',
9
10
  };
10
- export function InteractiveHtmlEditor({ html, onChange, className, isProtectedElement, canvasStyling, describeEmbedPlaceholder, }) {
11
- const { containerRef } = useInteractiveHtmlEditor(html, onChange, isProtectedElement, canvasStyling, describeEmbedPlaceholder);
11
+ export const InteractiveHtmlEditor = forwardRef(function InteractiveHtmlEditor({ html, onChange, className, isProtectedElement, canvasStyling, describeEmbedPlaceholder }, ref) {
12
+ const { containerRef, flush } = useInteractiveHtmlEditor(html, onChange, isProtectedElement, canvasStyling, describeEmbedPlaceholder);
13
+ useImperativeHandle(ref, () => ({ flush }), [flush]);
12
14
  const wrapperClassName = className ? `interactive-html-editor ${className}` : 'interactive-html-editor';
13
15
  return _jsx("div", { ref: containerRef, className: wrapperClassName, style: GJS_STYLE_VARS });
14
- }
16
+ });
15
17
  //# sourceMappingURL=InteractiveHtmlEditor.js.map
@@ -2,7 +2,7 @@ import { type CanvasEmbedPlaceholderDescriptor } from '../../canvas-embed-placeh
2
2
  import { type CanvasStyling } from '../../canvas-style.js';
3
3
  /**
4
4
  * Owns one GrapesJS `Editor` instance for the lifetime of the calling component's mount. `onChange`
5
- * fires with the editor's current serialized content (see `serializeEditorContent`) on every content
5
+ * fires with the editor's current serialized content (see `serializeWithSplice`) on every content
6
6
  * mutation (GrapesJS's `update` event — components, styles, or attributes changing); it is not
7
7
  * debounced, so a caller replacing an existing uncontrolled `<textarea>` (which already pushes a full
8
8
  * value on every keystroke) sees matching behavior.
@@ -22,6 +22,15 @@ import { type CanvasStyling } from '../../canvas-style.js';
22
22
  * `class="frame"` forever, inert and unrendered. `.gjs-frame` — the class `FrameView.render()`
23
23
  * assigns — is what callers should select on, never a bare `iframe` under the wrapper.)
24
24
  *
25
+ * **`flush()` exists because GrapesJS's RTE only syncs on `disableEditing`, and `update` is
26
+ * double-debounced and never fires after `off()`.** A host that calls `onChange` on every keystroke
27
+ * (like an uncontrolled `<textarea>`) still misses the operator's last edit if they click Save or
28
+ * navigate away while an RTE session is still open: GrapesJS writes that session's text into the
29
+ * component model only when it closes, and by the time a host's save handler runs, this hook has
30
+ * already stopped listening (or the debounce hasn't fired yet). `flush` closes any open RTE session,
31
+ * computes and emits the current content the same way `handleUpdate` does, and hands the result back
32
+ * directly — see its own doc below for exactly what it returns and when.
33
+ *
25
34
  * @complexity O(1) setup/teardown. Steady-state cost is GrapesJS's own serialization per edit,
26
35
  * proportional to document size — not something this hook controls or should hide a note about
27
36
  * beyond that.
@@ -29,5 +38,6 @@ import { type CanvasStyling } from '../../canvas-style.js';
29
38
  */
30
39
  export declare function useInteractiveHtmlEditor(html: string, onChange: (html: string) => void, isProtectedElement?: (el: Element) => boolean, canvasStyling?: CanvasStyling, describeEmbedPlaceholder?: (el: Element) => CanvasEmbedPlaceholderDescriptor | undefined): {
31
40
  containerRef: import("react").RefObject<HTMLDivElement | null>;
41
+ flush: () => Promise<string | undefined>;
32
42
  };
33
43
  //# sourceMappingURL=useInteractiveHtmlEditor.d.ts.map