@bloklabs/angular 1.15.2 → 1.16.1

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.
@@ -1,18 +1,16 @@
1
- import * as i0 from '@angular/core';
2
- import { AfterViewInit, DoCheck, EventEmitter, OnDestroy, InjectionToken, EnvironmentProviders, Type, Signal, ElementRef } from '@angular/core';
3
- import { ControlValueAccessor } from '@angular/forms';
4
- import { BlokConfig, EditorWidth, Blok, OutputData, API, BlockMutationEvent, ResolvedTheme, BlocksRenderedPayload, BlockRenderedPayload, OutputBlockData, ThemeMode, LooseOutputData, BlockToolData, BlockTuneData, MarkdownImportConfig, BlockOrigin, BlockToolConstructable, ToolboxConfig, BlockAPI, BlockToolConstructorOptions } from '@bloklabs/core';
5
-
1
+ import * as i0 from "@angular/core";
2
+ import { AfterViewInit, DoCheck, ElementRef, EnvironmentProviders, EventEmitter, InjectionToken, OnDestroy, Signal, Type } from "@angular/core";
3
+ import { ControlValueAccessor } from "@angular/forms";
4
+ import { API, BlockAPI, BlockMutationEvent, BlockOrigin, BlockPosition, BlockRenderedPayload, BlockToolConstructable, BlockToolConstructorOptions, BlockToolData, BlockTuneData, BlocksRenderedPayload, Blok, BlokConfig, EditorWidth, LooseOutputBlockData, LooseOutputData, MarkdownImportConfig, OutputBlockData, OutputData, ReadOnlyModeConfig, ResolvedTheme, ThemeMode, ToolboxConfig } from "@bloklabs/core";
6
5
  /**
7
6
  * Configuration for the Angular adapter. Same as `BlokConfig` but without
8
7
  * `holder` (the adapter owns the host element), plus the reactive `width` prop.
9
8
  * Mirrors the React adapter's `UseBlokConfig`.
10
9
  */
11
10
  type BlokAngularConfig = Omit<BlokConfig, 'holder'> & {
12
- /** Editor content width mode. Synced reactively after mount via `editor.width.set()`. */
13
- width?: EditorWidth;
11
+ /** Editor content width mode. Synced reactively after mount via `editor.width.set()`. */
12
+ width?: EditorWidth;
14
13
  };
15
-
16
14
  /**
17
15
  * The blessed all-in-one Angular component for embedding Blok (mirrors React's
18
16
  * `BlokEditor`). Delegates instance lifecycle to an internal `BlokContentDirective`
@@ -35,227 +33,343 @@ type BlokAngularConfig = Omit<BlokConfig, 'holder'> & {
35
33
  * `BlokContentDirective`. Each reactive input is backed by an internal `signal`
36
34
  * so effect-based syncing still works; the public template API is unchanged.
37
35
  */
