@bloklabs/react 1.2.1 → 1.2.3

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.
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Live tool-config functions: the reason a consumer should never have to freeze
3
+ * callback identities (`useState(() => ...)`) or recreate the editor to get
4
+ * fresh closures into tool configs.
5
+ *
6
+ * Core snapshots each tool's config when a block instance is constructed
7
+ * (`ToolClass.settings` builds a merged copy), so a function passed in `tools`
8
+ * at editor construction would otherwise be pinned forever — consumers worked
9
+ * around it by freezing identities and smuggling live values through external
10
+ * stores. Instead, `useBlok` hands core STABLE wrapper functions that delegate
11
+ * to whatever closure the LATEST render passed at the same config path. New
12
+ * render → new closure → the wrapper calls it; the editor is never recreated.
13
+ */
14
+
15
+ type AnyFunction = (...args: unknown[]) => unknown;
16
+
17
+ const isPlainObject = (value: unknown): value is Record<string, unknown> => {
18
+ if (typeof value !== 'object' || value === null) {
19
+ return false;
20
+ }
21
+
22
+ const proto: unknown = Object.getPrototypeOf(value);
23
+
24
+ return proto === Object.prototype || proto === null;
25
+ };
26
+
27
+ /** Walk `root` down `path`, returning undefined the moment the shape diverges. */
28
+ const resolveAtPath = (root: unknown, path: readonly (string | number)[]): unknown =>
29
+ path.reduce<unknown>((node, key) => {
30
+ if (Array.isArray(node)) {
31
+ return typeof key === 'number' ? node[key] : undefined;
32
+ }
33
+
34
+ if (isPlainObject(node)) {
35
+ return node[key];
36
+ }
37
+
38
+ return undefined;
39
+ }, root);
40
+
41
+ /**
42
+ * Build the component-facing `config` for a react block from the LATEST render's
43
+ * raw config, preferring the stable live wrapper from `wrappedConfig` wherever
44
+ * both sides hold a function at the same path. The result carries the newest
45
+ * non-function VALUES while keeping function identities stable across renders
46
+ * (wrappers already delegate to the latest closure), so a deep-equal dedupe of
47
+ * successive results only fires when a value genuinely changed.
48
+ */
49
+ export const buildLiveBlockConfig = (
50
+ wrappedConfig: unknown,
51
+ latestConfig: unknown
52
+ ): Record<string, unknown> => {
53
+ const merge = (latest: unknown, wrapped: unknown): unknown => {
54
+ if (typeof latest === 'function') {
55
+ return typeof wrapped === 'function' ? wrapped : latest;
56
+ }
57
+
58
+ if (Array.isArray(latest)) {
59
+ const wrappedArray = Array.isArray(wrapped) ? wrapped : [];
60
+
61
+ return latest.map((item, index) => merge(item, wrappedArray[index]));
62
+ }
63
+
64
+ if (isPlainObject(latest)) {
65
+ const wrappedObject = isPlainObject(wrapped) ? wrapped : {};
66
+ const result: Record<string, unknown> = {};
67
+
68
+ for (const [key, value] of Object.entries(latest)) {
69
+ result[key] = merge(value, wrappedObject[key]);
70
+ }
71
+
72
+ return result;
73
+ }
74
+
75
+ return latest;
76
+ };
77
+
78
+ const merged = merge(latestConfig, wrappedConfig);
79
+
80
+ return isPlainObject(merged) ? merged : {};
81
+ };
82
+
83
+ /**
84
+ * Return a copy of `tools` where every function inside a tool's `config` is
85
+ * replaced by a stable wrapper that calls the function found at the same path
86
+ * in `getLatestTools()` (falling back to the construction-time original when
87
+ * the latest config no longer carries one). Tool classes and non-function
88
+ * values pass through untouched; the consumer's objects are never mutated.
89
+ */
90
+ export const bindLiveToolConfigFunctions = (tools: unknown, getLatestTools: () => unknown): unknown => {
91
+ if (!isPlainObject(tools)) {
92
+ return tools;
93
+ }
94
+
95
+ const result: Record<string, unknown> = {};
96
+
97
+ for (const [name, entry] of Object.entries(tools)) {
98
+ if (!isPlainObject(entry) || !isPlainObject(entry.config)) {
99
+ result[name] = entry;
100
+
101
+ continue;
102
+ }
103
+
104
+ const seen = new WeakSet();
105
+
106
+ const wrapNode = (node: unknown, path: readonly (string | number)[]): unknown => {
107
+ if (typeof node === 'function') {
108
+ const original = node as AnyFunction;
109
+
110
+ return function liveConfigFunction(this: unknown, ...args: unknown[]): unknown {
111
+ const latestTools = getLatestTools();
112
+ const latestEntry = isPlainObject(latestTools) ? latestTools[name] : undefined;
113
+ const latestConfig = isPlainObject(latestEntry) ? latestEntry.config : undefined;
114
+ const latest = resolveAtPath(latestConfig, path);
115
+ const target = typeof latest === 'function' ? (latest as AnyFunction) : original;
116
+
117
+ // Preserve the call-site `this` (config functions may be invoked as
118
+ // methods of their config object).
119
+ return Reflect.apply(target, this, args);
120
+ };
121
+ }
122
+
123
+ if (Array.isArray(node)) {
124
+ if (seen.has(node)) {
125
+ return node;
126
+ }
127
+ seen.add(node);
128
+
129
+ return node.map((item, index) => wrapNode(item, [...path, index]));
130
+ }
131
+
132
+ if (isPlainObject(node)) {
133
+ if (seen.has(node)) {
134
+ return node;
135
+ }
136
+ seen.add(node);
137
+
138
+ const wrapped: Record<string, unknown> = {};
139
+
140
+ for (const [key, value] of Object.entries(node)) {
141
+ wrapped[key] = wrapNode(value, [...path, key]);
142
+ }
143
+
144
+ return wrapped;
145
+ }
146
+
147
+ return node;
148
+ };
149
+
150
+ result[name] = { ...entry, config: wrapNode(entry.config, []) };
151
+ }
152
+
153
+ return result;
154
+ };
@@ -0,0 +1,21 @@
1
+ import type { BlockPortalRegistry } from './block-portal-registry';
2
+
3
+ /**
4
+ * Associates each live Blok instance with its portal registry, keyed by the
5
+ * editor (mirrors the holder WeakMap). `useBlok` sets it at creation;
6
+ * `BlokContent` reads it to mount the shared `BlockPortalHost`. WeakMap → no
7
+ * leak on destroy.
8
+ */
9
+ const registries = new WeakMap<WeakKey, BlockPortalRegistry>();
10
+
11
+ export function setRegistry(editor: WeakKey, registry: BlockPortalRegistry): void {
12
+ registries.set(editor, registry);
13
+ }
14
+
15
+ export function getRegistry(editor: WeakKey): BlockPortalRegistry | undefined {
16
+ return registries.get(editor);
17
+ }
18
+
19
+ export function removeRegistry(editor: WeakKey): void {
20
+ registries.delete(editor);
21
+ }
package/src/useBlok.ts CHANGED
@@ -3,6 +3,14 @@ import { Blok as BlokRuntime } from '@bloklabs/core';
3
3
  import { setHolder, removeHolder } from './holder-map';