38
- declare class BlokEditorComponent implements AfterViewInit, DoCheck, ControlValueAccessor {
39
- private readonly ngZone;
40
- /** App-wide defaults from `provideBlok()`; merged UNDER per-instance inputs. */
41
- private readonly defaults;
42
- private contentQuery?;
43
- /** Signal bridge for the @ViewChild directive so effects react to its arrival. */
44
- private readonly content;
45
- /**
46
- * Output half of two-way `[(data)]`. Emits the editor's serialized content on
47
- * every change. Wiring the core `onSave` callback is gated on this being
48
- * observed, since its mere presence makes the core serialize on every batch.
49
- */
50
- /** Emits the live Blok instance once ready, after `instance()` is populated. */
51
- readonly ready: EventEmitter<Blok>;
52
- readonly dataChange: EventEmitter<OutputData>;
53
- /** Fires with the full serialized content on every change (notification half). */
54
- readonly save: EventEmitter<OutputData>;
55
- /** Raw block mutation channel (core `onChange`). */
56
- readonly change: EventEmitter<{
57
- api: API;
58
- event: BlockMutationEvent | BlockMutationEvent[];
59
- }>;
60
- /** Fires after the editor finishes (re-)rendering (core `onAfterRender`). */
61
- readonly afterRender: EventEmitter<API>;
62
- /** Fires with the resolved theme whenever it changes (core `onThemeChange`). */
63
- readonly themeChange: EventEmitter<ResolvedTheme>;
64
- /** Fires after a batch render completes (core `blocks:rendered` event). */
65
- readonly blocksRendered: EventEmitter<BlocksRenderedPayload>;
66
- /** Fires for each block rendered into the DOM (core `block:rendered` event). */
67
- readonly blockRendered: EventEmitter<BlockRenderedPayload>;
68
- /**
69
- * Transform hook applied to blocks before render (core `onBeforeRender`). Must
70
- * return the (possibly modified) block list. An input, not an output, because
71
- * it returns a value.
72
- */
73
- onBeforeRender?: (blocks: OutputBlockData[]) => OutputBlockData[];
74
- /**
75
- * Transform hook applied to pasted HTML (core `onBeforePaste`). Returns the
76
- * (possibly modified) html, or null to drop the paste.
77
- */
78
- onBeforePaste?: (html: string) => string | null;
79
- /**
80
- * Error channel (core `onError`). Fires with the raised error whenever an
81
- * editor operation — currently serialization — fails instead of only logging.
82
- */
83
- onError?: BlokConfig['onError'];
84
- private readonly readOnly$;
85
- set readOnly(value: BlokConfig['readOnly'] | undefined);
86
- private readonly hideToolbar$;
87
- set hideToolbar(value: boolean | undefined);
88
- private readonly toolbarPosition$;
89
- set toolbarPosition(value: BlokConfig['toolbarPosition'] | undefined);
90
- private readonly inlineToolbar$;
91
- set inlineToolbar(value: boolean | string[] | undefined);
92
- private readonly theme$;
93
- set theme(value: ThemeMode | undefined);
94
- private readonly width$;
95
- set width(value: EditorWidth | undefined);
96
- private readonly placeholder$;
97
- set placeholder(value: string | false | undefined);
98
- /**
99
- * Theme tokens. Construction-only config forced hosts with a live light/dark
100
- * toggle to recreate the editor or hand-write the global stylesheet Blok
101
- * already injects; this drives the runtime `tokens` API instead.
102
- */
103
- private readonly styleTokens$;
104
- set styleTokens(value: Record<string, string> | undefined);
105
- /**
106
- * Locale, host message overrides and text direction. `config.i18n` was
107
- * consumed once at boot, so a host driving a language switcher had to
108
- * recreate the editor (losing caret, focus and undo stack) to relabel the
109
- * UI; this drives the runtime `i18n.update` API instead. `defaultLocale` is
110
- * not forwarded — it only affects the INITIAL locale resolution.
111
- */
112
- private readonly i18n$;
113
- set i18n(value: BlokConfig['i18n'] | undefined);
114
- private readonly autofocus$;
115
- set autofocus(value: boolean | undefined);
116
- private readonly data$;
117
- set data(value: OutputData | LooseOutputData | null | undefined);
118
- tools?: BlokConfig['tools'];
119
- /** Host-supplied per-type block migrations applied at load (merged across layers). */
120
- migrations?: BlokConfig['migrations'];
121
- /**
122
- * Escape hatch: a full config object for keys without a dedicated input
123
- * (sanitizer, minHeight, …). Layered between provideBlok
124
- * defaults and the discrete inputs.
125
- */
126
- config?: Partial<BlokAngularConfig>;
127
- /** Changing this input's identity destroys and recreates the editor (≙ React `deps`). */
128
- recreateKey: unknown;
129
- /**
130
- * Content the editor currently reflects. Set when seeded, when the data effect
131
- * renders, when the editor emits its own output (`coreOnSave`) and when the
132
- * imperative `render()` facade lands. A controlled `data` echo that
133
- * content-equals this baseline is a no-op, so it won't clobber the caret.
134
- * Renders are serialized via `renderChain`. `undefined` means "nothing recorded
135
- * yet" — never an empty document.
136
- */
137
- private lastRenderedData?;
138
- /** Last token set pushed through `tokens.set`, for deep-equal deduping. */
139
- private appliedTokens?;
140
- /** Last i18n config pushed through `i18n.update`, for deep-equal deduping. */
141
- private appliedI18n?;
142
- /** Editor instance the i18n baseline above belongs to. */
143
- private i18nAppliedFor;
144
- /** Last value pushed through `tools.setInlineToolbar`, for content-compare deduping. */
145
- private appliedInlineToolbar?;
146
- private seededEditor;
147
- private renderChain;
148
- /** One collaboration warning per component instance (see the data effect). */
149
- private collaborationWarned;
150
- /**
151
- * The `collaboration` the live editor was CONSTRUCTED with. The key is
152
- * mount-fixed, so dropping it from `[config]` (`collaboration: on ? {...} :
153
- * undefined`) cannot turn a live session into a single-player editor —
154
- * reading the current input instead would let that change fall through into
155
- * render(), which refuses under collaboration and rejects with nothing
156
- * surfaced anywhere.
157
- */
158
- private constructedCollaboration;
159
- /** Editor the capture above belongs to (recaptured on recreate). */
160
- private collaborationCapturedFor;
161
- /** Registered by `ControlValueAccessor.registerOnChange` (Angular forms). */
162
- private cvaOnChange?;
163
- private cvaOnTouched?;
164
- /**
165
- * Core `onSave` wrapper. Records the editor's own serialized output as the
166
- * rendered baseline BEFORE notifying Angular, so a controlled consumer echoing
167
- * it straight back into `data` deep-equals the baseline and is deduped to a
168
- * no-op (no redundant render, no caret reset). Re-enters the Angular zone since
169
- * the editor runs outside it.
170
- *
171
- * Falls back to an escape-hatch `[config]` callback when nothing Angular-side
172
- * consumes the save — the baseline is recorded either way, so the controlled
173
- * `data` dedupe works on both paths.
174
- */
175
- private readonly coreOnSave;
176
- /**
177
- * Stable wrappers for the remaining live callbacks. Each resolves its target
178
- * at call time (an observed output, else the `[config]` escape hatch), so a
179
- * pushed handler never goes stale and only PRESENCE has to be synced.
180
- */
181
- private readonly coreOnChange;
182
- private readonly coreOnAfterRender;
183
- private readonly coreOnBeforeRender;
184
- private readonly coreOnEnter;
185
- private readonly coreOnSubmit;
186
- /** provideBlok defaults merged under the `[config]` escape hatch. */
187
- private escapeHatchConfig;
188
- /** True when an output, a two-way binding or Angular forms consumes `onSave`. */
189
- private hasSaveConsumer;
190
- /**
191
- * The live callbacks this component currently wires into core, keyed by
192
- * handler name — `undefined` for the ones nothing consumes.
193
- *
194
- * PRESENCE is the semantics in core (an `onSubmit` turns Enter into
195
- * serialize-and-submit, an `onSave` arms the change-observation pipeline), so
196
- * an unconsumed callback must stay absent. One source of truth for both the
197
- * construction config and the runtime sync in `ngDoCheck`, so the two cannot
198
- * drift.
199
- * @returns the wrappers to install, `undefined` where nothing is wired
200
- */
201
- private liveHandlers;
202
- /** Handler presence installed on the live editor (the sync dedupe baseline). */
203
- private appliedHandlers;
204
- /** Live Blok instance, or null until `isReady` resolves / after destroy. */
205
- readonly instance: i0.Signal<Blok | null>;
206
- /** Guards `ready` to one emission per editor instance. */
207
- private lastReadyInstance;
208
- /**
209
- * Construction config handed to the directive. Seeds the editor's initial
210
- * values; the directive only reads it once (at construction), so post-mount
211
- * changes to construction-only inputs are no-ops by design.
212
- */
213
- buildConfig(): Partial<BlokAngularConfig>;
214
- ngAfterViewInit(): void;
215
- /**
216
- * Reactive callback presence.
217
- *
218
- * Callback wiring was decided once, at construction, and in core the presence
219
- * of a handler IS the semantics: an `onSubmit` makes Enter serialize-and-submit
220
- * instead of splitting the block, an `onSave` arms the whole change-observation
221
- * pipeline. A `[config]` swap, an `*ngIf`-gated `(save)` output or a
222
- * `registerOnChange` from Angular forms arriving after mount therefore needed a
223
- * `recreateKey` bump — destroying the editor and losing caret and undo history.
224
- *
225
- * Diff the wired handlers against what is installed and push genuine flips
226
- * through the runtime `handlers.set` API, writing `undefined` for a handler
227
- * that lost its consumer so the change stays reversible. `ngDoCheck` (not an
228
- * `effect`) because `EventEmitter.observed` and the plain `@Input` transforms
229
- * are not signals; the comparison is six identity checks against stable
230
- * wrappers, so an unchanged cycle pushes nothing.
231
- */
232
- ngDoCheck(): void;
233
- /** Serialize the current content. Resolves undefined until the editor is ready. */
234
- save$(): Promise<OutputData> | undefined;
235
- /** Move the caret into the editor. */
236
- focus(atEnd?: boolean): void;
237
- /**
238
- * Replace the editor content. Resolves undefined until the editor is ready.
239
- *
240
- * Safe to mix with a controlled `[data]` input: the rendered document becomes
241
- * the content baseline once the call lands, so a later `[data]` change back to
242
- * the previous document still re-renders instead of being deduped against a
243
- * baseline the editor no longer reflects. Recorded only on the resolved path —
244
- * a failed render leaves the editor on its previous content.
245
- * @param data - the document to render
246
- */
247
- render(data: OutputData | LooseOutputData): Promise<void> | undefined;
248
- constructor();
249
- /** Seeds/renders an externally-set form value through the same dedup machinery. */
250
- writeValue(value: OutputData | LooseOutputData | null): void;
251
- registerOnChange(fn: (data: OutputData) => void): void;
252
- registerOnTouched(fn: () => void): void;
253
- /** Form disabled state maps to the editor's read-only mode (via the readOnly effect). */
254
- setDisabledState(isDisabled: boolean): void;
255
- static ɵfac: i0.ɵɵFactoryDeclaration<BlokEditorComponent, never>;
256
- static ɵcmp: i0.ɵɵComponentDeclaration<BlokEditorComponent, "blok-editor", ["blok"], { "onBeforeRender": { "alias": "onBeforeRender"; "required": false; }; "onBeforePaste": { "alias": "onBeforePaste"; "required": false; }; "onError": { "alias": "onError"; "required": false; }; "readOnly": { "alias": "readOnly"; "required": false; }; "hideToolbar": { "alias": "hideToolbar"; "required": false; }; "toolbarPosition": { "alias": "toolbarPosition"; "required": false; }; "inlineToolbar": { "alias": "inlineToolbar"; "required": false; }; "theme": { "alias": "theme"; "required": false; }; "width": { "alias": "width"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "styleTokens": { "alias": "styleTokens"; "required": false; }; "i18n": { "alias": "i18n"; "required": false; }; "autofocus": { "alias": "autofocus"; "required": false; }; "data": { "alias": "data"; "required": false; }; "tools": { "alias": "tools"; "required": false; }; "migrations": { "alias": "migrations"; "required": false; }; "config": { "alias": "config"; "required": false; }; "recreateKey": { "alias": "recreateKey"; "required": false; }; }, { "ready": "ready"; "dataChange": "dataChange"; "save": "save"; "change": "change"; "afterRender": "afterRender"; "themeChange": "themeChange"; "blocksRendered": "blocksRendered"; "blockRendered": "blockRendered"; }, never, never, true, never>;
36
+ export declare class BlokEditorComponent implements AfterViewInit, DoCheck, OnDestroy, ControlValueAccessor {
37
+ private readonly ngZone;
38
+ /** App-wide defaults from `provideBlok()`; merged UNDER per-instance inputs. */
39
+ private readonly defaults;
40
+ private contentQuery?;
41
+ /** Signal bridge for the @ViewChild directive so effects react to its arrival. */
42
+ private readonly content;
43
+ /**
44
+ * Output half of two-way `[(data)]`. Emits the editor's serialized content on
45
+ * every change. Wiring the core `onSave` callback is gated on this being
46
+ * observed, since its mere presence makes the core serialize on every batch.
47
+ */
48
+ /** Emits the live Blok instance once ready, after `instance()` is populated. */
49
+ readonly ready: EventEmitter<Blok>;
50
+ readonly dataChange: EventEmitter<OutputData>;
51
+ /** Fires with the full serialized content on every change (notification half). */
52
+ readonly save: EventEmitter<OutputData>;
53
+ /** Raw block mutation channel (core `onChange`). */
54
+ readonly change: EventEmitter<{
55
+ api: API;
56
+ event: BlockMutationEvent | BlockMutationEvent[];
57
+ }>;
58
+ /** Fires after the editor finishes (re-)rendering (core `onAfterRender`). */
59
+ readonly afterRender: EventEmitter<API>;
60
+ /** Fires with the resolved theme whenever it changes (core `onThemeChange`). */
61
+ readonly themeChange: EventEmitter<ResolvedTheme>;
62
+ /** Fires after a batch render completes (core `blocks:rendered` event). */
63
+ readonly blocksRendered: EventEmitter<BlocksRenderedPayload>;
64
+ /** Fires for each block rendered into the DOM (core `block:rendered` event). */
65
+ readonly blockRendered: EventEmitter<BlockRenderedPayload>;
66
+ /**
67
+ * Transform hook applied to blocks before render (core `onBeforeRender`). Must
68
+ * return the (possibly modified) block list. An input, not an output, because
69
+ * it returns a value.
70
+ */
71
+ onBeforeRender?: (blocks: OutputBlockData[]) => OutputBlockData[];
72
+ /**
73
+ * Transform hook applied to pasted HTML (core `onBeforePaste`). Returns the
74
+ * (possibly modified) html, or null to drop the paste.
75
+ */
76
+ onBeforePaste?: (html: string) => string | null;
77
+ /**
78
+ * Error channel (core `onError`). Fires with the raised error whenever an
79
+ * editor operation — currently serialization — fails instead of only logging.
80
+ */
81
+ onError?: BlokConfig['onError'];
82
+ /**
83
+ * Failed-image channel (core `onImageFailure`). Return false to replace blok's own notice.
84
+ */
85
+ onImageFailure?: BlokConfig['onImageFailure'];
86
+ private readonly readOnly$;
87
+ set readOnly(value: BlokConfig['readOnly'] | undefined);
88
+ private readonly hideToolbar$;
89
+ set hideToolbar(value: boolean | undefined);
90
+ private readonly toolbarPosition$;
91
+ set toolbarPosition(value: BlokConfig['toolbarPosition'] | undefined);
92
+ private readonly inlineToolbar$;
93
+ set inlineToolbar(value: boolean | string[] | undefined);
94
+ private readonly theme$;
95
+ set theme(value: ThemeMode | undefined);
96
+ private readonly width$;
97
+ set width(value: EditorWidth | undefined);
98
+ private readonly placeholder$;
99
+ set placeholder(value: string | false | undefined);
100
+ /**
101
+ * Theme tokens. Construction-only config forced hosts with a live light/dark
102
+ * toggle to recreate the editor or hand-write the global stylesheet Blok
103
+ * already injects; this drives the runtime `tokens` API instead.
104
+ */
105
+ private readonly styleTokens$;
106
+ set styleTokens(value: Record<string, string> | undefined);
107
+ /**
108
+ * Locale, host message overrides and text direction. `config.i18n` was
109
+ * consumed once at boot, so a host driving a language switcher had to
110
+ * recreate the editor (losing caret, focus and undo stack) to relabel the
111
+ * UI; this drives the runtime `i18n.update` API instead. `defaultLocale` is
112
+ * not forwarded — it only affects the INITIAL locale resolution.
113
+ */
114
+ private readonly i18n$;
115
+ set i18n(value: BlokConfig['i18n'] | undefined);
116
+ private readonly autofocus$;
117
+ set autofocus(value: boolean | undefined);
118
+ private readonly data$;
119
+ set data(value: OutputData | LooseOutputData | null | undefined);
120
+ tools?: BlokConfig['tools'];
121
+ /** Host-supplied per-type block migrations applied at load (merged across layers). */
122
+ migrations?: BlokConfig['migrations'];
123
+ /**
124
+ * Escape hatch: a full config object for keys without a dedicated input
125
+ * (sanitizer, minHeight, …). Layered between provideBlok
126
+ * defaults and the discrete inputs.
127
+ */
128
+ config?: Partial<BlokAngularConfig>;
129
+ /** Changing this input's identity destroys and recreates the editor (≙ React `deps`). */
130
+ recreateKey: unknown;
131
+ /**
132
+ * Content the editor currently reflects. Set when seeded, when the data effect
133
+ * renders, when the editor emits its own output (`coreOnSave`) and when the
134
+ * imperative `render()` facade lands. A controlled `data` echo that
135
+ * content-equals this baseline is a no-op, so it won't clobber the caret.
136
+ * Renders are serialized via `renderChain`. `undefined` means "nothing recorded
137
+ * yet" — never an empty document.
138
+ */
139
+ private lastRenderedData?;
140
+ /**
141
+ * Content of the render currently queued or in flight — distinct from
142
+ * `lastRenderedData`, which names the last SUCCESSFUL one. The data effect
143
+ * dedupes against both, so an echo arriving mid-render is suppressed without
144
+ * the baseline having to move before the render resolves (a failed render
145
+ * would then strand it on content the editor never showed, and the host's
146
+ * retry of that same content would be deduped away forever).
147
+ * `undefined` means "nothing in flight".
148
+ */
149
+ private pendingData?;
150
+ /**
151
+ * Window of recently emitted `onSave` payloads. `lastRenderedData` remembers
152
+ * only the LAST one, but a host that persists on save and refetches can echo
153
+ * an EARLIER save back after a newer one already replaced the baseline (the
154
+ * user kept typing). That stale echo is still the editor's own output, and
155
+ * rendering it is a whole-document replace — it would drop every character
156
+ * typed between the two saves and hand the rewound document to the next save,
157
+ * making the loss permanent in the host's store. Every `[data]` path funnels
158
+ * through the `data$` signal (the template input AND `writeValue`), so the one
159
+ * check in the data effect covers `patchValue` / `setValue` / `reset` too.
160
+ */
161
+ private readonly emittedEcho;
162
+ /** Last token set pushed through `tokens.set`, for deep-equal deduping. */
163
+ private appliedTokens?;
164
+ /** Last i18n config pushed through `i18n.update`, for deep-equal deduping. */
165
+ private appliedI18n?;
166
+ /** Editor instance the i18n baseline above belongs to. */
167
+ private i18nAppliedFor;
168
+ /** Last value pushed through `tools.setInlineToolbar`, for content-compare deduping. */
169
+ private appliedInlineToolbar?;
170
+ private seededEditor;
171
+ private renderChain;
172
+ /**
173
+ * Editor the single collaboration warning has already been emitted for. Keyed
174
+ * by the editor, not a boolean, so the editor a `recreateKey` bump creates
175
+ * warns on its own first ignored `data` change instead of inheriting the
176
+ * previous editor's "already warned" flag and going silent.
177
+ */
178
+ private collaborationWarnedFor;
179
+ /** Registered by `ControlValueAccessor.registerOnChange` (Angular forms). */
180
+ private cvaOnChange?;
181
+ private cvaOnTouched?;
182
+ /**
183
+ * Core `onSave` wrapper. Records the editor's own serialized output as the
184
+ * rendered baseline BEFORE notifying Angular, so a controlled consumer echoing
185
+ * it straight back into `data` deep-equals the baseline and is deduped to a
186
+ * no-op (no redundant render, no caret reset). Re-enters the Angular zone since
187
+ * the editor runs outside it.
188
+ *
189
+ * Falls back to an escape-hatch `[config]` callback when nothing Angular-side
190
+ * consumes the save — the baseline is recorded either way, so the controlled
191
+ * `data` dedupe works on both paths.
192
+ */
193
+ private readonly coreOnSave;
194
+ /**
195
+ * Stable wrappers for the remaining live callbacks. Each resolves its target
196
+ * at call time (an observed output, else the `[config]` escape hatch), so a
197
+ * pushed handler never goes stale and only PRESENCE has to be synced.
198
+ */
199
+ private readonly coreOnChange;
200
+ private readonly coreOnAfterRender;
201
+ private readonly coreOnBeforeRender;
202
+ private readonly coreOnEnter;
203
+ private readonly coreOnSubmit;
204
+ /** provideBlok defaults merged under the `[config]` escape hatch. */
205
+ private escapeHatchConfig;
206
+ /** True when an output, a two-way binding or Angular forms consumes `onSave`. */
207
+ private hasSaveConsumer;
208
+ /**
209
+ * The live callbacks this component currently wires into core, keyed by
210
+ * handler name — `undefined` for the ones nothing consumes.
211
+ *
212
+ * PRESENCE is the semantics in core (an `onSubmit` turns Enter into
213
+ * serialize-and-submit, an `onSave` arms the change-observation pipeline), so
214
+ * an unconsumed callback must stay absent. One source of truth for both the
215
+ * construction config and the runtime sync in `ngDoCheck`, so the two cannot
216
+ * drift.
217
+ * @returns the wrappers to install, `undefined` where nothing is wired
218
+ */
219
+ private liveHandlers;
220
+ /** Handler presence installed on the live editor (the sync dedupe baseline). */
221
+ private appliedHandlers;
222
+ /** Live Blok instance, or null until `isReady` resolves / after destroy. */
223
+ readonly instance: import("@angular/core").Signal<Blok | null>;
224
+ /** Guards `ready` to one emission per editor instance. */
225
+ private lastReadyInstance;
226
+ /**
227
+ * Construction config handed to the directive. Seeds the editor's initial
228
+ * values; the directive only reads it once (at construction), so post-mount
229
+ * changes to construction-only inputs are no-ops by design.
230
+ */
231
+ buildConfig(): Partial<BlokAngularConfig>;
232
+ ngAfterViewInit(): void;
233
+ /** Save consumers captured at destroy, for core's final save. */
234
+ private saveHandlersAtDestroy;
235
+ ngOnDestroy(): void;
236
+ /**
237
+ * Reactive callback presence.
238
+ *
239
+ * Callback wiring was decided once, at construction, and in core the presence
240
+ * of a handler IS the semantics: an `onSubmit` makes Enter serialize-and-submit
241
+ * instead of splitting the block, an `onSave` arms the whole change-observation
242
+ * pipeline. A `[config]` swap, an `*ngIf`-gated `(save)` output or a
243
+ * `registerOnChange` from Angular forms arriving after mount therefore needed a
244
+ * `recreateKey` bump — destroying the editor and losing caret and undo history.
245
+ *
246
+ * Diff the wired handlers against what is installed and push genuine flips
247
+ * through the runtime `handlers.set` API, writing `undefined` for a handler
248
+ * that lost its consumer so the change stays reversible. `ngDoCheck` (not an
249
+ * `effect`) because `EventEmitter.observed` and the plain `@Input` transforms
250
+ * are not signals; the comparison is six identity checks against stable
251
+ * wrappers, so an unchanged cycle pushes nothing.
252
+ */
253
+ ngDoCheck(): void;
254
+ /** Serialize the current content. Resolves undefined until the editor is ready. */
255
+ save$(): Promise<OutputData> | undefined;
256
+ /** Ask before an in-app navigation (see `Blok.confirmLeave`). Resolves true until the editor is ready. */
257
+ confirmLeave(): Promise<boolean>;
258
+ /** Move the caret into the editor. */
259
+ focus(atEnd?: boolean): void;
260
+ /**
261
+ * Replace the editor content. Resolves undefined until the editor is ready.
262
+ *
263
+ * Safe to mix with a controlled `[data]` input: the rendered document becomes
264
+ * the content baseline once the call lands, so a later `[data]` change back to
265
+ * the previous document still re-renders instead of being deduped against a
266
+ * baseline the editor no longer reflects. Recorded only on the resolved path —
267
+ * a failed render leaves the editor on its previous content.
268
+ * @param data - the document to render
269
+ */
270
+ render(data: OutputData | LooseOutputData): Promise<void> | undefined;
271
+ constructor();
272
+ /** Seeds/renders an externally-set form value through the same dedup machinery. */
273
+ writeValue(value: OutputData | LooseOutputData | null): void;
274
+ registerOnChange(fn: (data: OutputData) => void): void;
275
+ registerOnTouched(fn: () => void): void;
276
+ /** Form disabled state maps to the editor's read-only mode (via the readOnly effect). */
277
+ setDisabledState(isDisabled: boolean): void;
278
+ static ɵfac: i0.ɵɵFactoryDeclaration<BlokEditorComponent, never>;
279
+ static ɵcmp: i0.ɵɵComponentDeclaration<BlokEditorComponent, "blok-editor", ["blok"], {
280
+ "onBeforeRender": {
281
+ "alias": "onBeforeRender";
282
+ "required": false;
283
+ };
284
+ "onBeforePaste": {
285
+ "alias": "onBeforePaste";
286
+ "required": false;
287
+ };
288
+ "onError": {
289
+ "alias": "onError";
290
+ "required": false;
291
+ };
292
+ "onImageFailure": {
293
+ "alias": "onImageFailure";
294
+ "required": false;
295
+ };
296
+ "readOnly": {
297
+ "alias": "readOnly";
298
+ "required": false;
299
+ };
300
+ "hideToolbar": {
301
+ "alias": "hideToolbar";
302
+ "required": false;
303
+ };
304
+ "toolbarPosition": {
305
+ "alias": "toolbarPosition";
306
+ "required": false;
307
+ };
308
+ "inlineToolbar": {
309
+ "alias": "inlineToolbar";
310
+ "required": false;
311
+ };
312
+ "theme": {
313
+ "alias": "theme";
314
+ "required": false;
315
+ };
316
+ "width": {
317
+ "alias": "width";
318
+ "required": false;
319
+ };
320
+ "placeholder": {
321
+ "alias": "placeholder";
322
+ "required": false;
323
+ };
324
+ "styleTokens": {
325
+ "alias": "styleTokens";
326
+ "required": false;
327
+ };
328
+ "i18n": {
329
+ "alias": "i18n";
330
+ "required": false;
331
+ };
332
+ "autofocus": {
333
+ "alias": "autofocus";
334
+ "required": false;
335
+ };
336
+ "data": {
337
+ "alias": "data";
338
+ "required": false;
339
+ };
340
+ "tools": {
341
+ "alias": "tools";
342
+ "required": false;
343
+ };
344
+ "migrations": {
345
+ "alias": "migrations";
346
+ "required": false;
347
+ };
348
+ "config": {
349
+ "alias": "config";
350
+ "required": false;
351
+ };
352
+ "recreateKey": {
353
+ "alias": "recreateKey";
354
+ "required": false;
355
+ };
356
+ }, {
357
+ "ready": "ready";
358
+ "dataChange": "dataChange";
359
+ "save": "save";
360
+ "change": "change";
361
+ "afterRender": "afterRender";
362
+ "themeChange": "themeChange";
363
+ "blocksRendered": "blocksRendered";
364
+ "blockRendered": "blockRendered";
365
+ }, never, never, true, never>;
257
366
  }
258
-
367
+ /**
368
+ * The `collaboration` the given editor was constructed with.
369
+ * @param editor - a Blok instance built by `BlokContentDirective`
370
+ * @returns the mount-time collaboration config, `undefined` when there was none
371
+ */
372
+ declare function getConstructedCollaboration(editor: WeakKey): BlokAngularConfig['collaboration'];
259
373
  /**
260
374
  * Escape-hatch directive and lifecycle engine for Blok (mirrors React's
261
375
  * `useBlok` + `BlokContent`). Constructs a Blok instance into its own host
@@ -274,47 +388,57 @@ declare class BlokEditorComponent implements AfterViewInit, DoCheck, ControlValu
274
388
  * adapter via JIT, which does not register signal-based members. `instance`
275
389
  * remains a plain `signal` (runtime API, JIT-safe) for reactive consumption.
276
390
  */
277
- declare class BlokContentDirective implements OnDestroy {
278
- private readonly host;
279
- private readonly ngZone;
280
- private readonly isBrowser;
281
- /**
282
- * App-wide defaults from `provideBlok()`, merged UNDER the bound `[config]` so
283
- * the escape-hatch path honors them just like `<blok-editor>` (which also
284
- * merges, idempotently). Mirrors React's `useBlok` merging context defaults.
285
- */
286
- private readonly defaults;
287
- private readonly envInjector;
288
- private readonly appRef;
289
- private readonly errorHandler;
290
- /** Construction-time Blok config (everything except `holder`, which the directive owns). */
291
- config: Partial<BlokAngularConfig>;
292
- /** Changing this input's identity (after the first build) destroys + recreates the editor. */
293
- set recreateKey(value: unknown);
294
- /** Emits the live Blok instance once it is ready, after `instance` is populated. */
295
- readonly ready: EventEmitter<Blok>;
296
- /** The live Blok instance, or null before `isReady` resolves / after destroy. */
297
- readonly instance: i0.WritableSignal<Blok | null>;
298
- /** The editor created by the most recent construction; used as the staleness key. */
299
- private current;
300
- private destroyed;
301
- private built;
302
- private currentKey;
303
- /** The portal registry for the current editor (Angular-block mounting). */
304
- private registry;
305
- constructor();
306
- /** Construct a Blok into the host element and publish it once ready. */
307
- private build;
308
- ngOnDestroy(): void;
309
- static ɵfac: i0.ɵɵFactoryDeclaration<BlokContentDirective, never>;
310
- static ɵdir: i0.ɵɵDirectiveDeclaration<BlokContentDirective, "[blokContent]", never, { "config": { "alias": "config"; "required": false; }; "recreateKey": { "alias": "recreateKey"; "required": false; }; }, { "ready": "ready"; }, never, never, true, never>;
391
+ export declare class BlokContentDirective implements OnDestroy {
392
+ private readonly host;
393
+ private readonly ngZone;
394
+ private readonly isBrowser;
395
+ /**
396
+ * App-wide defaults from `provideBlok()`, merged UNDER the bound `[config]` so
397
+ * the escape-hatch path honors them just like `<blok-editor>` (which also
398
+ * merges, idempotently). Mirrors React's `useBlok` merging context defaults.
399
+ */
400
+ private readonly defaults;
401
+ private readonly envInjector;
402
+ private readonly appRef;
403
+ private readonly errorHandler;
404
+ /** Construction-time Blok config (everything except `holder`, which the directive owns). */
405
+ config: Partial<BlokAngularConfig>;
406
+ /** Changing this input's identity (after the first build) destroys + recreates the editor. */
407
+ set recreateKey(value: unknown);
408
+ /** Emits the live Blok instance once it is ready, after `instance` is populated. */
409
+ readonly ready: EventEmitter<Blok>;
410
+ /** The live Blok instance, or null before `isReady` resolves / after destroy. */
411
+ readonly instance: import("@angular/core").WritableSignal<Blok | null>;
412
+ /** The editor created by the most recent construction; used as the staleness key. */
413
+ private current;
414
+ private destroyed;
415
+ private built;
416
+ private currentKey;
417
+ /** The portal registry for the current editor (Angular-block mounting). */
418
+ private registry;
419
+ constructor();
420
+ /** Construct a Blok into the host element and publish it once ready. */
421
+ private build;
422
+ ngOnDestroy(): void;
423
+ static ɵfac: i0.ɵɵFactoryDeclaration<BlokContentDirective, never>;
424
+ static ɵdir: i0.ɵɵDirectiveDeclaration<BlokContentDirective, "[blokContent]", never, {
425
+ "config": {
426
+ "alias": "config";
427
+ "required": false;
428
+ };
429
+ "recreateKey": {
430
+ "alias": "recreateKey";
431
+ "required": false;
432
+ };
433
+ }, {
434
+ "ready": "ready";
435
+ }, never, never, true, never>;
311
436
  }
312
-
313
437
  /**
314
438
  * DI token holding app-wide Blok defaults (shared tools registry, default theme,
315
439
  * i18n). Merged under per-instance inputs by `BlokEditorComponent`.
316
440
  */
317
- declare const BLOK_DEFAULT_CONFIG: InjectionToken<Partial<BlokAngularConfig>>;
441
+ export declare const BLOK_DEFAULT_CONFIG: InjectionToken<Partial<BlokAngularConfig>>;
318
442
  /**
319
443
  * Standalone provider registering app-wide Blok defaults.
320
444
  *
@@ -325,16 +449,15 @@ declare const BLOK_DEFAULT_CONFIG: InjectionToken<Partial<BlokAngularConfig>>;
325
449
  * });
326
450
  * ```
327
451
  */
328
- declare function provideBlok(defaults: Partial<BlokAngularConfig>): EnvironmentProviders;
329
-
452
+ export declare function provideBlok(defaults: Partial<BlokAngularConfig>): EnvironmentProviders;
330
453
  /**
331
454
  * Where to place the caret within a block. `position` selects the input
332
455
  * (`'start'`/`'end'`/`'default'`) and `offset` is the character offset within it
333
456
  * — the same shape core's `caret.setToBlock` accepts.
334
457
  */
335
458
  interface CaretTarget {
336
- position?: 'start' | 'end' | 'default';
337
- offset?: number;
459
+ position?: 'start' | 'end' | 'default';
460
+ offset?: number;
338
461
  }
339
462
  /**
340
463
  * A plain, serializable view of one block in the tree.
@@ -346,52 +469,48 @@ interface CaretTarget {
346
469
  * identity; depend on the `id` instead.
347
470
  */
348
471
  interface BlockNode {
349
- id: string;
350
- type: string;
351
- parentId: string | null;
352
- contentIds: readonly string[];
472
+ id: string;
473
+ type: string;
474
+ parentId: string | null;
475
+ contentIds: readonly string[];
353
476
  }
354
477
  /** Where to place a block among its siblings. */
355
- type InsertPosition = 'start' | 'end' | {
356
- before: string;
357
- } | {
358
- after: string;
359
- };
478
+ type InsertPosition = BlockPosition;
360
479
  interface InsertSpec {
361
- type?: string;
362
- data?: BlockToolData;
363
- parentId?: string | null;
364
- position?: InsertPosition;
365
- /**
366
- * Move the caret into the new block. Defaults to `false`: a programmatic
367
- * insert from React must not steal focus from wherever the user is typing.
368
- * Set `true` for an explicit "add a block and start editing it" flow.
369
- */
370
- focus?: boolean;
371
- /**
372
- * Replace the block at the resolved slot instead of inserting a new one — a
373
- * programmatic "turn into". Combine with a `position` that targets the block
374
- * to replace, e.g. `{ position: { before: id }, replace: true }`.
375
- */
376
- replace?: boolean;
377
- /**
378
- * Explicit id for the new block (generated when omitted). Passing a stable id
379
- * makes the insert idempotent: if a block with this id already exists the
380
- * existing node is returned and nothing is inserted ("insert if absent"),
381
- * so an effect that re-runs won't create duplicates.
382
- */
383
- id?: string;
384
- /** Block tune data to apply at creation, keyed by tune name. */
385
- tunes?: {
386
- [name: string]: BlockTuneData;
387
- };
388
- /**
389
- * Place the caret inside the newly-created block at a specific position/offset
390
- * (e.g. `{ offset: 3 }`). Implies focus. Applied ONLY when a block is actually
391
- * created — an insert-if-absent hit (existing id) does not move the caret.
392
- * Use this instead of the boolean `focus` when you need a specific offset.
393
- */
394
- caret?: CaretTarget;
480
+ type?: string;
481
+ data?: BlockToolData;
482
+ parentId?: string | null;
483
+ position?: InsertPosition;
484
+ /**
485
+ * Move the caret into the new block. Defaults to `false`: a programmatic
486
+ * insert from React must not steal focus from wherever the user is typing.
487
+ * Set `true` for an explicit "add a block and start editing it" flow.
488
+ */
489
+ focus?: boolean;
490
+ /**
491
+ * Replace the block at the resolved slot instead of inserting a new one — a
492
+ * programmatic "turn into". Combine with a `position` that targets the block
493
+ * to replace, e.g. `{ position: { before: id }, replace: true }`.
494
+ */
495
+ replace?: boolean;
496
+ /**
497
+ * Explicit id for the new block (generated when omitted). Passing a stable id
498
+ * makes the insert idempotent: if a block with this id already exists the
499
+ * existing node is returned and nothing is inserted ("insert if absent"),
500
+ * so an effect that re-runs won't create duplicates.
501
+ */
502
+ id?: string;
503
+ /** Block tune data to apply at creation, keyed by tune name. */
504
+ tunes?: {
505
+ [name: string]: BlockTuneData;
506
+ };
507
+ /**
508
+ * Place the caret inside the newly-created block at a specific position/offset
509
+ * (e.g. `{ offset: 3 }`). Implies focus. Applied ONLY when a block is actually
510
+ * created — an insert-if-absent hit (existing id) does not move the caret.
511
+ * Use this instead of the boolean `focus` when you need a specific offset.
512
+ */
513
+ caret?: CaretTarget;
395
514
  }
396
515
  /**
397
516
  * One node of a pre-built nested subtree for {@link UseBlocksApi.insertTree}.
@@ -403,270 +522,517 @@ interface InsertSpec {
403
522
  * is always their enclosing node.
404
523
  */
405
524
  interface TreeInsertSpec {
406
- type?: string;
407
- data?: BlockToolData;
408
- tunes?: {
409
- [name: string]: BlockTuneData;
410
- };
411
- /**
412
- * Explicit id for this node (generated when omitted). Unlike `insert`, this is
413
- * NOT insert-if-absent: a tree insert always creates fresh blocks. A colliding
414
- * id — one that already exists in the document, or is reused by another node
415
- * in the same spec — is REJECTED up front: nothing is inserted and `insertTree`
416
- * returns `null` (a duplicate id would corrupt every id-keyed lookup).
417
- */
418
- id?: string;
419
- /** Direct children, inserted nested under this node, in array order. */
420
- children?: TreeInsertSpec[];
421
- /** Root-only: where to place the whole subtree. Ignored on nested children. */
422
- parentId?: string | null;
423
- /** Root-only: slot among siblings of `parentId`. Ignored on nested children. */
424
- position?: InsertPosition;
525
+ type?: string;
526
+ data?: BlockToolData;
527
+ tunes?: {
528
+ [name: string]: BlockTuneData;
529
+ };
530
+ /**
531
+ * Explicit id for this node (generated when omitted). Unlike `insert`, this is
532
+ * NOT insert-if-absent: a tree insert always creates fresh blocks. A colliding
533
+ * id — one that already exists in the document, or is reused by another node
534
+ * in the same spec — is REJECTED up front: nothing is inserted and `insertTree`
535
+ * returns `null` (a duplicate id would corrupt every id-keyed lookup).
536
+ */
537
+ id?: string;
538
+ /** Direct children, inserted nested under this node, in array order. */
539
+ children?: TreeInsertSpec[];
540
+ /** Root-only: where to place the whole subtree. Ignored on nested children. */
541
+ parentId?: string | null;
542
+ /** Root-only: slot among siblings of `parentId`. Ignored on nested children. */
543
+ position?: InsertPosition;
425
544
  }
426
545
  /**
427
546
  * Where to move an existing block.
428
547
  *
429
- * `before`/`after` are POSITION targets, not parent assignments: the block is
430
- * relocated to that flat slot and — because Blok keeps the flat array as the
431
- * canonical document order — ADOPTS the parent of wherever it lands. Moving a
432
- * nested block to `{ after: someRootBlock }` therefore unnests it to root, and
433
- * moving a root block in among a container's children nests it. Use
434
- * `nest`/`unnest` when you want to change the parent without choosing a sibling
435
- * slot. `toIndex` is an absolute flat index (clamped into range).
548
+ * `before`/`after` make the block a sibling of the ref: it takes the ref's
549
+ * parent. Moving a nested block to `{ after: someRootBlock }` therefore unnests
550
+ * it to root, and moving a root block next to a container's child nests it.
551
+ * Use `nest`/`unnest` when you want to change the parent without choosing a
552
+ * sibling slot. `toIndex` is an absolute flat index (clamped into range).
436
553
  */
437
554
  type MoveTarget = {
438
- before: string;
555
+ before: string;
439
556
  } | {
440
- after: string;
557
+ after: string;
441
558
  } | {
442
- toIndex: number;
559
+ toIndex: number;
443
560
  };
444
561
  interface UseBlocksApi {
445
- getById(id: string): BlockNode | null;
446
- getChildren(parentId: string | null): BlockNode[];
447
- /**
448
- * Insert one block; returns the created node or null when rejected (unknown
449
- * tool type, dangling `parentId`, or a `replace` whose target is missing). An
450
- * explicit `id` that already exists is insert-if-absent (returns the existing
451
- * node, creates nothing). Atomic — one undo step. The returned node is
452
- * {@link BlockNode}-volatile; read it now, don't put it in a dep array.
453
- */
454
- insert(spec?: InsertSpec): BlockNode | null;
455
- /**
456
- * Insert several blocks atomically, in array order, as ONE undo step. Each
457
- * spec is a full {@link InsertSpec} (own type/data/parentId/position), routed
458
- * through the same single-`insert` path, so per-spec parent assertion and
459
- * positioning still apply. Specs that fail to insert (e.g. a dangling
460
- * parentId, or a replace whose target is missing) are dropped; the returned
461
- * array holds only the successfully created nodes. An empty input is a no-op
462
- * (returns `[]`, opens no transaction). Like `insert`, the returned nodes are
463
- * fresh-snapshot volatile — read them now, don't stash them in dep arrays.
464
- */
465
- insertMany(specs: InsertSpec[]): BlockNode[];
466
- /**
467
- * Insert a pre-built NESTED subtree in ONE atomic operation (one undo step).
468
- * Each {@link TreeInsertSpec} node becomes a block; its `children` are inserted
469
- * nested under it (recursively, in array order) so the whole hierarchy lands in
470
- * a single call — no follow-up `nest` round-trips. Delegates to core's
471
- * tree-aware `blocks.insertMany`, which composes the flat DFS pre-order array
472
- * honoring each node's `parent`/`content` links.
473
- *
474
- * Placement is root-only: the root node's `parentId`/`position` position the
475
- * whole subtree among existing blocks (default: appended at the document end);
476
- * nested children ignore those fields (their parent is their enclosing node). A
477
- * dangling root `parentId` is rejected — nothing is inserted and `null` is
478
- * returned (mirrors {@link insert}). Returns the root {@link BlockNode}, which
479
- * is fresh-snapshot volatile — read it now, don't stash it in a dep array.
480
- */
481
- insertTree(spec: TreeInsertSpec): BlockNode | null;
482
- /**
483
- * Convert a Markdown string to blocks and insert them ADDITIVELY at a
484
- * position, WITHOUT clearing the document (unlike core's `importMarkdown` /
485
- * `renderFromHTML`, which replace the whole document). This is the React
486
- * "paste markdown → blocks appear" path.
487
- *
488
- * Async: the markdown converter is lazy-loaded (kept out of the main bundle),
489
- * so this is the ONE async creator in the API — `await` the returned promise.
490
- * The whole batch is inserted as a single atomic undo step.
491
- *
492
- * `position` (default `'end'`) places the converted run among `parentId`'s
493
- * children (or root siblings when `parentId` is omitted/null), reusing the
494
- * same `start`/`end`/`before`/`after` semantics as {@link insert}.
495
- *
496
- * `parentId` (default `null` = root) nests the import: every TOP-LEVEL
497
- * converted block (one the converter left un-parented) is reparented under
498
- * `parentId`, while blocks the markdown nested internally (e.g. table-cell
499
- * children) keep their intra-import parent. A dangling `parentId` is a no-op
500
- * (returns `[]`, opens no transaction), matching {@link insert}.
501
- *
502
- * `config` (optional {@link MarkdownImportConfig}) is forwarded to the
503
- * converter so custom-tool consumers can map markdown nodes into their tools
504
- * (`toolMap`/`onUnknownNode`), toggle GFM, or add micromark/mdast extensions.
505
- *
506
- * Returns ALL created {@link BlockNode}s in document order — including any the
507
- * markdown nested internally (e.g. a table's cell children), not just the
508
- * top-level run (this differs from {@link insertTree}, which returns only the
509
- * root). Empty or whitespace-only markdown, a dangling `parentId` (checked
510
- * again after the async convert, so a parent removed mid-flight also no-ops),
511
- * and a converter failure (chunk-load or parse error, swallowed) all return
512
- * `[]` and open no transaction. The nodes are fresh-snapshot volatile — read
513
- * them now, don't stash them in dep arrays.
514
- */
515
- insertMarkdown(markdown: string, options?: {
516
- parentId?: string | null;
517
- position?: InsertPosition;
518
- config?: MarkdownImportConfig;
519
- }): Promise<BlockNode[]>;
520
- /**
521
- * Serialize the WHOLE document to a Markdown string — the outbound twin of
522
- * core's `blocks.exportMarkdown` (and the read-side counterpart of the
523
- * additive {@link insertMarkdown}). Async: the serializer is lazy-loaded, like
524
- * the importer. Markdown cannot express every block, so some structure is
525
- * dropped (table `colspan`/`rowspan`, heading columns). Returns `''` for an
526
- * empty document. Pre-ready: resolves to `''`.
527
- */
528
- exportMarkdown(): Promise<string>;
529
- move(id: string, target: MoveTarget): void;
530
- nest(id: string, parentId: string): void;
531
- unnest(id: string): void;
532
- remove(id: string): void;
533
- /**
534
- * Update a block's data and/or tunes by id. Delegates to core's async
535
- * `blocks.update`, which forms its OWN undo step — the call is NOT wrapped in
536
- * `transact` (that would close the group before the async write lands). An
537
- * unknown id is a silent no-op; a rejected update is swallowed so it can't
538
- * surface as an unhandled rejection. Reads refresh reactively once core emits
539
- * 'block changed'. Returns `void`.
540
- */
541
- update(id: string, data?: BlockToolData, tunes?: {
542
- [name: string]: BlockTuneData;
543
- }): void;
544
- /**
545
- * Convert a block to another type ("turn into") by id. Delegates to core's
546
- * async `blocks.convert`; both tools must provide a `conversionConfig` or core
547
- * rejects — that rejection (and any other) is swallowed so a non-convertible
548
- * block is a graceful no-op rather than an unhandled rejection. An unknown id
549
- * is a silent no-op. Not wrapped in `transact` (core owns its history step).
550
- * Returns `void`.
551
- */
552
- convert(id: string, newType: string, dataOverrides?: BlockToolData, options?: {
553
- caret?: CaretTarget;
554
- }): void;
555
- transact(fn: () => void): void;
556
- /**
557
- * Run `fn` as one atomic operation that is NOT captured in the undo history —
558
- * the React-surface counterpart of core's `transactWithoutCapture`. Use for
559
- * silent auto-repair/normalization that a user's CMD+Z should never step
560
- * through. Mutations inside still emit reactively. Pre-ready it just runs `fn`.
561
- */
562
- transactWithoutCapture(fn: () => void): void;
563
- /**
564
- * The current block count. Reactive (re-reads on 'block changed'). Pre-ready: 0.
565
- */
566
- getBlocksCount(): number;
567
- /**
568
- * The flat index of the block holding the caret, or -1 when none. Pre-ready: -1.
569
- */
570
- getCurrentBlockIndex(): number;
571
- /** The block at a flat index as a snapshot {@link BlockNode}, or null. */
572
- getBlockByIndex(index: number): BlockNode | null;
573
- /**
574
- * The absolute flat index of a block by id, or null when unknown. The
575
- * counterpart to {@link getBlockByIndex} — use it to target an off-caret
576
- * {@link splitBlock} (whose `insertIndex` is absolute) without the ref. Unknown
577
- * ids return null silently (no console warn). Pre-ready: null.
578
- */
579
- getBlockIndex(id: string): number | null;
580
- /**
581
- * Read a block's current `data` and `tunes` by id WITHOUT mutating anything —
582
- * the synchronous last-extracted view (the same snapshot clipboard ops use).
583
- * Makes a client-side duplicate composable from the hook alone: read a node,
584
- * then `insert({ type, data, tunes, position })`, no ref escape hatch. Unknown
585
- * id returns null. Pre-ready: null.
586
- */
587
- getBlockData(id: string): {
588
- data: BlockToolData;
589
- tunes: {
590
- [name: string]: BlockTuneData;
591
- };
592
- } | null;
593
- /**
594
- * The block whose holder contains/equals `element`, as a snapshot
595
- * {@link BlockNode}, or null. Useful for mapping a DOM event target back to a
596
- * block.
597
- */
598
- getBlockByElement(element: HTMLElement): BlockNode | null;
599
- /**
600
- * Read a tool's default empty data WITHOUT inserting anything — delegates to
601
- * core's `composeBlockData`. Async (a tool's data may be composed lazily).
602
- * Rejects (via core) for an unknown tool. Pre-ready: resolves to `{}`.
603
- */
604
- composeBlockData(toolName: string): Promise<BlockToolData>;
605
- /**
606
- * Replace the WHOLE document with blocks parsed from an HTML string —
607
- * delegates to core's `renderFromHTML`. Unlike {@link insertMarkdown} (which is
608
- * additive), this CLEARS existing content first, so it's a document-load
609
- * primitive, not an insert. Async. Pre-ready: resolves immediately (no-op).
610
- */
611
- renderFromHTML(html: string): Promise<void>;
612
- /**
613
- * Insert a flat array of already-serialized {@link OutputBlockData} (the
614
- * `save()` shape) directly, honoring each block's `parent`/`content` links —
615
- * the raw counterpart of core's `blocks.insertMany`. Use to re-insert a saved
616
- * document fragment without reshaping it into {@link TreeInsertSpec}. One
617
- * atomic undo step. Returns the created nodes; pre-ready: `[]` (no insert).
618
- */
619
- insertOutputData(blocks: OutputBlockData[], options?: {
620
- index?: number;
621
- }): BlockNode[];
622
- /**
623
- * Atomically split a block: update `currentBlockId` with `currentBlockData`
624
- * and insert a new `newBlockType` block at `insertIndex`, as ONE undo step.
625
- * Delegates to core's `splitBlock`. Returns the new node, or null pre-ready /
626
- * on an unknown id.
627
- */
628
- splitBlock(currentBlockId: string, currentBlockData: Partial<BlockToolData>, newBlockType: string, newBlockData: BlockToolData, insertIndex: number): BlockNode | null;
629
- /**
630
- * Insert a single child block under `parentId` at flat `insertIndex`, atomically
631
- * (block creation AND parent assignment in ONE undo step) — delegates to core's
632
- * `blocks.insertInsideParent`. This is the atomic nested-child creator: prefer it
633
- * over `insert()` + `nest()`, which is TWO undo steps. A dangling `parentId` is a
634
- * no-op returning `null` (mirrors {@link insert}); an unknown child tool returns
635
- * `null`. `childData` defaults to an empty paragraph. Returns the created
636
- * {@link BlockNode}, fresh-snapshot volatile — read it now, don't stash it in a
637
- * dep array. Pre-ready: `null`.
638
- */
639
- insertInsideParent(parentId: string, insertIndex: number, childData?: BlockToolData): BlockNode | null;
640
- /**
641
- * Replace the WHOLE document with blocks parsed from saved {@link OutputData}
642
- * (the `save()` shape) — delegates to core's `blocks.render`. Unlike
643
- * {@link insertOutputData}/{@link insertMarkdown} (which are ADDITIVE), this
644
- * CLEARS existing content first: a document-LOAD primitive, not an insert. The
645
- * HTML counterpart is {@link renderFromHTML}. Async. Pre-ready: resolves
646
- * immediately (no-op).
647
- */
648
- render(data: OutputData): Promise<void>;
649
- /**
650
- * Remove EVERY block from the document — delegates to core's `blocks.clear`. A
651
- * document-reset primitive (pairs with {@link render}). Async. Pre-ready:
652
- * resolves immediately (no-op).
653
- */
654
- clear(): Promise<void>;
655
- /**
656
- * Whether a Yjs sync (undo/redo) is currently in progress — the React mirror of
657
- * core's `blocks.isSyncingFromYjs`. A METHOD (not a property) so it reads the
658
- * LIVE flag at call time even though the api handle is memoized. Use it to skip
659
- * cleanup that would fight Yjs state during an undo/redo. Pre-ready: `false`.
660
- */
661
- isSyncingFromYjs(): boolean;
562
+ getById(id: string): BlockNode | null;
563
+ getChildren(parentId: string | null): BlockNode[];
564
+ /**
565
+ * Insert one block; returns the created node or null when rejected (unknown
566
+ * tool type, dangling `parentId`, or a `replace` whose target is missing). An
567
+ * explicit `id` that already exists is insert-if-absent (returns the existing
568
+ * node, creates nothing). Atomic — one undo step. The returned node is
569
+ * {@link BlockNode}-volatile; read it now, don't put it in a dep array.
570
+ */
571
+ insert(spec?: InsertSpec): BlockNode | null;
572
+ /**
573
+ * Insert several blocks atomically, in array order, as ONE undo step. Each
574
+ * spec is a full {@link InsertSpec} (own type/data/parentId/position), routed
575
+ * through the same single-`insert` path, so per-spec parent assertion and
576
+ * positioning still apply. Specs that fail to insert (e.g. a dangling
577
+ * parentId, or a replace whose target is missing) are dropped; the returned
578
+ * array holds only the successfully created nodes. An empty input is a no-op
579
+ * (returns `[]`, opens no transaction). Like `insert`, the returned nodes are
580
+ * fresh-snapshot volatile — read them now, don't stash them in dep arrays.
581
+ */
582
+ insertMany(specs: InsertSpec[]): BlockNode[];
583
+ /**
584
+ * Insert a pre-built NESTED subtree in ONE atomic operation (one undo step).
585
+ * Each {@link TreeInsertSpec} node becomes a block; its `children` are inserted
586
+ * nested under it (recursively, in array order) so the whole hierarchy lands in
587
+ * a single call — no follow-up `nest` round-trips. Delegates to core's
588
+ * tree-aware `blocks.insertMany`, which composes the flat DFS pre-order array
589
+ * honoring each node's `parent`/`content` links.
590
+ *
591
+ * Placement is root-only: the root node's `parentId`/`position` position the
592
+ * whole subtree among existing blocks (default: appended at the document end);
593
+ * nested children ignore those fields (their parent is their enclosing node). A
594
+ * dangling root `parentId` is rejected — nothing is inserted and `null` is
595
+ * returned (mirrors {@link insert}). Returns the root {@link BlockNode}, which
596
+ * is fresh-snapshot volatile — read it now, don't stash it in a dep array.
597
+ */
598
+ insertTree(spec: TreeInsertSpec): BlockNode | null;
599
+ /**
600
+ * Convert a Markdown string to blocks and insert them ADDITIVELY at a
601
+ * position, WITHOUT clearing the document (unlike core's `importMarkdown` /
602
+ * `renderFromHTML`, which replace the whole document). This is the React
603
+ * "paste markdown → blocks appear" path.
604
+ *
605
+ * Async: the markdown converter is lazy-loaded (kept out of the main bundle),
606
+ * so this is the ONE async creator in the API — `await` the returned promise.
607
+ * The whole batch is inserted as a single atomic undo step.
608
+ *
609
+ * `position` (default `'end'`) places the converted run among `parentId`'s
610
+ * children (or root siblings when `parentId` is omitted/null), reusing the
611
+ * same `start`/`end`/`before`/`after` semantics as {@link insert}.
612
+ *
613
+ * `parentId` (default `null` = root) nests the import: every TOP-LEVEL
614
+ * converted block (one the converter left un-parented) is reparented under
615
+ * `parentId`, while blocks the markdown nested internally (e.g. table-cell
616
+ * children) keep their intra-import parent. A dangling `parentId` is a no-op
617
+ * (returns `[]`, opens no transaction), matching {@link insert}.
618
+ *
619
+ * `config` (optional {@link MarkdownImportConfig}) is forwarded to the
620
+ * converter so custom-tool consumers can map markdown nodes into their tools
621
+ * (`toolMap`/`onUnknownNode`), toggle GFM, or add micromark/mdast extensions.
622
+ *
623
+ * Returns ALL created {@link BlockNode}s in document order — including any the
624
+ * markdown nested internally (e.g. a table's cell children), not just the
625
+ * top-level run (this differs from {@link insertTree}, which returns only the
626
+ * root). Empty or whitespace-only markdown, a dangling `parentId` (checked
627
+ * again after the async convert, so a parent removed mid-flight also no-ops),
628
+ * and a converter failure (chunk-load or parse error, swallowed) all return
629
+ * `[]` and open no transaction. The nodes are fresh-snapshot volatile — read
630
+ * them now, don't stash them in dep arrays.
631
+ */
632
+ insertMarkdown(markdown: string, options?: {
633
+ parentId?: string | null;
634
+ position?: InsertPosition;
635
+ config?: MarkdownImportConfig;
636
+ }): Promise<BlockNode[]>;
637
+ /**
638
+ * Serialize the WHOLE document to a Markdown string — the outbound twin of
639
+ * core's `blocks.exportMarkdown` (and the read-side counterpart of the
640
+ * additive {@link insertMarkdown}). Async: the serializer is lazy-loaded, like
641
+ * the importer. Markdown cannot express every block, so some structure is
642
+ * dropped (table `colspan`/`rowspan`, heading columns). Returns `''` for an
643
+ * empty document. Pre-ready: resolves to `''`.
644
+ */
645
+ exportMarkdown(): Promise<string>;
646
+ move(id: string, target: MoveTarget): void;
647
+ nest(id: string, parentId: string): void;
648
+ unnest(id: string): void;
649
+ remove(id: string): void;
650
+ /**
651
+ * Update a block's data and/or tunes by id. Delegates to core's async
652
+ * `blocks.update`, which forms its OWN undo step — the call is NOT wrapped in
653
+ * `transact` (that would close the group before the async write lands). An
654
+ * unknown id is a silent no-op; a rejected update is swallowed so it can't
655
+ * surface as an unhandled rejection. Reads refresh reactively once core emits
656
+ * 'block changed'. Returns `void`.
657
+ */
658
+ update(id: string, data?: BlockToolData, tunes?: {
659
+ [name: string]: BlockTuneData;
660
+ }): void;
661
+ /**
662
+ * Convert a block to another type ("turn into") by id. Delegates to core's
663
+ * async `blocks.convert`; both tools must provide a `conversionConfig` or core
664
+ * rejects — that rejection (and any other) is swallowed so a non-convertible
665
+ * block is a graceful no-op rather than an unhandled rejection. An unknown id
666
+ * is a silent no-op. Not wrapped in `transact` (core owns its history step).
667
+ * Returns `void`.
668
+ */
669
+ convert(id: string, newType: string, dataOverrides?: BlockToolData, options?: {
670
+ caret?: CaretTarget;
671
+ }): void;
672
+ transact(fn: () => void): void;
673
+ /**
674
+ * Run `fn` as one atomic operation that is NOT captured in the undo history —
675
+ * the React-surface counterpart of core's `transactWithoutCapture`. Use for
676
+ * silent auto-repair/normalization that a user's CMD+Z should never step
677
+ * through. Mutations inside still emit reactively. Pre-ready it just runs `fn`.
678
+ *
679
+ * With `options`, the operations are data worked out from the `from` keys of
680
+ * block `derivedFrom` (a finished upload, for example): they join the undo
681
+ * entry that wrote those values, as in core.
682
+ */
683
+ transactWithoutCapture(fn: () => void, options?: {
684
+ derivedFrom?: string;
685
+ from?: readonly string[];
686
+ }): void;
687
+ /**
688
+ * The current block count. Reactive (re-reads on 'block changed'). Pre-ready: 0.
689
+ */
690
+ getBlocksCount(): number;
691
+ /**
692
+ * The flat index of the block holding the caret, or -1 when none. Pre-ready: -1.
693
+ */
694
+ getCurrentBlockIndex(): number;
695
+ /** The block at a flat index as a snapshot {@link BlockNode}, or null. */
696
+ getBlockByIndex(index: number): BlockNode | null;
697
+ /**
698
+ * The absolute flat index of a block by id, or null when unknown. The
699
+ * counterpart to {@link getBlockByIndex} — use it to target an off-caret
700
+ * {@link splitBlock} (whose `insertIndex` is absolute) without the ref. Unknown
701
+ * ids return null silently (no console warn). Pre-ready: null.
702
+ */
703
+ getBlockIndex(id: string): number | null;
704
+ /**
705
+ * Read a block's current `data` and `tunes` by id WITHOUT mutating anything —
706
+ * the synchronous last-extracted view (the same snapshot clipboard ops use).
707
+ * Makes a client-side duplicate composable from the hook alone: read a node,
708
+ * then `insert({ type, data, tunes, position })`, no ref escape hatch. Unknown
709
+ * id returns null. Pre-ready: null.
710
+ */
711
+ getBlockData(id: string): {
712
+ data: BlockToolData;
713
+ tunes: {
714
+ [name: string]: BlockTuneData;
715
+ };
716
+ } | null;
717
+ /**
718
+ * The block whose holder contains/equals `element`, as a snapshot
719
+ * {@link BlockNode}, or null. Useful for mapping a DOM event target back to a
720
+ * block.
721
+ */
722
+ getBlockByElement(element: HTMLElement): BlockNode | null;
723
+ /**
724
+ * Read a tool's default empty data WITHOUT inserting anything — delegates to
725
+ * core's `composeBlockData`. Async (a tool's data may be composed lazily).
726
+ * Rejects (via core) for an unknown tool. Pre-ready: resolves to `{}`.
727
+ */
728
+ composeBlockData(toolName: string): Promise<BlockToolData>;
729
+ /**
730
+ * Replace the WHOLE document with blocks parsed from an HTML string —
731
+ * delegates to core's `renderFromHTML`. Unlike {@link insertMarkdown} (which is
732
+ * additive), this CLEARS existing content first, so it's a document-load
733
+ * primitive, not an insert. Async. Pre-ready: resolves immediately (no-op).
734
+ */
735
+ renderFromHTML(html: string): Promise<void>;
736
+ /**
737
+ * Insert a flat array of already-serialized {@link OutputBlockData} (the
738
+ * `save()` shape) directly, honoring each block's `parent`/`content` links —
739
+ * the raw counterpart of core's `blocks.insertMany`. Use to re-insert a saved
740
+ * document fragment without reshaping it into {@link TreeInsertSpec}. One
741
+ * atomic undo step. Returns the created nodes; pre-ready: `[]` (no insert).
742
+ */
743
+ insertOutputData(blocks: OutputBlockData[], options?: {
744
+ index?: number;
745
+ }): BlockNode[];
746
+ /**
747
+ * Atomically split a block: update `currentBlockId` with `currentBlockData`
748
+ * and insert a new `newBlockType` block at `insertIndex`, as ONE undo step.
749
+ * Delegates to core's `splitBlock`. Returns the new node, or null pre-ready /
750
+ * on an unknown id.
751
+ */
752
+ splitBlock(currentBlockId: string, currentBlockData: Partial<BlockToolData>, newBlockType: string, newBlockData: BlockToolData, insertIndex: number): BlockNode | null;
753
+ /**
754
+ * Insert a single child block under `parentId` at flat `insertIndex`, atomically
755
+ * (block creation AND parent assignment in ONE undo step) — delegates to core's
756
+ * `blocks.insertInsideParent`. This is the atomic nested-child creator: prefer it
757
+ * over `insert()` + `nest()`, which is TWO undo steps. A dangling `parentId` is a
758
+ * no-op returning `null` (mirrors {@link insert}); an unknown child tool returns
759
+ * `null`. `childData` defaults to an empty paragraph. Returns the created
760
+ * {@link BlockNode}, fresh-snapshot volatile — read it now, don't stash it in a
761
+ * dep array. Pre-ready: `null`.
762
+ */
763
+ insertInsideParent(parentId: string, insertIndex: number, childData?: BlockToolData): BlockNode | null;
764
+ /**
765
+ * Replace the WHOLE document with blocks parsed from saved {@link OutputData}
766
+ * (the `save()` shape) — delegates to core's `blocks.render`. Unlike
767
+ * {@link insertOutputData}/{@link insertMarkdown} (which are ADDITIVE), this
768
+ * CLEARS existing content first: a document-LOAD primitive, not an insert. The
769
+ * HTML counterpart is {@link renderFromHTML}. Async. Pre-ready: resolves
770
+ * immediately (no-op).
771
+ */
772
+ render(data: OutputData): Promise<void>;
773
+ /**
774
+ * Remove EVERY block from the document — delegates to core's `blocks.clear`. A
775
+ * document-reset primitive (pairs with {@link render}). Async. Pre-ready:
776
+ * resolves immediately (no-op).
777
+ */
778
+ clear(): Promise<void>;
779
+ /**
780
+ * Whether a Yjs sync (undo/redo) is currently in progress — the React mirror of
781
+ * core's `blocks.isSyncingFromYjs`. A METHOD (not a property) so it reads the
782
+ * LIVE flag at call time even though the api handle is memoized. Use it to skip
783
+ * cleanup that would fight Yjs state during an undo/redo. Pre-ready: `false`.
784
+ */
785
+ isSyncingFromYjs(): boolean;
786
+ }
787
+ /** The minimal slice of editor.blocks that the snapshot helpers read. */
788
+ interface BlocksReader {
789
+ getBlocksCount(): number;
790
+ getBlockByIndex(index: number): {
791
+ id: string;
792
+ name: string;
793
+ parentId: string | null;
794
+ } | undefined;
795
+ }
796
+ /**
797
+ * Enumerate the editor's blocks once into BlockNode records, in flat order,
798
+ * deriving each node's contentIds from the children that name it as parent.
799
+ */
800
+ declare const snapshotNodes: (reader: BlocksReader) => BlockNode[];
801
+ /** BlocksReader plus id→flat-index lookup. */
802
+ interface IndexReader extends BlocksReader {
803
+ getBlockIndex(id: string): number | undefined;
804
+ }
805
+ /** Map of block id -> parentId, in flat order. */
806
+ declare const parentMap: (reader: BlocksReader) => Map<string, string | null>;
807
+ /** True if `id` descends from `ancestorId` via the parentId chain. */
808
+ declare const isDescendantOf: (parentOf: Map<string, string | null>, id: string, ancestorId: string) => boolean;
809
+ /**
810
+ * The flat index of the last block in the contiguous subtree rooted at `index`.
811
+ * In flat (DFS) order a block's descendants sit immediately after it, so this
812
+ * walks forward while blocks remain descendants of the block at `index`.
813
+ * Returns `index` itself when the block has no descendants.
814
+ */
815
+ declare const subtreeEndIndex: (reader: BlocksReader, index: number) => number;
816
+ /**
817
+ * The flat index at which a new block should be inserted.
818
+ *
819
+ * `replace` flips the meaning of a before/after `position`: under replace the
820
+ * ref IS the block being overwritten (a "turn into"), not a sibling anchor, so
821
+ * it resolves to the ref's OWN flat slot regardless of which parent it lives in
822
+ * — bypassing the sibling-relative parent guard that would otherwise redirect a
823
+ * nested target to the requested parent's end and replace the wrong block.
824
+ */
825
+ declare const resolveInsertIndex: (reader: IndexReader, parentId: string | null, position: InsertPosition, replace?: boolean) => number;
826
+ /** The flat toIndex for editor.blocks.move. */
827
+ declare const resolveMoveIndex: (reader: IndexReader, target: MoveTarget) => number;
828
+ /** Adapt the live editor to the IndexReader the snapshot helpers expect. */
829
+ declare const readerFor: (editor: Blok) => IndexReader;
830
+ /**
831
+ * Stable API returned while the editor is null (before the adapter resolves).
832
+ * Every read returns empty/null and every MUTATOR is a no-op — EXCEPT
833
+ * `transact`/`transactWithoutCapture`, which still invoke their callback so a
834
+ * consumer wrapping conditional work in `transact` still runs that work even
835
+ * pre-ready.
836
+ */
837
+ declare const EMPTY_API: UseBlocksApi;
838
+ /**
839
+ * Does a `block changed` event touch the subtree rooted at `withinId`?
840
+ *
841
+ * The reactivity filter behind `useBlocks(editor, { within })` in all three
842
+ * adapters. Without it every consumer of `useBlocks` re-renders on every change
843
+ * anywhere in the document — so a container block that only renders its own
844
+ * children still re-rendered on each keystroke in an unrelated block, and a page
845
+ * of N such containers turned one keystroke into N re-renders.
846
+ *
847
+ * INDETERMINATE READS AS "TOUCHED". The walk resolves ancestry through the LIVE
848
+ * tree, and a removal commonly emits after the block is already gone, so its
849
+ * parent chain cannot be read. Answering `false` there would leave a container
850
+ * rendering a child that no longer exists; answering `true` costs one extra
851
+ * render pass. Same for a payload with no readable target.
852
+ *
853
+ * Ancestry is walked UPWARD one `getById` at a time rather than snapshotted with
854
+ * `parentMap`: this runs on every emission — i.e. on every keystroke — so it must
855
+ * cost O(depth), not O(document).
856
+ * @param editor - the live Blok instance
857
+ * @param payload - the `block changed` payload
858
+ * @param withinId - id of the block whose subtree is the scope
859
+ * @returns true when the change is inside the scope (or cannot be placed)
860
+ */
861
+ declare const changeTouchesSubtree: (editor: Blok, payload: unknown, withinId: string) => boolean;
862
+ /**
863
+ * Build the framework-agnostic, id/parentId-relative block-tree API over a LIVE
864
+ * editor. Mutators route through the editor-level `blocks` API (core's
865
+ * chokepoints), so undo/redo and Yjs sync are inherited rather than
866
+ * re-implemented; readers enumerate the live tree on every call.
867
+ *
868
+ * This is the shared engine behind both the React (`useSyncExternalStore`) and
869
+ * Vue (`shallowRef` version) `useBlocks` wrappers — one implementation so the
870
+ * two adapters cannot drift. Each adapter supplies the `onRead` seam:
871
+ *
872
+ * - React leaves it the default no-op: `useSyncExternalStore` re-renders the
873
+ * whole component on `block changed`, and the reads run live during that
874
+ * render, so no per-call dependency tracking is needed.
875
+ * - Vue passes `() => { void version.value }` so a read inside a `computed` /
876
+ * template touches the reactive version ref and re-runs on every structural
877
+ * mutation.
878
+ *
879
+ * `onRead` is invoked at the top of every read method; mutators never call it.
880
+ * The editor MUST be the raw (non-proxied) instance — adapters `toRaw`-unwrap
881
+ * before calling, so a Vue reactive proxy never reaches core (Risk R0).
882
+ *
883
+ * @param editor - the live Blok instance (never null; callers gate on EMPTY_API)
884
+ * @param onRead - reactivity seam called at the start of each read (default no-op)
885
+ */
886
+ declare const createBlocksApiForEditor: (editor: Blok, onRead?: () => void) => UseBlocksApi;
887
+ /**
888
+ * Normalized shape of the `readOnly` config option
889
+ */
890
+ interface NormalizedReadOnlyConfig {
891
+ enabled: boolean;
892
+ hideControls: boolean;
662
893
  }
663
-
894
+ /**
895
+ * Single source of truth for interpreting the `readOnly` config option.
896
+ * The object form always means read-only is enabled.
897
+ * @param value - raw `readOnly` value from BlokConfig
898
+ */
899
+ declare function normalizeReadOnlyConfig(value: boolean | ReadOnlyModeConfig | undefined): NormalizedReadOnlyConfig;
900
+ /**
901
+ * Structural deep equality for JSON-like values (Blok `OutputData`).
902
+ *
903
+ * Framework-agnostic: shared by the React and Angular adapters to dedupe
904
+ * reactive `data` updates so identical content does not trigger a redundant
905
+ * re-render that would clobber the caret/selection. Handles the value shapes
906
+ * that appear in editor data: plain objects, arrays, and primitives (including
907
+ * `null`/`undefined`).
908
+ * @param a - first value to compare
909
+ * @param b - second value to compare
910
+ * @returns true when both values are structurally equal
911
+ */
912
+ declare function deepEqual(a: unknown, b: unknown): boolean;
913
+ type AnyOutputData = OutputData | LooseOutputData | null | undefined;
914
+ /**
915
+ * Options for {@link equalsOutputData}.
916
+ */
917
+ interface EqualsOutputDataOptions {
918
+ /**
919
+ * Ignore empty default blocks on both sides before comparing. A fresh editor
920
+ * always seeds one empty default paragraph, so a pristine document
921
+ * (`[{ type: 'paragraph', data: { text: '' } }]`) does not content-equal a
922
+ * saved-empty baseline (`[]`) by default. Turn this on for dirty-vs-baseline
923
+ * checks ("has the user actually typed anything?"): empty blocks of the
924
+ * default paragraph tool are dropped from both documents first, so pristine
925
+ * scaffolding and trailing empty lines don't register as a change. Empty
926
+ * NON-default blocks (a content-less divider, an empty image) are kept — they
927
+ * are meaningful content.
928
+ */
929
+ ignoreEmptyDefaultBlocks?: boolean;
930
+ }
931
+ /**
932
+ * Structural equality for saved documents. Compares the `blocks` arrays
933
+ * deeply; the volatile `time` and `version` envelope fields are ignored, so a
934
+ * document round-tripped through `save()` compares equal to its echo. Block
935
+ * ids are compared only when both sides carry one — the editor mints fresh
936
+ * ids for id-less content, so a legacy document still equals its saved echo.
937
+ * Edit metadata (`lastEditedAt`/`lastEditedBy`) is ignored for the same reason:
938
+ * it records who touched a block and when, not what it says. Nullish documents
939
+ * compare equal to `{ blocks: [] }`.
940
+ *
941
+ * Pass `{ ignoreEmptyDefaultBlocks: true }` for dirty-vs-baseline checks so a
942
+ * pristine editor (one empty default paragraph) equals a saved-empty baseline —
943
+ * see {@link EqualsOutputDataOptions}.
944
+ * @param a - first document to compare
945
+ * @param b - second document to compare
946
+ * @param options - comparison options
947
+ * @returns true when both documents hold structurally equal blocks
948
+ */
949
+ declare function equalsOutputData(a: AnyOutputData, b: AnyOutputData, options?: EqualsOutputDataOptions): boolean;
950
+ /**
951
+ * A bounded window of the editor's recently emitted `onSave` payloads, used by
952
+ * the framework adapters to recognize controlled-`data` echoes. Deduping
953
+ * against only the LAST emitted payload is not enough: a host that persists on
954
+ * save and refetches can hand the adapter a STALE echo — an earlier save
955
+ * arriving after a newer one already replaced the baseline — and re-rendering
956
+ * it would clobber the caret and any content typed since. Matching is
957
+ * structural ({@link equalsOutputData}), so envelopes reshaped in transit
958
+ * (fresh `time`, stripped ids) still count as echoes.
959
+ * @param capacity - number of payloads retained before the oldest is evicted
960
+ * @returns the echo window
961
+ */
962
+ declare function createEmittedEchoWindow(capacity?: number): {
963
+ /** Records a payload the editor emitted via `onSave`. */
964
+ record(data: OutputData | LooseOutputData): void;
965
+ /** True when the document content-equals any recorded payload. */
966
+ matches(data: AnyOutputData): boolean;
967
+ /** Forgets all recorded payloads (external content took over). */
968
+ clear(): void;
969
+ };
970
+ /**
971
+ * A shared, deeply frozen empty document. Consumers persisting editor content
972
+ * repeatedly hand-write `{ blocks: [] }` for cleared/pristine baselines; this
973
+ * is the canonical value to use instead. Frozen (blocks array included) so a
974
+ * shared reference can never be mutated into a stale non-empty baseline.
975
+ */
976
+ declare const EMPTY_OUTPUT_DATA: OutputData;
977
+ /**
978
+ * Maps a controlled `data` value to something the editor's `render()` accepts.
979
+ * A whole-document `null` — a controlled "clear to empty" — becomes
980
+ * `{ blocks: [] }`; any real document passes through untouched.
981
+ *
982
+ * The framework adapters (React/Vue/Angular) route their reactive `data` path
983
+ * through this so a `null` never reaches `render()`, whose strict guard reads
984
+ * `data.blocks` and would throw on `null`. `undefined` (uncontrolled) is handled
985
+ * by the adapters before this point, so it is intentionally not an input here.
986
+ * @param data - a controlled document, or `null` for an empty document
987
+ * @returns a document `render()` can consume
988
+ */
989
+ declare function toRenderableData(data: OutputData | LooseOutputData | null): OutputData | LooseOutputData;
990
+ /**
991
+ * Normalizes blocks from the loose wire shape into the strict saved shape at
992
+ * the editor's input boundaries: a `null`/missing `data` becomes `{}`, a
993
+ * `null`/empty `id` is dropped so the block factory generates a fresh one, and
994
+ * nullish/empty hierarchy references (`parent`, `content`) are dropped so a
995
+ * root-level, childless block is spelled the one way the editor and `save()`
996
+ * both spell it — absent. Idempotent — strict blocks pass through unchanged
997
+ * (shallow-copied).
998
+ * @param blocks - blocks in either the strict or the loose wire shape
999
+ * @returns blocks in the strict saved shape
1000
+ */
1001
+ declare function normalizeOutputBlocks(blocks: Array<OutputBlockData | LooseOutputBlockData>): OutputBlockData[];
1002
+ /**
1003
+ * Normalizes a whole loose wire document into the strict saved
1004
+ * {@link OutputData} shape at an input boundary. A nullish document becomes
1005
+ * `{ blocks: [] }`; `null` envelope fields (`time`/`version`) are dropped and a
1006
+ * non-empty `id` is kept; each
1007
+ * block is passed through {@link normalizeOutputBlocks}, so `null`/missing
1008
+ * `data` becomes `{}`, `null`/empty ids are dropped for regeneration and
1009
+ * nullish/empty `parent`/`content` references are dropped as absent.
1010
+ *
1011
+ * Unlike a hand-written `blocks.map(...)` mapper, this preserves every block
1012
+ * passthrough field — `tunes`, real `parent`/`content` references, `indent`,
1013
+ * edit metadata — so a backend DTO can be turned into strict `OutputData`
1014
+ * without silently losing hierarchy or tunes. Idempotent: a strict document
1015
+ * passes through unchanged (shallow-copied).
1016
+ * @param data - a document in the strict or loose wire shape, or nullish
1017
+ * @returns the document in the strict saved shape
1018
+ */
1019
+ declare function normalizeOutputData(data: AnyOutputData): OutputData;
1020
+ /**
1021
+ * True when the document carries no user content: it is nullish, has no
1022
+ * blocks, or every block's data holds only empty values (see
1023
+ * {@link isEmptyValue}). Note that content-less visual blocks (e.g. a divider,
1024
+ * whose data is `{}`) count as empty — check `blocks.length` when mere block
1025
+ * presence matters.
1026
+ * @param data - document to inspect
1027
+ * @returns true when the document carries no user content
1028
+ */
1029
+ declare function isEmptyOutputData(data: AnyOutputData): boolean;
664
1030
  /** One field of a block's prop schema. */
665
1031
  interface PropSchemaEntry {
666
- /** Default value, used when the incoming data omits this key. */
667
- default: unknown;
668
- /** Optional allowed values (advisory; not enforced at runtime in v1). */
669
- values?: readonly unknown[];
1032
+ /** Default value, used when the incoming data omits this key. */
1033
+ default: unknown;
1034
+ /** Optional allowed values (advisory; not enforced at runtime in v1). */
1035
+ values?: readonly unknown[];
670
1036
  }
671
1037
  /**
672
1038
  * Declarative data shape. The keys here are EXACTLY the keys `save()` returns to
@@ -674,7 +1040,504 @@ interface PropSchemaEntry {
674
1040
  * written as its explicit default, never dropped).
675
1041
  */
676
1042
  type PropSchema = Record<string, PropSchemaEntry>;
677
-
1043
+ /**
1044
+ * Fill a data object against the schema: every schema key present, incoming
1045
+ * value when defined else the schema default, and ONLY schema keys (so `save()`
1046
+ * is never partial). Returns a frozen plain object — safe to hand straight to
1047
+ * core's per-key Yjs sync.
1048
+ */
1049
+ declare const fillDefaults: <Data>(schema: PropSchema, data: Record<string, unknown>) => Readonly<Data>;
1050
+ /**
1051
+ * Every child slot carries one of these — first-party containers and every
1052
+ * adapter's `<BlockChildren>` alike. `BlockHierarchy` reads the same selector,
1053
+ * so the two sides agree on which blocks own a slot.
1054
+ */
1055
+ declare const CHILD_SLOT_SELECTOR = "[data-blok-toggle-children], [data-blok-nested-blocks]";
1056
+ /**
1057
+ * Parents that place their children themselves (per cell / per view). Their
1058
+ * descendants must never be pulled into an enclosing slot.
1059
+ */
1060
+ declare const SELF_PLACING_PARENTS: ReadonlySet<string>;
1061
+ /**
1062
+ * A slotless block renders its children as flat sibling holders right after
1063
+ * itself, in whatever slot holds the block — the same as nesting at root.
1064
+ * @param block - the block to test
1065
+ */
1066
+ declare const isSlotless: (block: {
1067
+ holder: HTMLElement;
1068
+ name?: string;
1069
+ }) => boolean;
1070
+ /**
1071
+ * The blocks a container slot hosts: its children, each followed by the
1072
+ * descendants of a slotless child (depth-first, flat order). A child that owns a
1073
+ * slot keeps its own descendants.
1074
+ * @param children - the container block's model children, in model order
1075
+ * @param getChildren - model children of a block, by id
1076
+ */
1077
+ declare const withSlotlessDescendants: <T extends {
1078
+ holder: HTMLElement;
1079
+ id?: string;
1080
+ name?: string;
1081
+ }>(children: T[], getChildren: (id: string) => T[]) => T[];
1082
+ /**
1083
+ * Mount child block holders into a container, skipping children that are
1084
+ * already in place or claimed by another nested-blocks container.
1085
+ *
1086
+ * Used by toggle, header, column, and callout tools — and by the React, Vue and
1087
+ * Angular block adapters — to reconcile child holders during the `rendered()`
1088
+ * lifecycle hook. It is the standing DOM↔model reconciler for container blocks:
1089
+ * safe to call on every render, and the only thing that heals a child holder
1090
+ * that core parked outside the container while the container's own slot had not
1091
+ * been created yet.
1092
+ * @param container - the container element child holders belong in; usually the
1093
+ * element carrying `data-blok-nested-blocks`.
1094
+ * @param children - the container block's MODEL children, in model order (e.g.
1095
+ * `api.blocks.getChildren(blockId)`).
1096
+ */
1097
+ declare const mountChildBlocks: (container: HTMLElement, children: {
1098
+ holder: HTMLElement;
1099
+ }[]) => void;
1100
+ /**
1101
+ * One child's decoration: attribute name → value. `null`/`undefined` removes the
1102
+ * attribute; a boolean or number is stringified (so `false` writes
1103
+ * `data-active="false"`, which CSS can select, rather than dropping the hook).
1104
+ */
1105
+ type ChildAttributes$1 = Record<string, string | number | boolean | null | undefined>;
1106
+ /** The minimum a child must expose for a decoration pass. */
1107
+ interface DecoratableChild {
1108
+ id: string;
1109
+ holder: HTMLElement;
1110
+ }
1111
+ /**
1112
+ * A per-child decorator: what to write for the child at `index`.
1113
+ *
1114
+ * Generic over the child so an adapter can declare its decorators in terms of the
1115
+ * full `BlockAPI` its authors receive. A non-generic `(child: DecoratableChild)`
1116
+ * parameter would REJECT those: a function that demands a full BlockAPI is not
1117
+ * assignable to one promising to accept the structural minimum.
1118
+ */
1119
+ type ChildAttributesFn$1<Child extends DecoratableChild = DecoratableChild> = (child: Child, index: number) => ChildAttributes$1;
1120
+ /** What the previous decoration pass wrote for one child, so it can be undone. */
1121
+ interface StampedChild {
1122
+ /** The element written to — a holder for one ledger, a content wrapper for the other. */
1123
+ target: HTMLElement;
1124
+ names: string[];
1125
+ }
1126
+ /** The ledgers a container keeps between passes; create one per child slot. */
1127
+ interface ChildDecorationLedger {
1128
+ holders: Map<string, StampedChild>;
1129
+ contents: Map<string, StampedChild>;
1130
+ }
1131
+ /**
1132
+ * A fresh pair of ledgers for one container's child slot.
1133
+ * @returns empty holder/content ledgers
1134
+ */
1135
+ declare const createChildDecorationLedger: () => ChildDecorationLedger;
1136
+ /**
1137
+ * Decorate a container's children: named attributes on each child's HOLDER and,
1138
+ * separately, on each child's `[data-blok-element-content]` wrapper.
1139
+ *
1140
+ * The single implementation behind the React, Vue and Angular per-child
1141
+ * decoration channels, so the three cannot drift on which element a hook lands
1142
+ * on or on when a dropped hook is cleaned up.
1143
+ *
1144
+ * BOTH levels exist because core's child-holder decoration law blesses both, and
1145
+ * a container needs both: the holder is the child's outer box (rails, indices,
1146
+ * hover states), while the content wrapper is where the child's own text box
1147
+ * begins — which is what a numbered rail or a connector line has to align to.
1148
+ * With only the holder reachable, a container had to encode core's wrapper chain
1149
+ * in its own CSS (`[data-step] > [data-blok-element-content] > …`) and broke
1150
+ * whenever that chain changed. Neither hook may introduce an element: holders
1151
+ * stay DIRECT children of the slot because core's reparenting and caret
1152
+ * sibling checks compare `holder.parentElement` by identity.
1153
+ *
1154
+ * Both are inert with respect to change tracking: core's mutation filter drops a
1155
+ * holder- or content-targeted attribute record for the CHILD block (both are
1156
+ * ancestors of the child's tool element, not descendants) and scores it
1157
+ * mutation-free for the CONTAINER (whose own `data-blok-mutation-free` host is
1158
+ * the nearest such ancestor).
1159
+ * @param ledger - the slot's ledgers from {@link createChildDecorationLedger}
1160
+ * @param children - the container's model children, in model order
1161
+ * @param decorators - the authored per-child decorators
1162
+ * @param decorators.childAttributes - written on each child's holder
1163
+ * @param decorators.childContentAttributes - written on each child's content wrapper
1164
+ */
1165
+ declare const applyChildDecoration: <Child extends DecoratableChild>(ledger: ChildDecorationLedger, children: Child[], decorators: {
1166
+ childAttributes?: ChildAttributesFn$1<Child>;
1167
+ childContentAttributes?: ChildAttributesFn$1<Child>;
1168
+ }) => void;
1169
+ /**
1170
+ * Centralized data attributes used across the Blok editor.
1171
+ * This is the single source of truth for all data-blok-* attributes.
1172
+ *
1173
+ * Exported as Blok.DATA_ATTR for users.
1174
+ *
1175
+ * Naming convention:
1176
+ * - Remove 'data-blok-' prefix and convert to camelCase
1177
+ * - Global attributes: hidden, disabled, focused
1178
+ * - Component prefixed: popover, popoverItem, toolbar, etc.
1179
+ */
1180
+ declare const DATA_ATTR: {
1181
+ /** Interface type identifier (blok, inline-toolbar, tooltip) */
1182
+ readonly interface: "data-blok-interface";
1183
+ /** Block element wrapper */
1184
+ readonly element: "data-blok-element";
1185
+ /** Block element content wrapper */
1186
+ readonly elementContent: "data-blok-element-content";
1187
+ /** Editor wrapper container */
1188
+ readonly editor: "data-blok-editor";
1189
+ /** Per-instance discriminator on the editor wrapper (a monotonic counter, as a
1190
+ * string). Two editors on one page share every other scope attribute, so this
1191
+ * is what lets a page-level stylesheet — Blok's own injected `style.fontSize`
1192
+ * sheet, or a host rule — address ONE editor. Public styling hook. */
1193
+ readonly instance: "data-blok-instance";
1194
+ /** Redactor zone */
1195
+ readonly redactor: "data-blok-redactor";
1196
+ /** Present on the editor wrapper once a `blocks.render()` batch has finished
1197
+ * inserting blocks into the DOM; removed while a re-render is in flight.
1198
+ * Acts as a stable render-readiness gate for consumers (e.g. E2E waits). */
1199
+ readonly rendered: "data-blok-rendered";
1200
+ /** Present on the editor wrapper while the loading skeleton owns it.
1201
+ * Public styling hook. */
1202
+ readonly loading: "data-blok-loading";
1203
+ /** The loading skeleton overlay (a wrapper child, never inside the redactor). */
1204
+ readonly loadingSkeleton: "data-blok-loading-skeleton";
1205
+ /** One skeleton row; the value is its kind: heading | paragraph | list. */
1206
+ readonly skeletonBar: "data-blok-skeleton-bar";
1207
+ /** Blok version number stamped on the editor wrapper (e.g. '1.10.0', 'dev').
1208
+ * Consumed by browser extensions to identify the running version. */
1209
+ readonly version: "data-blok-version";
1210
+ /** Block unique identifier */
1211
+ readonly id: "data-blok-id";
1212
+ readonly pageId: "data-blok-page-id";
1213
+ /** Block component/tool type */
1214
+ readonly component: "data-blok-component";
1215
+ /** Tool type attribute */
1216
+ readonly tool: "data-blok-tool";
1217
+ readonly blockContextMenu: "data-blok-block-context-menu";
1218
+ /** Block nesting depth (derived from the parentId chain) */
1219
+ readonly depth: "data-blok-depth";
1220
+ /** Flat list-nesting indentation level (0 = root); tool-agnostic, mirrors list depth */
1221
+ readonly indent: "data-blok-indent";
1222
+ /** Header tool's heading level (1-6). Public styling hook — keyed by level rather
1223
+ * than by tag name, so a level remapped to a custom tag via `levelOverrides[n].tag`
1224
+ * (types/tools/header.d.ts) still matches its level's typography rules. */
1225
+ readonly headingLevel: "data-blok-heading-level";
1226
+ /** Element is hidden from view */
1227
+ readonly hidden: "data-blok-hidden";
1228
+ /** Element is disabled and non-interactive */
1229
+ readonly disabled: "data-blok-disabled";
1230
+ /** Element is focused via keyboard navigation */
1231
+ readonly focused: "data-blok-focused";
1232
+ /** On a popover once a navigation key moved its cursor; gates the focus ring */
1233
+ readonly keyboardNavigated: "data-blok-keyboard-navigated";
1234
+ /** Block is selected */
1235
+ readonly selected: "data-blok-selected";
1236
+ /** On a block's spotlight target while it is being pointed out (Show in the failed-image notice) */
1237
+ readonly spotlight: "data-blok-spotlight";
1238
+ /** Marks the element inside a block that Show points at, e.g. the image's error card */
1239
+ readonly spotlightTarget: "data-blok-spotlight-target";
1240
+ /** Marks the control inside a spotlight target that receives focus, e.g. Retry */
1241
+ readonly spotlightFocus: "data-blok-spotlight-focus";
1242
+ /** Block is stretched */
1243
+ readonly stretched: "data-blok-stretched";
1244
+ /** Editor or element is empty */
1245
+ readonly empty: "data-blok-empty";
1246
+ /** Present on the editor wrapper while read-only mode is active.
1247
+ * Public styling hook — lets hosts key rules off the editing state
1248
+ * without JS. Deliberately does NOT collapse the gutter: plain
1249
+ * read-only still shows the block-hover copy-link control there, and
1250
+ * in-place readOnly.set() flips must not shift the layout. */
1251
+ readonly readonly: "data-blok-readonly";
1252
+ /** Present on the editor wrapper while read-only mode hides ALL editor
1253
+ * controls (readOnly: { hideControls: true }). Public styling hook —
1254
+ * drives the gutter auto-collapse for genuinely chromeless read-only. */
1255
+ readonly controlsHidden: "data-blok-controls-hidden";
1256
+ /** Present on the editor wrapper when config.hideToolbar is true.
1257
+ * Public styling hook — drives the gutter auto-collapse (the gutter
1258
+ * exists solely to house the toolbar's +/⠿ controls). */
1259
+ readonly toolbarHidden: "data-blok-toolbar-hidden";
1260
+ /** Which gutter the floating block controls occupy: 'left' (default,
1261
+ * inline-start) or 'right' (inline-end). Written on the editor wrapper from
1262
+ * config.toolbarPosition and kept in sync by `toolbar.setPosition()`.
1263
+ * Public styling hook — drives the gutter swap and the actions-bar side. */
1264
+ readonly toolbarPosition: "data-blok-toolbar-position";
1265
+ /** Content alignment mode (left, center, right) */
1266
+ readonly contentAlign: "data-blok-content-align";
1267
+ /** Right-to-left mode */
1268
+ readonly rtl: "data-blok-rtl";
1269
+ /** Editor content width mode (present with value "full" for wide mode; absent = narrow) */
1270
+ readonly width: "data-blok-width";
1271
+ /** Present on the editor wrapper when config.style.nativeSelection is true.
1272
+ * Public styling hook — disables Blok's ::selection repaint (preflight.css)
1273
+ * and re-points the fake-background highlight at the UA Highlight color
1274
+ * (colors.css), so selection falls back to native/host-defined colors. */
1275
+ readonly nativeSelection: "data-blok-native-selection";
1276
+ /** Present on the editor wrapper while a cross-block TEXT selection is painted.
1277
+ * Suppresses the engine's own ::selection paint (main.css) so the
1278
+ * ::highlight() sub-ranges are the only thing drawn — Chromium and Firefox
1279
+ * paint such a range natively too and would otherwise double it up. */
1280
+ readonly crossSelection: "data-blok-cross-selection";
1281
+ /** Block is being dragged */
1282
+ readonly dragging: "data-blok-dragging";
1283
+ /** Multiple blocks being dragged */
1284
+ readonly draggingMulti: "data-blok-dragging-multi";
1285
+ /** Block is being duplicated (Alt+drag) */
1286
+ readonly duplicating: "data-blok-duplicating";
1287
+ /** Drag handle element */
1288
+ readonly dragHandle: "data-blok-drag-handle";
1289
+ /** Toolbar element */
1290
+ readonly toolbar: "data-blok-toolbar";
1291
+ /** The floating block-controls bar (plus button + drag/settings handle)
1292
+ * inside the toolbar. Public styling hook — the side it docks to is driven
1293
+ * from the wrapper's `data-blok-toolbar-position`. */
1294
+ readonly toolbarActions: "data-blok-toolbar-actions";
1295
+ /** Settings toggler button */
1296
+ readonly settingsToggler: "data-blok-settings-toggler";
1297
+ /** Toolbox is open */
1298
+ readonly toolboxOpened: "data-blok-toolbox-opened";
1299
+ /** Block settings is open */
1300
+ readonly blockSettingsOpened: "data-blok-block-settings-opened";
1301
+ /** Element is opened (generic) */
1302
+ readonly opened: "data-blok-opened";
1303
+ /** Root popover element */
1304
+ readonly popover: "data-blok-popover";
1305
+ /** Popover container wrapper */
1306
+ readonly popoverContainer: "data-blok-popover-container";
1307
+ /** Popover items list */
1308
+ readonly popoverItems: "data-blok-popover-items";
1309
+ /** Custom, engine-independent scrollbar thumb overlaid on the popover items */
1310
+ readonly popoverScrollbar: "data-blok-popover-scrollbar";
1311
+ /** Stamped on the custom scrollbar thumb while it is being dragged (keeps it revealed) */
1312
+ readonly popoverScrollbarDragging: "data-blok-dragging";
1313
+ /** Stamped on a scroll container while it is actively scrolling (reveals the auto-hidden scrollbar thumb) */
1314
+ readonly scrolling: "data-blok-scrolling";
1315
+ /** Popover overlay element */
1316
+ readonly popoverOverlay: "data-blok-popover-overlay";
1317
+ /** Popover custom content area */
1318
+ readonly popoverCustomContent: "data-blok-popover-custom-content";
1319
+ /** Popover custom class */
1320
+ readonly popoverCustomClass: "data-blok-popover-custom-class";
1321
+ /** Inline popover variant */
1322
+ readonly popoverInline: "data-blok-popover-inline";
1323
+ /** Popover is open */
1324
+ readonly popoverOpened: "data-blok-popover-opened";
1325
+ /** Popover opens upward */
1326
+ readonly popoverOpenTop: "data-blok-popover-open-top";
1327
+ /** Popover opens leftward */
1328
+ readonly popoverOpenLeft: "data-blok-popover-open-left";
1329
+ /** Nested popover indicator */
1330
+ readonly nested: "data-blok-nested";
1331
+ /** Nesting level value */
1332
+ readonly nestedLevel: "data-blok-nested-level";
1333
+ /** Group label for promoted search results from nested children */
1334
+ readonly promotedGroupLabel: "data-blok-promoted-group-label";
1335
+ /** A nested child shown in the host menu's search results. The convert-menu styles skip it, so the host keeps its own layout. */
1336
+ readonly promotedItem: "data-blok-promoted-item";
1337
+ /** Group label for top-level matches in search results */
1338
+ readonly topLevelGroupLabel: "data-blok-top-level-group-label";
1339
+ /** Header container */
1340
+ readonly popoverHeader: "data-blok-popover-header";
1341
+ /** Header text element */
1342
+ readonly popoverHeaderText: "data-blok-popover-header-text";
1343
+ /** Back button in nested popover */
1344
+ readonly popoverHeaderBackButton: "data-blok-popover-header-back-button";
1345
+ /** Item container */
1346
+ readonly popoverItem: "data-blok-popover-item";
1347
+ /** Item icon wrapper */
1348
+ readonly popoverItemIcon: "data-blok-popover-item-icon";
1349
+ /** Chevron icon for nested items */
1350
+ readonly popoverItemIconChevronRight: "data-blok-popover-item-icon-chevron-right";
1351
+ /** Item title text */
1352
+ readonly popoverItemTitle: "data-blok-popover-item-title";
1353
+ /** Item secondary title */
1354
+ readonly popoverItemSecondaryTitle: "data-blok-popover-item-secondary-title";
1355
+ /** Item is active/selected */
1356
+ readonly popoverItemActive: "data-blok-popover-item-active";
1357
+ /** Item's child menu is currently open — keeps the trigger looking selected */
1358
+ readonly popoverItemChildrenOpen: "data-blok-popover-item-children-open";
1359
+ /** Confirmation state */
1360
+ readonly popoverItemConfirmation: "data-blok-popover-item-confirmation";
1361
+ /** Disable hover styling */
1362
+ readonly popoverItemNoHover: "data-blok-popover-item-no-hover";
1363
+ /** Disable focus handling */
1364
+ readonly popoverItemNoFocus: "data-blok-popover-item-no-focus";
1365
+ /** Destructive action item (e.g. delete) */
1366
+ readonly popoverItemDestructive: "data-blok-popover-item-destructive";
1367
+ /** Separator item */
1368
+ readonly popoverItemSeparator: "data-blok-popover-item-separator";
1369
+ /** Separator line element */
1370
+ readonly popoverItemSeparatorLine: "data-blok-popover-item-separator-line";
1371
+ /** HTML-based item */
1372
+ readonly popoverItemHtml: "data-blok-popover-item-html";
1373
+ /** Item has child menu */
1374
+ readonly hasChildren: "data-blok-has-children";
1375
+ /** Item name identifier */
1376
+ readonly itemName: "data-blok-item-name";
1377
+ /** No search results shown */
1378
+ readonly nothingFoundDisplayed: "data-blok-nothing-found-displayed";
1379
+ /** Selection overlay */
1380
+ readonly overlay: "data-blok-overlay";
1381
+ /** Overlay container */
1382
+ readonly overlayContainer: "data-blok-overlay-container";
1383
+ /** Selection rectangle */
1384
+ readonly overlayRectangle: "data-blok-overlay-rectangle";
1385
+ /** Overlay is hidden */
1386
+ readonly overlayHidden: "data-blok-overlay-hidden";
1387
+ /** Fake cursor indicator */
1388
+ readonly fakeCursor: "data-blok-fake-cursor";
1389
+ /** Fake background for selection */
1390
+ readonly fakeBackground: "data-blok-fake-background";
1391
+ /** Auto-scroll zone (top/bottom) */
1392
+ readonly scrollZone: "data-blok-scroll-zone";
1393
+ /** Scroll is locked */
1394
+ readonly scrollLocked: "data-blok-scroll-locked";
1395
+ /** Hard scroll lock */
1396
+ readonly scrollLockedHard: "data-blok-scroll-locked-hard";
1397
+ /** Shadow caret element */
1398
+ readonly shadowCaret: "data-blok-shadow-caret";
1399
+ /** Placeholder text */
1400
+ readonly placeholder: "data-blok-placeholder";
1401
+ /** Active placeholder text */
1402
+ readonly placeholderActive: "data-blok-placeholder-active";
1403
+ /** The columns row rendered by the column_list tool (the flex container).
1404
+ * Public styling hook — its direct `[data-blok-element]` children are the
1405
+ * column holders, whose shrink floor reads `--blok-column-min-width` and
1406
+ * whose gutter reads `--blok-column-gutter`. */
1407
+ readonly columns: "data-blok-columns";
1408
+ /** A single column inside a columns row. */
1409
+ readonly column: "data-blok-column";
1410
+ /** Drag-to-resize separator between two adjacent columns. Present only in
1411
+ * edit mode — the separators ARE the gutter there. */
1412
+ readonly columnResizer: "data-blok-column-resizer";
1413
+ /** Present on a columns row whose gutter comes from the container's own
1414
+ * column-gap instead of from resizer elements. Set in read-only mode, where
1415
+ * no resizers are built — the discriminator between an editable row and a
1416
+ * published one. */
1417
+ readonly columnsStaticGutter: "data-blok-columns-static-gutter";
1418
+ /** Container that hosts nested block holders (table cells, toggle/callout/header children).
1419
+ * Used as a universal guard: before moving a block holder via appendChild,
1420
+ * check `holder.closest([nestedBlocks])` — if truthy, the holder is already
1421
+ * claimed by another container and must not be stolen. */
1422
+ readonly nestedBlocks: "data-blok-nested-blocks";
1423
+ /** A drop zone. Empty value: an empty container's placeholder (an empty
1424
+ * tab); while the block's own holder shows one without the `hidden` class,
1425
+ * a drop on the block nests the dragged blocks as its first children.
1426
+ * A block id (a tab pill): a drop on the zone appends the dragged blocks
1427
+ * as the LAST children of that block. */
1428
+ readonly dropInto: "data-blok-drop-into";
1429
+ /** Set by core on a named drop zone while it is the drop target. */
1430
+ readonly dropIntoActive: "data-blok-drop-into-active";
1431
+ /** A container's empty-state hint that stands in for its first child block
1432
+ * (empty tab, empty toggle body). It is not a block, so hovering or pressing
1433
+ * it shows no block toolbar; the block a click creates gets its own. */
1434
+ readonly childStandIn: "data-blok-child-stand-in";
1435
+ /** Element excluded from mutation tracking */
1436
+ readonly mutationFree: "data-blok-mutation-free";
1437
+ /** Tool UI inside a block that is not the user's content (a list marker, a
1438
+ * checkbox, a code block's language button and line numbers). `Block.isEmpty`
1439
+ * and the block's copied HTML skip it. Never put it on an element that holds
1440
+ * child blocks or the user's text: that content would be skipped too. */
1441
+ readonly chrome: "data-blok-chrome";
1442
+ /** Marks a subtree whose keyboard belongs to the Tool that rendered it, not to
1443
+ * the editor. Blok's block-level keydown/keyup handling stands down entirely
1444
+ * for events originating inside it — Escape, Tab, the arrows, "/" and the
1445
+ * Enter/Backspace/Delete structural keys all reach the element untouched.
1446
+ *
1447
+ * Blok already exempts native `<input>`/`<textarea>` from the STRUCTURAL keys
1448
+ * (Enter/Backspace/Delete and "/") because those are contenteditable-shaped
1449
+ * and would splice the document around a form field. Escape/Tab/arrows are
1450
+ * deliberately NOT exempt — they are how a user leaves a field. That default
1451
+ * is right for a one-line title input and wrong for a field with its own
1452
+ * keyboard semantics (Tab between sub-fields, Escape to cancel an edit, arrows
1453
+ * to walk a suggestion list), which is exactly what this attribute is for.
1454
+ * Public authoring hook. */
1455
+ readonly keyboardOwner: "data-blok-keyboard-owner";
1456
+ /** Marks a subtree whose links belong to the tool: Blok's link hover card
1457
+ * and link-click navigation stand down inside it. For a link the tool opens
1458
+ * and previews itself (the page block). Public authoring hook. */
1459
+ readonly linkOwner: "data-blok-link-owner";
1460
+ /** Block has navigation focus */
1461
+ readonly navigationFocused: "data-blok-navigation-focused";
1462
+ /** Flipper navigation target */
1463
+ readonly flipperNavigationTarget: "data-blok-flipper-navigation-target";
1464
+ /** Inline toolbar enabled on external element */
1465
+ readonly inlineToolbar: "data-blok-inline-toolbar";
1466
+ /** Link tool is active */
1467
+ readonly linkToolActive: "data-blok-link-tool-active";
1468
+ /** Link tool unlink mode */
1469
+ readonly linkToolUnlink: "data-blok-link-tool-unlink";
1470
+ /** Link tool input is opened */
1471
+ readonly linkToolInputOpened: "data-blok-link-tool-input-opened";
1472
+ /** Bold collapsed length tracking */
1473
+ readonly boldCollapsedLength: "data-blok-bold-collapsed-length";
1474
+ /** Bold collapsed active state */
1475
+ readonly boldCollapsedActive: "data-blok-bold-collapsed-active";
1476
+ /** Bold previous length tracking */
1477
+ readonly boldPrevLength: "data-blok-bold-prev-length";
1478
+ /** Bold leading whitespace */
1479
+ readonly boldLeadingWs: "data-blok-bold-leading-ws";
1480
+ /** Bold marker */
1481
+ readonly boldMarker: "data-blok-bold-marker";
1482
+ /** Tooltip is shown */
1483
+ readonly shown: "data-blok-shown";
1484
+ /** Tooltip placement */
1485
+ readonly placement: "data-blok-placement";
1486
+ /** Bounce in animation */
1487
+ readonly bounceIn: "data-blok-bounce-in";
1488
+ /** Live region announcer */
1489
+ readonly announcer: "data-blok-announcer";
1490
+ /** Stub block element */
1491
+ readonly stub: "data-blok-stub";
1492
+ /** Stub info section */
1493
+ readonly stubInfo: "data-blok-stub-info";
1494
+ /** Stub title */
1495
+ readonly stubTitle: "data-blok-stub-title";
1496
+ /** Stub subtitle */
1497
+ readonly stubSubtitle: "data-blok-stub-subtitle";
1498
+ /** Slash search active on content editable */
1499
+ readonly slashSearch: "data-blok-slash-search";
1500
+ /** Test identifier (for E2E tests) */
1501
+ readonly testid: "data-blok-testid";
1502
+ /** Force hover state (for tests/storybook) */
1503
+ readonly forceHover: "data-blok-force-hover";
1504
+ };
1505
+ /**
1506
+ * Type for DATA_ATTR keys
1507
+ */
1508
+ type DataAttrKey = keyof typeof DATA_ATTR;
1509
+ /**
1510
+ * Type for DATA_ATTR values
1511
+ */
1512
+ type DataAttrValue = (typeof DATA_ATTR)[DataAttrKey];
1513
+ /**
1514
+ * Helper function to create a CSS selector from an attribute
1515
+ *
1516
+ * @param attr - The data attribute name from DATA_ATTR
1517
+ * @param value - Optional value for the attribute (defaults to presence selector)
1518
+ * @returns CSS selector string
1519
+ *
1520
+ * @example
1521
+ * createSelector(DATA_ATTR.element) // '[data-blok-element]'
1522
+ * createSelector(DATA_ATTR.selected, true) // '[data-blok-selected="true"]'
1523
+ * createSelector(DATA_ATTR.tool, 'paragraph') // '[data-blok-tool="paragraph"]'
1524
+ */
1525
+ declare const createSelector: (attr: DataAttrValue, value?: string | boolean) => string;
1526
+ /**
1527
+ * Fired when a CONTAINER block's child holders have been mounted into its
1528
+ * child slot — the moment the child DOM has settled and a caret set into a
1529
+ * freshly inserted child sticks.
1530
+ *
1531
+ * Emitted by the React, Vue and Angular block adapters, whose portals commit a
1532
+ * frame AFTER core's `rendered()` hook; a vanilla container tool mounts its
1533
+ * children synchronously inside `rendered()`, so for those `block:rendered`
1534
+ * already means "settled". It fires on every reconciliation pass of the slot,
1535
+ * not only the first, so treat it as a settle signal rather than a change
1536
+ * signal.
1537
+ *
1538
+ * Public event name: `block:childrenMounted`.
1539
+ */
1540
+ declare const BlockChildrenMounted = "block:childrenMounted";
678
1541
  /**
679
1542
  * Every STATIC member of core's block-tool contract an Angular block may declare
680
1543
  * for itself — `ownsChildren`, `keepsChildrenOnEnter`, `conversionConfig`,
@@ -694,117 +1557,126 @@ type BlockToolStatics = Omit<BlockToolConstructable, 'toolbox' | 'isReadOnlySupp
694
1557
  * off its own `BlockAPI`.
695
1558
  */
696
1559
  interface AngularBlockMountedContext {
697
- /**
698
- * Why this block instance was constructed: a CREATION origin (`user`, `api`,
699
- * `convert`) means the author just made it, so seeding default children is
700
- * correct; a RESTORE origin (`load`, `replay`, `paste`) means the document
701
- * already says what the children are. `probe` is an off-tree instance built
702
- * only to read a tool's default data — it must not touch the block tree at
703
- * all. Defaults to `'api'` when the constructor was handed no origin.
704
- */
705
- origin: BlockOrigin;
706
- /** The editor-level API (`api.blocks`, `api.caret`, `api.events`…). */
707
- api: API;
1560
+ /**
1561
+ * Why this block instance was constructed: a CREATION origin (`user`, `api`,
1562
+ * `convert`) means the author just made it, so seeding default children is
1563
+ * correct; a RESTORE origin (`load`, `replay`, `paste`) means the document
1564
+ * already says what the children are. `probe` is an off-tree instance built
1565
+ * only to read a tool's default data — it must not touch the block tree at
1566
+ * all. Defaults to `'api'` when the constructor was handed no origin.
1567
+ */
1568
+ origin: BlockOrigin;
1569
+ /** The editor-level API (`api.blocks`, `api.caret`, `api.events`…). */
1570
+ api: API;
708
1571
  }
709
1572
  /** Spec for {@link createAngularBlock}. Authored as a standalone component. */
710
1573
  interface CreateAngularBlockSpec<Data = BlockToolData> {
711
- /** Tool type name (registered key). */
712
- type: string;
713
- /** Optional toolbox entry. */
714
- toolbox?: ToolboxConfig;
715
- /** Declarative defaults that also define the exact `save()` key set. */
716
- propSchema: PropSchema;
717
- /**
718
- * The standalone Angular component to render for each block. It injects the
719
- * per-block context via `inject(BLOK_BLOCK_CONTEXT)`.
720
- */
721
- component: Type<unknown>;
722
- /**
723
- * Static members of core's tool contract, forwarded verbatim onto the
724
- * generated tool class — the single channel for everything core reads off the
725
- * CLASS rather than the instance (`ownsChildren`, `keepsChildrenOnEnter`,
726
- * `conversionConfig`, `pasteConfig`, `sanitize`, `shortcut`, `upgradeData`…).
727
- * Without it the only way to declare one was to subclass the generated class.
728
- *
729
- * `keepsChildrenOnEnter` is the per-tool Enter POLICY: declare it and Enter on
730
- * this container's empty LAST child creates the new line INSIDE the container
731
- * instead of escaping to the container's parent (Blok's default, which is
732
- * Notion's callout behaviour). Core cannot read that off the DOM — a callout
733
- * renders the same `data-blok-nested-blocks` slot as a column yet wants the
734
- * escape — so before it existed a layout container (a card, a `steps` block)
735
- * had to hijack the editor-global `config.onEnter` and re-derive containment.
736
- *
737
- * `toolbox` and `isReadOnlySupported` are owned by the factory and cannot be
738
- * overridden here (see {@link BlockToolStatics}).
739
- */
740
- statics?: BlockToolStatics;
741
- /**
742
- * The element the +/drag toolbar should vertically center on — core's
743
- * `getToolbarAnchorElement` hook, resolved against this block's host element
744
- * on every call (never cached, so it tracks re-renders).
745
- *
746
- * A container block whose own chrome is not editable needs it: with no anchor,
747
- * core centers the toolbar on the first `[contenteditable]` under the host,
748
- * which for a container is its FIRST CHILD BLOCK. Return `null`/`undefined`
749
- * (or omit the field) to keep core's default.
750
- * @param host - this block's mutation-free host element
751
- * @param block - this block's per-block API
752
- */
753
- getToolbarAnchorElement?: (host: HTMLElement, block: BlockAPI) => HTMLElement | null | undefined;
754
- /** Optional lifecycle callbacks mapped from Blok's block hooks. */
755
- onRendered?: (block: BlockAPI) => void;
756
- /**
757
- * Fired ONCE per block instance, once the component's DOM (and, for a
758
- * container, the child holders its `ctx.mountChildren` adopted) exists.
759
- * Angular mounts the block synchronously while core is still inside
760
- * `render()`, so this lands with `rendered()` — the first hook at which the
761
- * host is also in the document. The React and Vue adapters spell the same
762
- * contract; there it is genuinely LATER than `onRendered`, because their
763
- * portals commit a frame after core returns.
764
- *
765
- * It is also the create-vs-restore signal: `context.origin` says whether the
766
- * author just made this block (`user`/`api`/`convert`) or the document is
767
- * being re-materialised (`load`/`replay`/`paste`) — so a container can seed
768
- * its default children here exactly once, without the "children are
769
- * transiently empty during a replay" trap.
770
- * @example
771
- * ```ts
772
- * onMounted: (block, { origin, api }) => {
773
- * if (origin === 'user' && block.getChildren().length === 0) {
774
- * api.blocks.insertInsideParent(block.id);
775
- * }
776
- * }
777
- * ```
778
- */
779
- onMounted?: (block: BlockAPI, context: AngularBlockMountedContext) => void;
780
- /**
781
- * The SEEDING hook: `onMounted`, narrowed to a genuine creation. Fired ONCE
782
- * per block instance, once the component's DOM exists, and only when this
783
- * instance is the author making a new block (`origin` of `user`, `api` or
784
- * `convert`) — never for a `load`/`replay`/`paste` restore, and never for the
785
- * off-tree `probe` instance core builds to read a tool's default data.
786
- *
787
- * That predicate is why the hook exists rather than leaving every block to
788
- * read `context.origin` in `onMounted`: the intuitive `origin === 'user'` test
789
- * is wrong. It drops `api.blocks.insert('steps')` and turn-into, so a
790
- * container seeded that way comes up empty for every path except a keystroke.
791
- * Core refused to ship that axis into its own `column`/`column_list`; this
792
- * encodes the correct one once, here.
793
- *
794
- * The `context` is the same object {@link CreateAngularBlockSpec.onMounted}
795
- * receives, so a block that only seeds can read `origin` for finer decisions.
796
- * @example
797
- * ```ts
798
- * onCreated: (block, { api }) => {
799
- * if (block.getChildren().length === 0) {
800
- * api.blocks.insertInsideParent(block.id);
801
- * }
802
- * }
803
- * ```
804
- */
805
- onCreated?: (block: BlockAPI, context: AngularBlockMountedContext) => void;
806
- onMoved?: (block: BlockAPI) => void;
807
- onRemoved?: (block: BlockAPI) => void;
1574
+ /** Tool type name (registered key). */
1575
+ type: string;
1576
+ /** Optional toolbox entry. */
1577
+ toolbox?: ToolboxConfig;
1578
+ /** Declarative defaults that also define the exact `save()` key set. */
1579
+ propSchema: PropSchema;
1580
+ /**
1581
+ * The standalone Angular component to render for each block. It injects the
1582
+ * per-block context via `inject(BLOK_BLOCK_CONTEXT)`.
1583
+ */
1584
+ component: Type<unknown>;
1585
+ /**
1586
+ * Static members of core's tool contract, forwarded verbatim onto the
1587
+ * generated tool class — the single channel for everything core reads off the
1588
+ * CLASS rather than the instance (`ownsChildren`, `keepsChildrenOnEnter`,
1589
+ * `conversionConfig`, `pasteConfig`, `sanitize`, `shortcut`, `upgradeData`…).
1590
+ * Without it the only way to declare one was to subclass the generated class.
1591
+ *
1592
+ * `keepsChildrenOnEnter` is the per-tool Enter POLICY: declare it and Enter on
1593
+ * this container's empty LAST child creates the new line INSIDE the container
1594
+ * instead of escaping to the container's parent (Blok's default, which is
1595
+ * Notion's callout behaviour). Core cannot read that off the DOM — a callout
1596
+ * renders the same `data-blok-nested-blocks` slot as a column yet wants the
1597
+ * escape — so before it existed a layout container (a card, a `steps` block)
1598
+ * had to hijack the editor-global `config.onEnter` and re-derive containment.
1599
+ *
1600
+ * `toolbox` and `isReadOnlySupported` are owned by the factory and cannot be
1601
+ * overridden here (see {@link BlockToolStatics}).
1602
+ */
1603
+ statics?: BlockToolStatics;
1604
+ /**
1605
+ * The element the +/drag toolbar should vertically center on — core's
1606
+ * `getToolbarAnchorElement` hook, resolved against this block's host element
1607
+ * on every call (never cached, so it tracks re-renders).
1608
+ *
1609
+ * A container block whose own chrome is not editable needs it: with no anchor,
1610
+ * core centers the toolbar on the first `[contenteditable]` under the host,
1611
+ * which for a container is its FIRST CHILD BLOCK. Return `null`/`undefined`
1612
+ * (or omit the field) to keep core's default.
1613
+ * @param host - this block's mutation-free host element
1614
+ * @param block - this block's per-block API
1615
+ */
1616
+ getToolbarAnchorElement?: (host: HTMLElement, block: BlockAPI) => HTMLElement | null | undefined;
1617
+ /**
1618
+ * Enter (or Cmd/Ctrl+Enter) on this block while it is the keyboard
1619
+ * navigation target — core's `onNavigationEnter` hook. Return true when the
1620
+ * block handled it (e.g. opened a link); omit it, or return false, to let
1621
+ * core put the caret in the block.
1622
+ * @param event - the Enter keydown
1623
+ * @param block - this block's per-block API
1624
+ */
1625
+ onNavigationEnter?: (event: KeyboardEvent, block: BlockAPI) => boolean;
1626
+ /** Optional lifecycle callbacks mapped from Blok's block hooks. */
1627
+ onRendered?: (block: BlockAPI) => void;
1628
+ /**
1629
+ * Fired ONCE per block instance, once the component's DOM (and, for a
1630
+ * container, the child holders its `ctx.mountChildren` adopted) exists.
1631
+ * Angular mounts the block synchronously while core is still inside
1632
+ * `render()`, so this lands with `rendered()` — the first hook at which the
1633
+ * host is also in the document. The React and Vue adapters spell the same
1634
+ * contract; there it is genuinely LATER than `onRendered`, because their
1635
+ * portals commit a frame after core returns.
1636
+ *
1637
+ * It is also the create-vs-restore signal: `context.origin` says whether the
1638
+ * author just made this block (`user`/`api`/`convert`) or the document is
1639
+ * being re-materialised (`load`/`replay`/`paste`) — so a container can seed
1640
+ * its default children here exactly once, without the "children are
1641
+ * transiently empty during a replay" trap.
1642
+ * @example
1643
+ * ```ts
1644
+ * onMounted: (block, { origin, api }) => {
1645
+ * if (origin === 'user' && block.getChildren().length === 0) {
1646
+ * api.blocks.insertInsideParent(block.id);
1647
+ * }
1648
+ * }
1649
+ * ```
1650
+ */
1651
+ onMounted?: (block: BlockAPI, context: AngularBlockMountedContext) => void;
1652
+ /**
1653
+ * The SEEDING hook: `onMounted`, narrowed to a genuine creation. Fired ONCE
1654
+ * per block instance, once the component's DOM exists, and only when this
1655
+ * instance is the author making a new block (`origin` of `user`, `api` or
1656
+ * `convert`) — never for a `load`/`replay`/`paste` restore, and never for the
1657
+ * off-tree `probe` instance core builds to read a tool's default data.
1658
+ *
1659
+ * That predicate is why the hook exists rather than leaving every block to
1660
+ * read `context.origin` in `onMounted`: the intuitive `origin === 'user'` test
1661
+ * is wrong. It drops `api.blocks.insert('steps')` and turn-into, so a
1662
+ * container seeded that way comes up empty for every path except a keystroke.
1663
+ * Core refused to ship that axis into its own `column`/`column_list`; this
1664
+ * encodes the correct one once, here.
1665
+ *
1666
+ * The `context` is the same object {@link CreateAngularBlockSpec.onMounted}
1667
+ * receives, so a block that only seeds can read `origin` for finer decisions.
1668
+ * @example
1669
+ * ```ts
1670
+ * onCreated: (block, { api }) => {
1671
+ * if (block.getChildren().length === 0) {
1672
+ * api.blocks.insertInsideParent(block.id);
1673
+ * }
1674
+ * }
1675
+ * ```
1676
+ */
1677
+ onCreated?: (block: BlockAPI, context: AngularBlockMountedContext) => void;
1678
+ onMoved?: (block: BlockAPI) => void;
1679
+ onRemoved?: (block: BlockAPI) => void;
808
1680
  }
809
1681
  /**
810
1682
  * Author a first-party Angular block. Returns a `BlockToolConstructable`
@@ -822,43 +1694,43 @@ interface CreateAngularBlockSpec<Data = BlockToolData> {
822
1694
  * - `setReadOnly()` flips a reactive flag and flushes CD (in-place, no remount).
823
1695
  * - `removed()`/`destroy()` unregister the portal (deterministic unmount).
824
1696
  */
825
- declare function createAngularBlock<Data = BlockToolData>(spec: CreateAngularBlockSpec<Data>): (new (options: BlockToolConstructorOptions) => {
826
- render(): HTMLElement;
827
- save(): BlockToolData;
828
- setData(newData: BlockToolData): Promise<boolean>;
829
- setReadOnly(state: boolean): void;
830
- getToolbarAnchorElement(): HTMLElement | undefined;
831
- rendered(): void;
832
- moved(): void;
833
- removed(): void;
834
- destroy(): void;
1697
+ export declare function createAngularBlock<Data = BlockToolData>(spec: CreateAngularBlockSpec<Data>): (new (options: BlockToolConstructorOptions) => {
1698
+ render(): HTMLElement;
1699
+ save(): BlockToolData;
1700
+ setData(newData: BlockToolData): Promise<boolean>;
1701
+ setReadOnly(state: boolean): void;
1702
+ getToolbarAnchorElement(): HTMLElement | undefined;
1703
+ onNavigationEnter(event: KeyboardEvent): boolean;
1704
+ rendered(): void;
1705
+ moved(): void;
1706
+ removed(): void;
1707
+ destroy(): void;
835
1708
  }) & BlockToolStatics & {
836
- readonly __isBlokAngularBlock: true;
837
- readonly toolbox: ToolboxConfig | undefined;
838
- readonly isReadOnlySupported: boolean;
1709
+ readonly __isBlokAngularBlock: true;
1710
+ readonly toolbox: ToolboxConfig | undefined;
1711
+ readonly isReadOnlySupported: boolean;
839
1712
  };
840
-
841
1713
  /** Options for {@link injectBlocks}. */
842
1714
  interface InjectBlocksOptions {
843
- /**
844
- * Invalidate reads only for changes inside the subtree rooted at this block id
845
- * (the block itself or any descendant). Omit — or pass `null` — for the
846
- * document-wide default.
847
- *
848
- * This bounds REACTIVITY, not reads: the returned API still sees the whole
849
- * tree, so a scoped consumer can still `getById` anything. Reach for it in a
850
- * container block that renders only its own children — unscoped, such a block
851
- * invalidates on every keystroke anywhere in the document, and a page of N
852
- * containers turns one keystroke into N re-renders.
853
- *
854
- * Accepted as a plain value or a signal (like `editor`) and read at EMIT time,
855
- * so changing it takes effect immediately with no re-subscription.
856
- *
857
- * A change whose block cannot be placed in the tree (a removal that emits
858
- * after the block is gone) counts as in-scope: skipping it would leave a
859
- * container rendering a child that no longer exists.
860
- */
861
- within?: Signal<string | null> | string | null;
1715
+ /**
1716
+ * Invalidate reads only for changes inside the subtree rooted at this block id
1717
+ * (the block itself or any descendant). Omit — or pass `null` — for the
1718
+ * document-wide default.
1719
+ *
1720
+ * This bounds REACTIVITY, not reads: the returned API still sees the whole
1721
+ * tree, so a scoped consumer can still `getById` anything. Reach for it in a
1722
+ * container block that renders only its own children — unscoped, such a block
1723
+ * invalidates on every keystroke anywhere in the document, and a page of N
1724
+ * containers turns one keystroke into N re-renders.
1725
+ *
1726
+ * Accepted as a plain value or a signal (like `editor`) and read at EMIT time,
1727
+ * so changing it takes effect immediately with no re-subscription.
1728
+ *
1729
+ * A change whose block cannot be placed in the tree (a removal that emits
1730
+ * after the block is gone) counts as in-scope: skipping it would leave a
1731
+ * container rendering a child that no longer exists.
1732
+ */
1733
+ within?: Signal<string | null> | string | null;
862
1734
  }
863
1735
  /**
864
1736
  * Angular factory exposing an id/parentId-relative, reactive view of the block
@@ -896,30 +1768,29 @@ interface InjectBlocksOptions {
896
1768
  * @param options - reactivity options; see {@link InjectBlocksOptions.within} to
897
1769
  * scope invalidation to one block's subtree
898
1770
  */
899
- declare function injectBlocks(editor: Signal<Blok | null>, options?: InjectBlocksOptions): UseBlocksApi;
900
-
1771
+ export declare function injectBlocks(editor: Signal<Blok | null>, options?: InjectBlocksOptions): UseBlocksApi;
901
1772
  /** How the caller names the DOM scope to observe. */
902
1773
  type BlokReadyScope = (() => Element | ElementRef<Element> | null | undefined) | ElementRef<Element> | Element | null;
903
1774
  /** Options accepted by {@link injectBlokReady}. */
904
1775
  interface InjectBlokReadyOptions {
905
- /**
906
- * Restrict the wait to editors mounted inside this element. Accepts an
907
- * element, an `ElementRef`, or a getter/signal returning either — the getter
908
- * form lets you pass `() => this.scopeRef?.nativeElement` from a field
909
- * initializer, before the view exists. It is re-read on every readiness
910
- * change.
911
- *
912
- * Omit it to observe every editor on the page. Passing it while it still
913
- * resolves to null reports NOT ready — an unresolved scope must never fall
914
- * back to the page-global one.
915
- */
916
- within?: BlokReadyScope;
917
- /**
918
- * `'ready'` (default) settles when each editor has finished booting.
919
- * `'rendered'` also waits for its content to be in the DOM and re-arms on
920
- * every post-boot re-render.
921
- */
922
- settleOn?: 'ready' | 'rendered';
1776
+ /**
1777
+ * Restrict the wait to editors mounted inside this element. Accepts an
1778
+ * element, an `ElementRef`, or a getter/signal returning either — the getter
1779
+ * form lets you pass `() => this.scopeRef?.nativeElement` from a field
1780
+ * initializer, before the view exists. It is re-read on every readiness
1781
+ * change.
1782
+ *
1783
+ * Omit it to observe every editor on the page. Passing it while it still
1784
+ * resolves to null reports NOT ready — an unresolved scope must never fall
1785
+ * back to the page-global one.
1786
+ */
1787
+ within?: BlokReadyScope;
1788
+ /**
1789
+ * `'ready'` (default) settles when each editor has finished booting.
1790
+ * `'rendered'` also waits for its content to be in the DOM and re-arms on
1791
+ * every post-boot re-render.
1792
+ */
1793
+ settleOn?: 'ready' | 'rendered';
923
1794
  }
924
1795
  /**
925
1796
  * Angular factory exposing the live readiness of the Blok editors in a DOM
@@ -937,8 +1808,7 @@ interface InjectBlokReadyOptions {
937
1808
  * `@ViewChild` scope to exist. Over-waiting is safe; under-waiting is a bug.
938
1809
  * @param options - scope and readiness depth
939
1810
  */
940
- declare function injectBlokReady(options?: InjectBlokReadyOptions): Signal<boolean>;
941
-
1811
+ export declare function injectBlokReady(options?: InjectBlokReadyOptions): Signal<boolean>;
942
1812
  /**
943
1813
  * One child's decoration: attribute name → value. `null`/`undefined` removes the
944
1814
  * attribute; a boolean or number is stringified (so `false` writes
@@ -954,88 +1824,87 @@ type ChildAttributesFn = (child: BlockAPI, index: number) => ChildAttributes;
954
1824
  * JIT test harness. The ONLY data write path is `commit`.
955
1825
  */
956
1826
  interface AngularBlockRenderContext<Data> {
957
- /** Reactive, FROZEN snapshot of the block data. Read `data()`; never mutate. */
958
- data: Signal<Readonly<Data>>;
959
- /** The ONLY data write path: merge a partial patch and sync once. */
960
- commit: (patch: Partial<Data>) => void;
961
- /** This block's per-block API (id, getChildren, dispatchChange…). */
962
- block: BlockAPI;
963
- /**
964
- * The EDITOR-level API this block belongs to (`api.blocks`, `api.caret`,
965
- * `api.toolbar`…) — the same object a vanilla tool receives in its
966
- * constructor. Reach for it when a block has to drive the document around it
967
- * instead of routing everything through `block.call()` string dispatch.
968
- *
969
- * For the reactive, id/parentId-relative view of the tree (and to re-render
970
- * when your own children change), pair `injectBlocks` with
971
- * `injectBlokInstance()` instead — the api handle itself is not reactive.
972
- */
973
- api: API;
974
- /**
975
- * Reactive read-only flag. Read `readOnly()` in the template to disable
976
- * editing (drop `contenteditable`, hide controls). Toggled IN PLACE by core's
977
- * read-only switch — the component reacts without a remount, so ephemeral
978
- * state survives. A block that ignores it stays interactive when the editor is
979
- * read-only (same contract as a vanilla tool's `setReadOnly`).
980
- */
981
- readOnly: Signal<boolean>;
982
- /**
983
- * Container blocks only: append this block's real child holders into `host`
984
- * (a `data-blok-nested` element the author owns). Call it once in
985
- * `ngAfterViewInit`; the factory re-runs the same mount on every data change
986
- * so late-added children appear. Angular must NOT manage these child holders.
987
- *
988
- * `childAttributes` decorates each child's HOLDER after the holders are
989
- * mounted, and is REMEMBERED — every later remount re-applies it. Named hooks
990
- * (`data-step-index`, `data-active`…) replace positional `:nth-child()` CSS
991
- * over Blok's holders, which breaks the moment a child is inserted, removed or
992
- * reordered. The holders stay DIRECT children of `host` — core requires that
993
- * (hierarchy reparenting and caret navigation compare `holder.parentElement`
994
- * by identity), so decoration is attributes, never wrapper elements.
995
- * Attributes the callback stops producing are removed on the next pass.
996
- *
997
- * `childContentAttributes` is the same decoration applied one level IN — on
998
- * each child's `[data-blok-element-content]` wrapper instead of its holder —
999
- * and is remembered the same way. Core's decoration law blesses both, and a
1000
- * container needs both: the holder is the child's outer box (rails, indices,
1001
- * hover states), the content wrapper is where the child's own text box begins,
1002
- * which is what a numbered rail or a connector line has to align to. Reach for
1003
- * it instead of walking core's wrapper chain from a holder hook (`[data-step] >
1004
- * [data-blok-element-content] > …`) — those selectors encode engine DOM in host
1005
- * CSS and break when that structure changes. A child whose DOM has not
1006
- * committed yet has no wrapper to write to and is stamped on the next pass.
1007
- *
1008
- * Writing on a child's holder is inert by design: core's mutation filter drops
1009
- * a holder-targeted attribute record for the child block and suppresses it for
1010
- * the container. The guarantee stops at the holder and its
1011
- * `[data-blok-element-content]` wrapper — writing AT or BELOW a child's tool
1012
- * root DOES score as that child's edit.
1013
- */
1014
- mountChildren: (host: HTMLElement, childAttributes?: ChildAttributesFn, childContentAttributes?: ChildAttributesFn) => void;
1015
- /**
1016
- * Name the element the +/drag toolbar should vertically center on, from inside
1017
- * the component: `ctx.setToolbarAnchor(this.head().nativeElement)` in
1018
- * `ngAfterViewInit`. The Angular counterpart of React's/Vue's
1019
- * `toolbarAnchorRef` — core's `getToolbarAnchorElement` is otherwise a
1020
- * `(host, block) => Element` hook resolved OUTSIDE the component, so pointing
1021
- * at an element the template renders meant inventing a data attribute and
1022
- * `querySelector`-ing for it from
1023
- * `CreateAngularBlockSpec.getToolbarAnchorElement`.
1024
- *
1025
- * A container block whose own chrome is not editable needs an anchor: with
1026
- * none, core centers the toolbar on the first `[contenteditable]` under the
1027
- * host, which for a container is its FIRST CHILD BLOCK — parking the +/drag
1028
- * handles halfway down, beside content that has a toolbar of its own.
1029
- *
1030
- * The element set here outranks the declared hook while it is MOUNTED; once it
1031
- * detaches (or `null` is passed) the hook takes over again, so the toolbar is
1032
- * never positioned against a stale node. Never calling it keeps core's default.
1033
- */
1034
- setToolbarAnchor: (element: HTMLElement | null) => void;
1827
+ /** Reactive, FROZEN snapshot of the block data. Read `data()`; never mutate. */
1828
+ data: Signal<Readonly<Data>>;
1829
+ /** The ONLY data write path: merge a partial patch and sync once. */
1830
+ commit: (patch: Partial<Data>) => void;
1831
+ /** This block's per-block API (id, getChildren, dispatchChange…). */
1832
+ block: BlockAPI;
1833
+ /**
1834
+ * The EDITOR-level API this block belongs to (`api.blocks`, `api.caret`,
1835
+ * `api.toolbar`…) — the same object a vanilla tool receives in its
1836
+ * constructor. Reach for it when a block has to drive the document around it
1837
+ * instead of routing everything through `block.call()` string dispatch.
1838
+ *
1839
+ * For the reactive, id/parentId-relative view of the tree (and to re-render
1840
+ * when your own children change), pair `injectBlocks` with
1841
+ * `injectBlokInstance()` instead — the api handle itself is not reactive.
1842
+ */
1843
+ api: API;
1844
+ /**
1845
+ * Reactive read-only flag. Read `readOnly()` in the template to disable
1846
+ * editing (drop `contenteditable`, hide controls). Toggled IN PLACE by core's
1847
+ * read-only switch — the component reacts without a remount, so ephemeral
1848
+ * state survives. A block that ignores it stays interactive when the editor is
1849
+ * read-only (same contract as a vanilla tool's `setReadOnly`).
1850
+ */
1851
+ readOnly: Signal<boolean>;
1852
+ /**
1853
+ * Container blocks only: append this block's real child holders into `host`
1854
+ * (a `data-blok-nested` element the author owns). Call it once in
1855
+ * `ngAfterViewInit`; the factory re-runs the same mount on every data change
1856
+ * so late-added children appear. Angular must NOT manage these child holders.
1857
+ *
1858
+ * `childAttributes` decorates each child's HOLDER after the holders are
1859
+ * mounted, and is REMEMBERED — every later remount re-applies it. Named hooks
1860
+ * (`data-step-index`, `data-active`…) replace positional `:nth-child()` CSS
1861
+ * over Blok's holders, which breaks the moment a child is inserted, removed or
1862
+ * reordered. The holders stay DIRECT children of `host` — core requires that
1863
+ * (hierarchy reparenting and caret navigation compare `holder.parentElement`
1864
+ * by identity), so decoration is attributes, never wrapper elements.
1865
+ * Attributes the callback stops producing are removed on the next pass.
1866
+ *
1867
+ * `childContentAttributes` is the same decoration applied one level IN — on
1868
+ * each child's `[data-blok-element-content]` wrapper instead of its holder —
1869
+ * and is remembered the same way. Core's decoration law blesses both, and a
1870
+ * container needs both: the holder is the child's outer box (rails, indices,
1871
+ * hover states), the content wrapper is where the child's own text box begins,
1872
+ * which is what a numbered rail or a connector line has to align to. Reach for
1873
+ * it instead of walking core's wrapper chain from a holder hook (`[data-step] >
1874
+ * [data-blok-element-content] > …`) — those selectors encode engine DOM in host
1875
+ * CSS and break when that structure changes. A child whose DOM has not
1876
+ * committed yet has no wrapper to write to and is stamped on the next pass.
1877
+ *
1878
+ * Writing on a child's holder is inert by design: core's mutation filter drops
1879
+ * a holder-targeted attribute record for the child block and suppresses it for
1880
+ * the container. The guarantee stops at the holder and its
1881
+ * `[data-blok-element-content]` wrapper — writing AT or BELOW a child's tool
1882
+ * root DOES score as that child's edit.
1883
+ */
1884
+ mountChildren: (host: HTMLElement, childAttributes?: ChildAttributesFn, childContentAttributes?: ChildAttributesFn) => void;
1885
+ /**
1886
+ * Name the element the +/drag toolbar should vertically center on, from inside
1887
+ * the component: `ctx.setToolbarAnchor(this.head().nativeElement)` in
1888
+ * `ngAfterViewInit`. The Angular counterpart of React's/Vue's
1889
+ * `toolbarAnchorRef` — core's `getToolbarAnchorElement` is otherwise a
1890
+ * `(host, block) => Element` hook resolved OUTSIDE the component, so pointing
1891
+ * at an element the template renders meant inventing a data attribute and
1892
+ * `querySelector`-ing for it from
1893
+ * `CreateAngularBlockSpec.getToolbarAnchorElement`.
1894
+ *
1895
+ * A container block whose own chrome is not editable needs an anchor: with
1896
+ * none, core centers the toolbar on the first `[contenteditable]` under the
1897
+ * host, which for a container is its FIRST CHILD BLOCK — parking the +/drag
1898
+ * handles halfway down, beside content that has a toolbar of its own.
1899
+ *
1900
+ * The element set here outranks the declared hook while it is MOUNTED; once it
1901
+ * detaches (or `null` is passed) the hook takes over again, so the toolbar is
1902
+ * never positioned against a stale node. Never calling it keeps core's default.
1903
+ */
1904
+ setToolbarAnchor: (element: HTMLElement | null) => void;
1035
1905
  }
1036
1906
  /** DI token carrying the per-block render context into the authored component. */
1037
- declare const BLOK_BLOCK_CONTEXT: InjectionToken<AngularBlockRenderContext<unknown>>;
1038
-
1907
+ export declare const BLOK_BLOCK_CONTEXT: InjectionToken<AngularBlockRenderContext<unknown>>;
1039
1908
  /**
1040
1909
  * DI token carrying the LIVE editor instance (as a signal) into every block
1041
1910
  * mounted by `createAngularBlock`. `BlokContentDirective` publishes its own
@@ -1045,7 +1914,7 @@ declare const BLOK_BLOCK_CONTEXT: InjectionToken<AngularBlockRenderContext<unkno
1045
1914
  * Provided on the block's ELEMENT injector by the portal registry, so it is
1046
1915
  * per-EDITOR: two editors on one page each publish their own instance.
1047
1916
  */
1048
- declare const BLOK_EDITOR_INSTANCE: InjectionToken<Signal<Blok | null>>;
1917
+ export declare const BLOK_EDITOR_INSTANCE: InjectionToken<Signal<Blok | null>>;
1049
1918
  /**
1050
1919
  * The live Blok instance the component is mounted inside, as a signal, or a
1051
1920
  * signal of null before it exists. Inside a `createAngularBlock` component this
@@ -1065,7 +1934,5 @@ declare const BLOK_EDITOR_INSTANCE: InjectionToken<Signal<Blok | null>>;
1065
1934
  * Call it in an injection context (a field initializer or the constructor).
1066
1935
  * Outside an editor it returns a signal of null.
1067
1936
  */
1068
- declare function injectBlokInstance(): Signal<Blok | null>;
1069
-
1070
- export { BLOK_BLOCK_CONTEXT, BLOK_DEFAULT_CONFIG, BLOK_EDITOR_INSTANCE, BlokContentDirective, BlokEditorComponent, createAngularBlock, injectBlocks, injectBlokInstance, injectBlokReady, provideBlok };
1071
- export type { AngularBlockMountedContext, AngularBlockRenderContext, BlockNode, BlockToolStatics, BlokAngularConfig, BlokReadyScope, CaretTarget, ChildAttributes, ChildAttributesFn, CreateAngularBlockSpec, InjectBlocksOptions, InjectBlokReadyOptions, InsertPosition, InsertSpec, MoveTarget, PropSchema, PropSchemaEntry, TreeInsertSpec, UseBlocksApi };
1937
+ export declare function injectBlokInstance(): Signal<Blok | null>;
1938
+ export type { AngularBlockMountedContext, AngularBlockRenderContext, BlockNode, BlockToolStatics, BlokAngularConfig, BlokReadyScope, CaretTarget, ChildAttributes, ChildAttributesFn, CreateAngularBlockSpec, InjectBlocksOptions, InjectBlokReadyOptions, InsertPosition, InsertSpec, MoveTarget, PropSchema, PropSchemaEntry, TreeInsertSpec, UseBlocksApi };