4
4
  import { deepEqual } from './deep-equal';
5
5
  import { BlokDefaultsContext, mergeBlokDefaults } from './provide-blok';
6
+ import {
7
+ createBlockPortalRegistry,
8
+ BLOK_PORTAL_REGISTRY_CONFIG_KEY,
9
+ BLOK_TOOL_NAME_CONFIG_KEY,
10
+ type BlockPortalRegistry,
11
+ } from './block-portal-registry';
12
+ import { setRegistry, removeRegistry } from './registry-map';
13
+ import { bindLiveToolConfigFunctions, buildLiveBlockConfig } from './live-tool-config';
6
14
  import { normalizeReadOnlyConfig } from '@bloklabs/core/adapters';
7
15
  import type { Blok } from '@/types';
8
16
  import type { UseBlokConfig } from './types';
@@ -18,6 +26,50 @@ interface EditorInstanceState {
18
26
  depsToken: Record<string, unknown> | null;
19
27
  }
20
28
 
29
+ /**
30
+ * Inject the editor's portal registry into every `createReactBlock` tool's
31
+ * config (vanilla tools are left untouched), returning a NEW tools object so
32
+ * the consumer's config is never mutated. A react-block tool is constructed by
33
+ * CORE, outside any React render, so it cannot read context — this config
34
+ * bridge is how the tool reaches its editor-scoped registry.
35
+ */
36
+ const isReactBlockEntry = (entry: unknown): boolean => {
37
+ const toolClass = typeof entry === 'function' ? entry : (entry as { class?: unknown })?.class;
38
+
39
+ return (
40
+ typeof toolClass === 'function' &&
41
+ (toolClass as { __isBlokReactBlock?: boolean }).__isBlokReactBlock === true
42
+ );
43
+ };
44
+
45
+ const injectPortalRegistry = (tools: unknown, registry: BlockPortalRegistry): unknown => {
46
+ if (tools === null || typeof tools !== 'object') {
47
+ return tools;
48
+ }
49
+
50
+ const result: Record<string, unknown> = {};
51
+
52
+ for (const [name, entry] of Object.entries(tools as Record<string, unknown>)) {
53
+ if (!isReactBlockEntry(entry)) {
54
+ result[name] = entry;
55
+
56
+ continue;
57
+ }
58
+
59
+ const base: Record<string, unknown> =
60
+ typeof entry === 'function' ? { class: entry } : { ...(entry as Record<string, unknown>) };
61
+
62
+ base.config = {
63
+ ...((base.config as Record<string, unknown> | undefined) ?? {}),
64
+ [BLOK_PORTAL_REGISTRY_CONFIG_KEY]: registry,
65
+ [BLOK_TOOL_NAME_CONFIG_KEY]: name,
66
+ };
67
+ result[name] = base;
68
+ }
69
+
70
+ return result;
71
+ };
72
+
21
73
  /**
22
74
  * React hook that manages a Blok editor instance lifecycle.
23
75
  *
@@ -76,6 +128,12 @@ export function useBlok(configInput: UseBlokConfig, deps?: DependencyList): Blok
76
128
  // success baseline — so a failed render can't strand the baseline on content
77
129
  // the editor never actually showed.
78
130
  const pendingDataRef = useRef<UseBlokConfig['data']>(undefined);
131
+ // Live block-config channel (see the sync effect below): the current editor's
132
+ // portal registry, its construction-time wrapped tools (stable function
133
+ // wrappers), and the last config pushed per tool name (dedupe baseline).
134
+ const liveRegistryRef = useRef<BlockPortalRegistry | null>(null);
135
+ const wrappedToolsRef = useRef<unknown>(undefined);
136
+ const pushedBlockConfigsRef = useRef<Map<string, Record<string, unknown>>>(new Map());
79
137
 
80
138
  // Main lifecycle effect
81
139
  useEffect(() => {
@@ -115,6 +173,7 @@ export function useBlok(configInput: UseBlokConfig, deps?: DependencyList): Blok
115
173
  // Destroy leftover editor from a previous deps cycle
116
174
  if (state.editor !== null && !state.isDestroyed) {
117
175
  removeHolder(state.editor);
176
+ removeRegistry(state.editor);
118
177
  try {
119
178
  state.editor.destroy();
120
179
  } catch {
@@ -136,8 +195,29 @@ export function useBlok(configInput: UseBlokConfig, deps?: DependencyList): Blok
136
195
 
137
196
  // Wrap callbacks via ref so they never go stale
138
197
  const currentConfig = configRef.current;
198
+
199
+ // Per-editor portal registry for `createReactBlock` tools: injected into
200
+ // each react-block tool's config (so the core-constructed tool can reach
201
+ // it) and associated with the editor below (so BlokContent can mount the
202
+ // shared portal host). Tool-config FUNCTIONS are first re-bound to stable
203
+ // wrappers that always call the latest render's closure — so consumers
204
+ // never need to freeze identities or recreate the editor to update them.
205
+ const registry = createBlockPortalRegistry();
206
+ const wrappedTools =
207
+ currentConfig.tools === undefined
208
+ ? undefined
209
+ : bindLiveToolConfigFunctions(currentConfig.tools, () => configRef.current.tools);
210
+ const liveTools = wrappedTools === undefined ? undefined : injectPortalRegistry(wrappedTools, registry);
211
+
212
+ // Reset the live block-config channel for this editor: the sync effect below
213
+ // reads these to push changed non-function config VALUES to mounted blocks.
214
+ liveRegistryRef.current = registry;
215
+ wrappedToolsRef.current = wrappedTools;
216
+ pushedBlockConfigsRef.current = new Map();
217
+
139
218
  const blokConfig = {
140
219
  ...currentConfig,
220
+ ...(liveTools === undefined ? {} : { tools: liveTools as UseBlokConfig['tools'] }),
141
221
  holder,
142
222
  onReady: (...args: Parameters<NonNullable<UseBlokConfig['onReady']>>): void => {
143
223
  configRef.current.onReady?.(...args);
@@ -182,6 +262,7 @@ export function useBlok(configInput: UseBlokConfig, deps?: DependencyList): Blok
182
262
  // tell a genuine post-construction change from the construction value itself.
183
263
  constructedDataRef.current = currentConfig.data;
184
264
  setHolder(blok, holder);
265
+ setRegistry(blok, registry);
185
266
 
186
267
  void blok.isReady
187
268
  .then(() => {
@@ -193,6 +274,7 @@ export function useBlok(configInput: UseBlokConfig, deps?: DependencyList): Blok
193
274
  .catch(() => {
194
275
  if (state.editor === blok && !state.isDestroyed) {
195
276
  removeHolder(blok);
277
+ removeRegistry(blok);
196
278
  try {
197
279
  blok.destroy();
198
280
  } catch {
@@ -211,6 +293,58 @@ export function useBlok(configInput: UseBlokConfig, deps?: DependencyList): Blok
211
293
  };
212
294
  }, [depsToken]);
213
295
 
296
+ // Live tool-config VALUES for react blocks. Function slots are already live
297
+ // via the stable wrappers, but non-function values (permissions, URLs,
298
+ // locale…) were snapshotted at construction — which forced consumers to put
299
+ // them in `deps` and recreate the whole editor to change them. Instead, this
300
+ // effect runs on every render, rebuilds each react-block tool's
301
+ // component-facing config from the LATEST render's values (keeping wrapper
302
+ // identities for functions), and pushes it through the portal registry when
303
+ // it genuinely changed — mounted blocks re-render in place, future blocks
304
+ // start from the latest config.
305
+ useEffect(() => {
306
+ const registry = liveRegistryRef.current;
307
+ const state = stateRef.current;
308
+
309
+ if (registry === null || state.editor === null || state.isDestroyed) {
310
+ return;
311
+ }
312
+
313
+ const latestTools = configRef.current.tools as Record<string, unknown> | undefined;
314
+
315
+ if (latestTools === null || typeof latestTools !== 'object') {
316
+ return;
317
+ }
318
+
319
+ const wrappedTools = wrappedToolsRef.current as Record<string, unknown> | undefined;
320
+
321
+ for (const [name, entry] of Object.entries(latestTools)) {
322
+ if (!isReactBlockEntry(entry)) {
323
+ continue;
324
+ }
325
+
326
+ const latestConfig =
327
+ typeof entry === 'function' ? {} : ((entry as { config?: unknown }).config ?? {});
328
+ const wrappedEntry = wrappedTools?.[name];
329
+ const wrappedConfig =
330
+ wrappedEntry !== null && typeof wrappedEntry === 'object'
331
+ ? (wrappedEntry as { config?: unknown }).config
332
+ : undefined;
333
+
334
+ const candidate = buildLiveBlockConfig(wrappedConfig, latestConfig);
335
+ const pushed = pushedBlockConfigsRef.current.get(name);
336
+
337
+ if (pushed !== undefined && deepEqual(candidate, pushed)) {
338
+ continue;
339
+ }
340
+
341
+ const frozen = Object.freeze(candidate);
342
+
343
+ pushedBlockConfigsRef.current.set(name, frozen);
344
+ registry.setToolConfig(name, frozen);
345
+ }
346
+ });
347
+
214
348
  // Reactive: readOnly (object form normalized so the effect dep is a stable boolean)
215
349
  const readOnlyEnabled = normalizeReadOnlyConfig(config.readOnly).enabled;
216
350
  useEffect(() => {
@@ -348,6 +482,7 @@ function deferDestroy(
348
482
  state.destroyTimeout = setTimeout(() => {
349
483
  if (state.editor !== null) {
350
484
  removeHolder(state.editor);
485
+ removeRegistry(state.editor);
351
486
  try {
352
487
  state.editor.destroy();
353
488
  } catch {
package/types/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { BlokConfig, BlockToolData, OutputBlockData, OutputData } from '@bloklabs/core';
2
+ import type { BlockAPI, BlockToolConstructable, ToolboxConfig } from '@bloklabs/core';
2
3
  import type { Blok, EditorWidth, BlockRenderedPayload, BlocksRenderedPayload } from '@bloklabs/core';
3
4
  import type { BlockTuneData } from '@bloklabs/core';
4
5
  import type { MarkdownImportConfig } from '@bloklabs/core/markdown';
@@ -38,7 +39,7 @@ export interface BlokContentProps extends React.HTMLAttributes<HTMLDivElement> {
38
39
  * React hook that creates and manages a Blok editor instance.
39
40
  *
40
41
  * @param config - Editor configuration (all BlokConfig props except `holder`)
41
- * @param deps - Optional dependency array. When any dep changes, the editor is destroyed and recreated. Keep each value referentially stable (primitives or useMemo-stable objects) so the editor isn't recreated every render.
42
+ * @param deps - Optional dependency array. When any dep changes, the editor is destroyed and recreated. Keep each value referentially stable (primitives or useMemo-stable objects) so the editor isn't recreated every render. Reserve it for true identity changes (e.g. the document id): tool-config functions are re-bound to the latest render's closure automatically, and non-function config values of `createReactBlock` tools are pushed to mounted blocks in place — neither belongs in deps.
42
43
  * @returns The Blok editor instance, or null during SSR / before initialization.
43
44
  *
44
45
  * @example
@@ -80,6 +81,8 @@ export interface BlokEditorProps
80
81
  * When any value changes, the editor is destroyed and recreated. Keep each
81
82
  * value referentially stable (primitives or useMemo-stable objects) — a dep
82
83
  * whose identity changes every render recreates the editor each time.
84
+ * Reserve it for true identity changes (e.g. the document id): tool-config
85
+ * functions and `createReactBlock` config values stay live without it.
83
86
  */
84
87
  deps?: React.DependencyList;
85
88
  /** Test id forwarded to the editor container element (via data-testid). */
@@ -536,3 +539,144 @@ export interface UseBlocksApi {
536
539
  * @param editor - the Blok instance from useBlok, or null before it is ready
537
540
  */
538
541
  export declare function useBlocks(editor: Blok | null): UseBlocksApi;
542
+
543
+ // ---------------------------------------------------------------------------
544
+ // Block authoring (createReactBlock + portal host)
545
+ // ---------------------------------------------------------------------------
546
+
547
+ /** One field of a {@link CreateReactBlockSpec.propSchema}. */
548
+ export interface PropSchemaEntry {
549
+ default: unknown;
550
+ values?: readonly unknown[];
551
+ }
552
+
553
+ /**
554
+ * Declarative data shape. Its keys are EXACTLY the keys `save()` returns to Yjs
555
+ * (a cleared field is written as its default, never dropped).
556
+ */
557
+ export type PropSchema = Record<string, PropSchemaEntry>;
558
+
559
+ /** Props handed to a React block's `component` (the only data write path is `commit`). */
560
+ export interface ReactBlockRenderProps<Data, Config = Record<string, unknown>> {
561
+ /** Frozen snapshot of the block data. Re-rendered with a new value on change; never mutate. */
562
+ data: Readonly<Data>;
563
+ /**
564
+ * The ONLY data write path: merge a partial patch and sync once. Idempotent —
565
+ * a patch that changes nothing is a full no-op (no re-render, no change
566
+ * event), so effects may safely echo current values without a guard.
567
+ */
568
+ commit: (patch: Partial<Data>) => void;
569
+ /** This block's per-block API. */
570
+ block: BlockAPI;
571
+ /**
572
+ * Read-only flag. Toggled IN PLACE by core's read-only switch (no remount,
573
+ * ephemeral state survives). Honor it — a block that ignores it stays
574
+ * interactive when the editor is read-only.
575
+ */
576
+ readOnly: boolean;
577
+ /**
578
+ * The tool's `config` from the consumer's `tools` map (adapter-internal keys
579
+ * stripped) — the first-class channel for host props (permissions, CDN/upload
580
+ * URLs, locale…). LIVE under `useBlok`/`BlokEditor`: functions always call
581
+ * the latest render's closure, and changed non-function values are pushed to
582
+ * mounted blocks in place — config values never belong in `deps`.
583
+ */
584
+ config: Readonly<Partial<Config>>;
585
+ /** Engine-owned child slot — render `<BlockChildren />` for a container block. */
586
+ BlockChildren: React.ComponentType;
587
+ }
588
+
589
+ /** Spec for {@link createReactBlock}. */
590
+ export interface CreateReactBlockSpec<
591
+ Data extends BlockToolData = BlockToolData,
592
+ Config = Record<string, unknown>
593
+ > {
594
+ type: string;
595
+ toolbox?: ToolboxConfig;
596
+ propSchema: PropSchema;
597
+ component: React.ComponentType<ReactBlockRenderProps<Data, Config>>;
598
+ onRendered?: (block: BlockAPI) => void;
599
+ onMoved?: (block: BlockAPI) => void;
600
+ onRemoved?: (block: BlockAPI) => void;
601
+ }
602
+
603
+ /**
604
+ * Author a first-party React block: returns a `BlockToolConstructable`
605
+ * registered exactly like a vanilla tool (`tools: { type: createReactBlock(...) }`).
606
+ * Every block renders through the editor's shared portal host INSIDE the React
607
+ * tree that renders `BlokContent`/`BlokEditor`, so app-level context (themes,
608
+ * design-system providers, stores) reaches block components with no bridge, and
609
+ * fresh render closures reach them with no editor recreation.
610
+ *
611
+ * @example
612
+ * ```tsx
613
+ * const Callout = createReactBlock<{ text: string }>({
614
+ * type: 'callout',
615
+ * propSchema: { text: { default: '' } },
616
+ * component: ({ data, commit }) => (
617
+ * <aside onClick={() => commit({ text: 'hi' })}>{data.text}</aside>
618
+ * ),
619
+ * });
620
+ * ```
621
+ */
622
+ export declare function createReactBlock<
623
+ Data extends BlockToolData = BlockToolData,
624
+ Config = Record<string, unknown>
625
+ >(spec: CreateReactBlockSpec<Data, Config>): BlockToolConstructable;
626
+
627
+ /**
628
+ * Tool-config key carrying the editor's portal registry into a
629
+ * `createReactBlock` tool. Injected automatically by `useBlok`; exposed for
630
+ * advanced setups that construct tools outside `useBlok`.
631
+ */
632
+ export declare const BLOK_PORTAL_REGISTRY_CONFIG_KEY: '__blokPortalRegistry';
633
+
634
+ /**
635
+ * Tool-config key carrying the name a react-block tool is registered under in
636
+ * the consumer's `tools` map. Injected automatically by `useBlok`; routes live
637
+ * tool-config updates to the tool's mounted blocks.
638
+ */
639
+ export declare const BLOK_TOOL_NAME_CONFIG_KEY: '__blokToolName';
640
+
641
+ /** One mounted React block in the portal registry. */
642
+ export interface BlockPortalEntry {
643
+ hostEl: HTMLElement;
644
+ component: React.ComponentType<Record<string, unknown>>;
645
+ props: Record<string, unknown>;
646
+ /** Name the tool is registered under — routes per-tool live config updates. */
647
+ toolName?: string;
648
+ }
649
+
650
+ /**
651
+ * Per-editor registry of React blocks, shaped as an external store
652
+ * (`subscribe`/`getSnapshot`) consumed by `BlockPortalHost` via
653
+ * `useSyncExternalStore`. Snapshots are immutable and referentially stable
654
+ * between mutations.
655
+ */
656
+ export interface BlockPortalRegistry {
657
+ register(id: string, entry: BlockPortalEntry): void;
658
+ unregister(id: string): void;
659
+ setProps(id: string, props: Record<string, unknown>): void;
660
+ /**
661
+ * Replace the `config` prop of every entry registered under `toolName`, and
662
+ * remember it so entries registered later start from the latest config.
663
+ */
664
+ setToolConfig(toolName: string, config: Record<string, unknown>): void;
665
+ subscribe(listener: () => void): () => void;
666
+ getSnapshot(): ReadonlyMap<string, BlockPortalEntry>;
667
+ }
668
+
669
+ /** Create a fresh portal registry (one per editor instance). */
670
+ export declare function createBlockPortalRegistry(): BlockPortalRegistry;
671
+
672
+ /** Props for {@link BlockPortalHost}. */
673
+ export interface BlockPortalHostProps {
674
+ registry: BlockPortalRegistry;
675
+ }
676
+
677
+ /**
678
+ * The single shared render tree that portals each registered React block into
679
+ * its Blok-owned host element. Mounted automatically by `BlokContent`; exposed
680
+ * for advanced setups that manage the registry themselves.
681
+ */
682
+ export declare function BlockPortalHost(props: BlockPortalHostProps): React.ReactElement;