@bloklabs/react 1.16.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bloklabs/react",
3
- "version": "1.16.0",
3
+ "version": "2.0.0",
4
4
  "description": "React block editor — official React bindings for Blok, the headless Notion-like rich text editor. Hooks and components that render blocks and save to JSON.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "JackUait",
@@ -43,8 +43,7 @@
43
43
  },
44
44
  "files": [
45
45
  "dist",
46
- "types",
47
- "src"
46
+ "types"
48
47
  ],
49
48
  "publishConfig": {
50
49
  "access": "public"
@@ -54,7 +53,7 @@
54
53
  "test": "yarn run -T vitest run"
55
54
  },
56
55
  "peerDependencies": {
57
- "@bloklabs/core": "^1.16.0",
56
+ "@bloklabs/core": "^2.0.0",
58
57
  "react": "^18.0.0 || ^19.0.0",
59
58
  "react-dom": "^18.0.0 || ^19.0.0"
60
59
  }
package/types/index.d.ts CHANGED
@@ -107,6 +107,29 @@ export function useBlok(
107
107
  */
108
108
  export declare const BlokContent: React.ForwardRefExoticComponent<BlokContentProps & React.RefAttributes<HTMLDivElement>>;
109
109
 
110
+ /**
111
+ * Props for the BlokTitle component.
112
+ * Div attributes pass through. Children are not rendered: the div belongs to Blok.
113
+ */
114
+ export type BlokTitleProps = {
115
+ /** The Blok editor instance, or null while it is not ready. */
116
+ editor: Blok | null;
117
+ } & Omit<React.HTMLAttributes<HTMLDivElement>, 'children'>;
118
+
119
+ /**
120
+ * Places the editor's page title in a div of your own, through `editor.title.mount`.
121
+ * The editor needs `pageTitle` set: this component only moves the title.
122
+ * On unmount the title goes back above the first block.
123
+ *
124
+ * @example
125
+ * ```tsx
126
+ * const [editor, setEditor] = useState<Blok | null>(null);
127
+ * <BlokTitle editor={editor} />
128
+ * <BlokEditor ref={setEditor} pageTitle />
129
+ * ```
130
+ */
131
+ export declare const BlokTitle: React.ForwardRefExoticComponent<BlokTitleProps & React.RefAttributes<HTMLDivElement>>;
132
+
110
133
  /**
111
134
  * Props for the BlokEditor component — all useBlok config (except `onReady`,
112
135
  * re-typed to receive the ready instance) plus every standard HTML div attribute
@@ -311,6 +334,19 @@ export interface InsertSpec {
311
334
  caret?: CaretTarget;
312
335
  }
313
336
 
337
+ /** Spec for {@link UseBlocksApi.create}. */
338
+ export interface CreateSpec {
339
+ type?: string;
340
+ data?: BlockToolData;
341
+ /** `null` is the root. Omitted: the `before`/`after` sibling's parent, or the root. */
342
+ parentId?: string | null;
343
+ /** Defaults to `'end'`. */
344
+ position?: InsertPosition;
345
+ id?: string;
346
+ tunes?: { [name: string]: BlockTuneData };
347
+ focus?: boolean;
348
+ }
349
+
314
350
  /**
315
351
  * One node of a pre-built nested subtree for {@link UseBlocksApi.insertTree}.
316
352
  *
@@ -421,7 +457,7 @@ export interface UseBlocksApi {
421
457
  *
422
458
  * `config` (optional {@link MarkdownImportConfig}) is forwarded to the
423
459
  * converter so custom-tool consumers can map markdown nodes into their tools
424
- * (`toolMap`/`onUnknownNode`), toggle GFM, or add micromark/mdast extensions.
460
+ * (`toolMap`/`onUnknownNode`) or toggle GFM.
425
461
  *
426
462
  * Returns ALL created {@link BlockNode}s in document order — including any the
427
463
  * markdown nested internally (e.g. a table's cell children), not just the
@@ -441,9 +477,10 @@ export interface UseBlocksApi {
441
477
  * additive {@link insertMarkdown}). Async: the serializer is lazy-loaded, like
442
478
  * the importer. Markdown cannot express every block, so some structure is
443
479
  * dropped (table `colspan`/`rowspan`, heading columns). Returns `''` for an
444
- * empty document. Pre-ready: resolves to `''`.
480
+ * empty document. Pre-ready: resolves to `''`. `title: true` writes the
481
+ * page title first, as core's does.
445
482
  */
446
- exportMarkdown(): Promise<string>;
483
+ exportMarkdown(options?: { title?: boolean }): Promise<string>;
447
484
  /**
448
485
  * Move `id` to a flat slot, as a single operation. No-op when `id` is unknown.
449
486
  * For a relative `{ before|after }` target it is also a no-op when the ref is
@@ -598,6 +635,14 @@ export interface UseBlocksApi {
598
635
  * dep array. Pre-ready: `null`.
599
636
  */
600
637
  insertInsideParent(parentId: string, insertIndex: number, childData?: BlockToolData): BlockNode | null;
638
+ /**
639
+ * Add a block the way a toolbox pick does, for a host button such as "New
640
+ * page" — delegates to core's `blocks.create`. Goes to the end of the root
641
+ * unless `parentId`/`position` say otherwise. Resolves `null` for an unknown
642
+ * tool or a place that does not exist; rejects with the tool's
643
+ * `prepareInsert` error, inserting nothing. Pre-ready: `null`.
644
+ */
645
+ create(spec?: CreateSpec): Promise<BlockNode | null>;
601
646
  /**
602
647
  * Replace the WHOLE document with blocks parsed from saved {@link OutputData}
603
648
  * (the `save()` shape) — delegates to core's `blocks.render`. Unlike
@@ -923,6 +968,19 @@ export interface CreateReactBlockSpec<
923
968
  * @param block - this block's per-block API
924
969
  */
925
970
  onNavigationEnter?: (event: KeyboardEvent, block: BlockAPI) => boolean;
971
+ /**
972
+ * Core's `isReadOnly` hook: true when this block takes no edits from the
973
+ * user or an agent while the editor stays editable (no delete, move, merge,
974
+ * nesting or undo into it). Asked on each gesture; a throw counts as true.
975
+ * @param block - this block's per-block API
976
+ */
977
+ isReadOnly?: (block: BlockAPI) => boolean;
978
+ /**
979
+ * Core's `isDataReadOnly` hook: true when only this block's own data is
980
+ * frozen and its children stay open. A throw counts as true.
981
+ * @param block - this block's per-block API
982
+ */
983
+ isDataReadOnly?: (block: BlockAPI) => boolean;
926
984
  onRendered?: (block: BlockAPI) => void;
927
985
  /**
928
986
  * Fired ONCE per block instance, after the portal's FIRST commit — the first
@@ -963,7 +1021,8 @@ export interface CreateReactBlockSpec<
963
1021
  */
964
1022
  onCreated?: (block: BlockAPI, context: ReactBlockMountedContext) => void;
965
1023
  onMoved?: (block: BlockAPI) => void;
966
- onRemoved?: (block: BlockAPI) => void;
1024
+ /** `removal.deleted` is true when a local edit deleted the block (see BlockTool.removed). */
1025
+ onRemoved?: (block: BlockAPI, removal?: { deleted: boolean }) => void;
967
1026
  }
968
1027
 
969
1028
  /**
@@ -1,58 +0,0 @@
1
- import { Component, useSyncExternalStore, type ReactElement, type ReactNode } from 'react';
2
- import { createPortal } from 'react-dom';
3
- import type { BlockPortalRegistry } from './block-portal-registry';
4
-
5
- interface BoundaryState {
6
- failed: boolean;
7
- }
8
-
9
- /**
10
- * Wraps a single block's render so a throw inside it can't tear down the whole
11
- * editor's render tree (only the broken block goes blank).
12
- */
13
- class BlockErrorBoundary extends Component<{ children: ReactNode }, BoundaryState> {
14
- public state: BoundaryState = { failed: false };
15
-
16
- public static getDerivedStateFromError(): BoundaryState {
17
- return { failed: true };
18
- }
19
-
20
- public componentDidCatch(): void {
21
- // Swallowed: one broken block must not unmount its siblings.
22
- }
23
-
24
- public render(): ReactNode {
25
- return this.state.failed ? null : this.props.children;
26
- }
27
- }
28
-
29
- export interface BlockPortalHostProps {
30
- registry: BlockPortalRegistry;
31
- }
32
-
33
- /**
34
- * The single shared render tree that portals each registered React block into
35
- * its Blok-owned host element. Mounted once per editor (by `BlokContent`), it
36
- * subscribes to the registry and emits one `createPortal` per entry — so all
37
- * blocks share one reconciler and the surrounding app's React context
38
- * (`createPortal` preserves the component tree's context, not the DOM target's).
39
- */
40
- export function BlockPortalHost({ registry }: BlockPortalHostProps): ReactElement {
41
- const entries = useSyncExternalStore(registry.subscribe, registry.getSnapshot, registry.getSnapshot);
42
-
43
- return (
44
- <>
45
- {Array.from(entries.entries()).map(([id, entry]) => {
46
- const BlockComponent = entry.component;
47
-
48
- return createPortal(
49
- <BlockErrorBoundary>
50
- <BlockComponent {...entry.props} />
51
- </BlockErrorBoundary>,
52
- entry.hostEl,
53
- id
54
- );
55
- })}
56
- </>
57
- );
58
- }
@@ -1,112 +0,0 @@
1
- import { useEffect, useRef, forwardRef } from 'react';
2
- import { getHolder } from './holder-map';
3
- import { getRegistry } from './registry-map';
4
- import { BlockPortalHost } from './BlockPortalHost';
5
- import { BlokInstanceContext } from './blok-instance-context';
6
- import type { Blok } from '@/types';
7
- import type { BlokContentProps } from './types';
8
-
9
- /**
10
- * Re-drive the boot-time URL-hash scroll now that the editor's holder is in the
11
- * document. Reads the current hash, decodes it (falling back to the raw value on
12
- * a malformed percent-sequence, which simply matches no block), and asks the
13
- * editor to scroll — a one-shot drain of the navigation the constructor deferred
14
- * because the holder was detached. No-op when the URL carries no hash.
15
- * @param editor - the ready Blok instance whose holder just connected
16
- */
17
- function drainDeferredHashScroll(editor: Blok): void {
18
- if (typeof window === 'undefined') {
19
- return;
20
- }
21
-
22
- const raw = window.location.hash.slice(1);
23
-
24
- if (raw === '') {
25
- return;
26
- }
27
-
28
- editor.blocks.scrollToBlock?.(safeDecodeHashFragment(raw));
29
- }
30
-
31
- /**
32
- * Decodes a URL hash fragment, falling back to the raw value on a malformed
33
- * percent-sequence (e.g. `%ZZ`) — which simply matches no block downstream.
34
- * @param raw - hash fragment without the leading `#`
35
- */
36
- function safeDecodeHashFragment(raw: string): string {
37
- try {
38
- return decodeURIComponent(raw);
39
- } catch {
40
- return raw;
41
- }
42
- }
43
-
44
- /**
45
- * Component that provides the DOM mount point for a Blok editor.
46
- * Renders a <div> and adopts the editor's detached holder DOM into it.
47
- * When editor is null, renders an empty div (for layout stability).
48
- * Passes through all standard HTML div attributes (className, style, etc.).
49
- */
50
- export const BlokContent = forwardRef<HTMLDivElement, BlokContentProps>(
51
- function BlokContent({ editor, ...divProps }, ref) {
52
- const containerRef = useRef<HTMLDivElement | null>(null);
53
-
54
- const setRefs = (node: HTMLDivElement | null): void => {
55
- containerRef.current = node;
56
- if (typeof ref === 'function') {
57
- ref(node);
58
- } else if (ref !== null && ref !== undefined) {
59
- const mutableRef = ref;
60
-
61
- mutableRef.current = node;
62
- }
63
- };
64
-
65
- useEffect(() => {
66
- if (editor === null || containerRef.current === null) {
67
- return;
68
- }
69
-
70
- const holder = getHolder(editor);
71
-
72
- if (holder === undefined) {
73
- return;
74
- }
75
-
76
- containerRef.current.appendChild(holder);
77
-
78
- // The editor rendered its seeded content into this holder while it was
79
- // still detached from the document, so the boot-time URL-hash scroll —
80
- // which queries the LIVE document — found nothing and deferred. Now that
81
- // the holder is connected, drain that deferred navigation once via the
82
- // public scroll API (a no-op when there's no matching hash/block), so
83
- // consumers don't have to hand-roll a DOM-polling hook.
84
- drainDeferredHashScroll(editor);
85
-
86
- return (): void => {
87
- holder.remove();
88
- };
89
- }, [editor]);
90
-
91
- // The portal host renders the editor's `createReactBlock` tools INSIDE this
92
- // component tree (via createPortal into each Blok-owned host element), so
93
- // app-level React context flows into block tools with no bridge. It emits
94
- // no DOM at this position, leaving the imperatively-adopted holder alone.
95
- //
96
- // The instance provider rides along for the same reason: this is the one
97
- // place that holds BOTH the live editor and the portal host, so publishing
98
- // it here is what lets a block component call `useBlocks(useBlokInstance())`
99
- // without the host prop-drilling the instance into every block.
100
- const registry = editor === null ? undefined : getRegistry(editor);
101
-
102
- return (
103
- <div ref={setRefs} {...divProps}>
104
- {registry === undefined ? null : (
105
- <BlokInstanceContext.Provider value={editor}>
106
- <BlockPortalHost registry={registry} />
107
- </BlokInstanceContext.Provider>
108
- )}
109
- </div>
110
- );
111
- }
112
- );
@@ -1,201 +0,0 @@
1
- import {
2
- forwardRef,
3
- useImperativeHandle,
4
- useEffect,
5
- useRef,
6
- type DependencyList,
7
- type HTMLAttributes,
8
- } from 'react';
9
- import { useBlok } from './useBlok';
10
- import { BlokContent } from './BlokContent';
11
- import { USE_BLOK_CONFIG_KEYS } from './config-keys';
12
- import type { Blok, BlockRenderedPayload, BlocksRenderedPayload } from '@/types';
13
- import type { UseBlokConfig } from './types';
14
-
15
- /**
16
- * Props for the all-in-one BlokEditor component.
17
- * Accepts every useBlok config prop (except `onReady`, re-typed below to receive
18
- * the ready instance) AND every standard HTML div attribute, which is forwarded
19
- * to the editor container element (mirrors `BlokContent`). `style` and `onChange`
20
- * keep their editor-config meaning (they collide with div attributes of the same
21
- * name), so the container is styled via `className` rather than an inline `style`.
22
- *
23
- * `data` is reactive (controlled-ish): it seeds the INITIAL content and, after
24
- * mount, changing it to new *content* re-renders the editor via `render()` — no
25
- * recreation. Updates are deep-equal–deduped (a new reference with identical
26
- * content is a no-op, so it won't clobber the caret) and serialized (rapid
27
- * changes can't overlap). For ad-hoc reloads you can still call
28
- * `ref.current.render(newData)`.
29
- *
30
- * Pair `data` with `onSave` for a true controlled component: `onSave` is the
31
- * output half, firing (debounced) with the full serialized `OutputData` on every
32
- * content change — no manual `ref.current.save()` polling. Echoing that payload
33
- * straight back via `onSave={setData}` is safe and caret-stable: the adapter
34
- * records the editor's own emitted output as the content baseline, so the
35
- * `onSave → setState → data` round-trip deep-equals that baseline and is deduped
36
- * to a no-op (no re-render, no caret reset, no recursion). Genuine external `data`
37
- * changes still re-render in place.
38
- */
39
- export interface BlokEditorProps
40
- extends Omit<UseBlokConfig, 'onReady'>,
41
- // `onChange`/`onError`/`onSubmit` are Blok config callbacks here — drop the
42
- // DOM element's same-named event handlers so the config signatures win.
43
- Omit<HTMLAttributes<HTMLDivElement>, 'style' | 'onChange' | 'onError' | 'onSubmit'> {
44
- /**
45
- * When any value changes, the editor is destroyed and recreated. Keep each
46
- * value referentially stable (primitives or useMemo-stable objects) — a dep
47
- * whose identity changes every render recreates the editor each time.
48
- *
49
- * Tool configs do NOT belong here — reserve deps for true identity changes
50
- * (e.g. the document id). FUNCTIONS inside tool configs are re-bound to the
51
- * latest render's closure automatically (an inline `uploadByFile` with a
52
- * fresh identity each render reaches the editor live), and non-function
53
- * config VALUES of `createReactBlock` tools (permissions, URLs, locale…) are
54
- * pushed to mounted blocks in place when they change. Neither requires
55
- * recreation or `useState` identity-freezing.
56
- */
57
- deps?: DependencyList;
58
- /** Test id forwarded to the editor container element (via data-testid). */
59
- 'data-testid'?: string;
60
- /**
61
- * Called with the live Blok instance once it is ready. Fires after the
62
- * forwarded ref is committed, so `ref.current` is also populated at this point.
63
- *
64
- * Contract: fires exactly once per editor instance. The editor is recreated
65
- * only when `deps` change or the component remounts; `data` changes —
66
- * including transitions to and from empty content — re-render in place and
67
- * never recreate the editor or re-fire `onReady`.
68
- */
69
- onReady?: (editor: Blok) => void;
70
- /**
71
- * Library-neutral shorthand for the active locale — a plain BCP-47 string
72
- * (`'en'`, `'ru-RU'`, `'nb'`) the host owns and updates. It is normalized and
73
- * routed to `editor.i18n.update({ locale })` in place (caret/undo survive),
74
- * so a host with its own language switcher never has to build an `i18n`
75
- * config object or destroy the editor. Pair it with the re-exported
76
- * `getDirection`/`normalizeLocale` to compute `dir` yourself.
77
- *
78
- * Shorthand for `i18n={{ locale }}`: if both are given, this wins. For
79
- * message overrides or an explicit direction, use the full `i18n` prop.
80
- */
81
- locale?: string;
82
- /**
83
- * Called after a batch render completes (core `blocks:rendered` event). The
84
- * declarative analog of `ref.current.on('blocks:rendered', …)` — mirrors the
85
- * Vue/Angular adapters' rendered-lifecycle outputs.
86
- */
87
- onBlocksRendered?: (payload: BlocksRenderedPayload) => void;
88
- /** Called for each block rendered into the DOM (core `block:rendered` event). */
89
- onBlockRendered?: (payload: BlockRenderedPayload) => void;
90
- }
91
-
92
- /** Fast membership test for partitioning props into editor config vs. div attributes. */
93
- const CONFIG_KEY_SET = new Set<string>(USE_BLOK_CONFIG_KEYS);
94
-
95
- /**
96
- * The recommended way to embed Blok in React. Internally wires `useBlok` and
97
- * `BlokContent`, and forwards a ref to the live `Blok` instance.
98
- *
99
- * Don't wrap this component in `styled()` or any HOC that reserves the `theme`
100
- * prop — styled-components claims `theme` for its own `ThemeProvider`, so it
101
- * never reaches the editor and theme sync silently breaks. Render it directly
102
- * and style the container via `className`.
103
- *
104
- * @example
105
- * ```tsx
106
- * const ref = useRef<Blok | null>(null);
107
- * <BlokEditor ref={ref} id="editor" tools={tools} data={data} theme={theme} onReady={(e) => e.focus()} />;
108
- * ```
109
- */
110
- export const BlokEditor = forwardRef<Blok | null, BlokEditorProps>(
111
- function BlokEditor({ deps, onReady, locale, onBlocksRendered, onBlockRendered, ...rest }, ref) {
112
- const config: Record<string, unknown> = {};
113
- const divProps: Record<string, unknown> = {};
114
-
115
- for (const key of Object.keys(rest)) {
116
- const value = (rest as Record<string, unknown>)[key];
117
-
118
- if (CONFIG_KEY_SET.has(key)) {
119
- config[key] = value;
120
- } else {
121
- divProps[key] = value;
122
- }
123
- }
124
-
125
- // `locale` is a neutral shorthand for `i18n.locale`. Fold it into the i18n
126
- // config (winning over any locale already there) so the single reactive
127
- // `i18n.update` path in `useBlok` carries it — no separate resolver. A fresh
128
- // object each render is fine: that path is deep-equal–deduped.
129
- if (locale !== undefined) {
130
- const existingI18n = config.i18n as Record<string, unknown> | undefined;
131
-
132
- config.i18n = { ...existingI18n, locale };
133
- }
134
-
135
- const editor = useBlok(config, deps);
136
-
137
- useImperativeHandle<Blok | null, Blok | null>(ref, () => editor, [editor]);
138
-
139
- // Own onReady here instead of forwarding it to the core config: the core
140
- // onReady fires before useImperativeHandle commits the ref. This passive
141
- // effect runs AFTER the commit, so both the instance argument and ref.current
142
- // are reliably populated.
143
- const onReadyRef = useRef(onReady);
144
- onReadyRef.current = onReady;
145
- // Hardening, not a bug fix: today this effect only re-runs when `editor`
146
- // changes, but future React semantics (Fast Refresh, <Activity>) may re-run
147
- // an effect with an unchanged value. Tracking the last-notified instance
148
- // guarantees at-most-once per instance while keeping the per-instance
149
- // re-fire on recreation (deps change / remount) intact.
150
- const notifiedEditorRef = useRef<Blok | null>(null);
151
- useEffect(() => {
152
- if (editor !== null && notifiedEditorRef.current !== editor) {
153
- notifiedEditorRef.current = editor;
154
- onReadyRef.current?.(editor);
155
- }
156
- }, [editor]);
157
-
158
- // Subscribe to the rendered-lifecycle events (the declarative analog of the
159
- // Vue/Angular outputs). Latest handlers are read through refs so a new
160
- // callback identity never resubscribes; presence booleans in the deps add or
161
- // drop subscriptions when a handler appears/disappears.
162
- const onBlocksRenderedRef = useRef(onBlocksRendered);
163
- onBlocksRenderedRef.current = onBlocksRendered;
164
- const onBlockRenderedRef = useRef(onBlockRendered);
165
- onBlockRenderedRef.current = onBlockRendered;
166
-
167
- const hasBlocksRendered = Boolean(onBlocksRendered);
168
- const hasBlockRendered = Boolean(onBlockRendered);
169
- useEffect(() => {
170
- if (editor === null) {
171
- return;
172
- }
173
-
174
- const subscriptions: Array<[string, (payload?: unknown) => void]> = [];
175
-
176
- if (hasBlocksRendered) {
177
- const handler = (payload?: unknown): void =>
178
- onBlocksRenderedRef.current?.(payload as BlocksRenderedPayload);
179
-
180
- editor.on('blocks:rendered', handler);
181
- subscriptions.push(['blocks:rendered', handler]);
182
- }
183
-
184
- if (hasBlockRendered) {
185
- const handler = (payload?: unknown): void =>
186
- onBlockRenderedRef.current?.(payload as BlockRenderedPayload);
187
-
188
- editor.on('block:rendered', handler);
189
- subscriptions.push(['block:rendered', handler]);
190
- }
191
-
192
- return (): void => {
193
- for (const [name, handler] of subscriptions) {
194
- editor.off(name, handler);
195
- }
196
- };
197
- }, [editor, hasBlocksRendered, hasBlockRendered]);
198
-
199
- return <BlokContent editor={editor} {...(divProps as HTMLAttributes<HTMLDivElement>)} />;
200
- }
201
- );
package/src/BlokView.tsx DELETED
@@ -1,106 +0,0 @@
1
- import type { HTMLAttributes, ReactNode } from 'react';
2
- import type { BlocksToHtmlOptions } from '@bloklabs/core/view';
3
- import type { LooseOutputData, OutputData } from '@bloklabs/core';
4
-
5
- import { useBlokView } from './useBlokView';
6
-
7
- /**
8
- * Props for {@link BlokView}. Beyond the render options below, any standard
9
- * `<div>` attribute (`id`, `style`, `data-*`, `aria-*`, event handlers, …) is
10
- * forwarded onto the single wrapper element.
11
- */
12
- export interface BlokViewProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
13
- /** Saved document to display (strict or loose wire shape; nullish tolerated). */
14
- data: OutputData | LooseOutputData | null | undefined;
15
- /** View schema from `defineBlokSchema` — display under the composition that produced the document. */
16
- schema?: BlocksToHtmlOptions['schema'];
17
- /** Custom per-tool renderers; win over built-ins. */
18
- renderers?: BlocksToHtmlOptions['renderers'];
19
- /** Unknown-tool policy (default 'skip'). */
20
- onUnknownBlock?: BlocksToHtmlOptions['onUnknownBlock'];
21
- /** Stamp `data-blok-tool="<type>"` on each block root as a styling hook (pairs with the opt-in view stylesheet). */
22
- toolAttributes?: BlocksToHtmlOptions['toolAttributes'];
23
- /** Stamp `data-blok-id="<id>"` on each block root (list items on their `<li>`) for "copy link to block" deep links. */
24
- blockIds?: BlocksToHtmlOptions['blockIds'];
25
- /** Pure URL rewrite hook for block URLs + inline anchors, run before the unsafe-scheme strip. */
26
- transformUrl?: BlocksToHtmlOptions['transformUrl'];
27
- /** Custom renderers for inline elements, keyed by lowercase tag name (equations, mentions). */
28
- inlineRenderers?: BlocksToHtmlOptions['inlineRenderers'];
29
- /** Build the link for a `page` block from its `pageId`. A page card is a link only with this and an allowing {@link pageInfo}. */
30
- pageHref?: BlocksToHtmlOptions['pageHref'];
31
- /** Host metadata (title, icon, access) for a `page` block. Without it a page card shows a neutral "Page" label and no link. */
32
- pageInfo?: BlocksToHtmlOptions['pageInfo'];
33
- /**
34
- * Render with the editor's presentational classes and per-block scaffolding
35
- * so the output matches a read-only editor render (default `true`). Needs
36
- * `@bloklabs/core/view.css` imported to paint. Set `false` for unstyled
37
- * semantic markup.
38
- */
39
- classes?: BlocksToHtmlOptions['classes'];
40
- /** Document direction: set as `dir` on the wrapper, and each block follows its own text. */
41
- direction?: BlocksToHtmlOptions['direction'];
42
- }
43
-
44
- /**
45
- * Display a saved Blok document synchronously — one `<div>` wrapper, no
46
- * editor chrome, no async, no effects. Content is mapped from the sanitized
47
- * view tree to real React elements (never `dangerouslySetInnerHTML`), so it
48
- * renders identically under SSR.
49
- *
50
- * This is the intended read-only path — reach for it instead of mounting the
51
- * full editing runtime (`BlokEditor readOnly`), which ships toolbar, history,
52
- * and mutation machinery to every viewer. Pair with `defineBlokSchema` to
53
- * display custom-tool content under the exact sanitization that produced it.
54
- *
55
- * The props API is semver-stable. Only the raw `ViewNode` tree that the
56
- * bindings map from (via `blocksToViewNodes`) remains `@experimental` — using
57
- * `BlokView`/`useBlokView` never exposes you to it.
58
- */
59
- export const BlokView = ({
60
- data,
61
- schema,
62
- renderers,
63
- onUnknownBlock,
64
- toolAttributes,
65
- blockIds,
66
- transformUrl,
67
- inlineRenderers,
68
- pageHref,
69
- pageInfo,
70
- classes,
71
- direction,
72
- ...divProps
73
- }: BlokViewProps): ReactNode => {
74
- /**
75
- * `classes: true` by default — this component's job is to look like a
76
- * read-only editor render, and it already owns a wrapper element, so the
77
- * per-block scaffolding parity needs is not a surprise here. (The bare
78
- * `useBlokView` hook defaults it OFF, because its contract is to emit no
79
- * wrapper at all.) Pass `classes={false}` for unstyled semantic markup.
80
- */
81
- const content = useBlokView(data, {
82
- schema,
83
- renderers,
84
- onUnknownBlock,
85
- toolAttributes,
86
- blockIds,
87
- transformUrl,
88
- inlineRenderers,
89
- pageHref,
90
- pageInfo,
91
- classes: classes ?? true,
92
- direction,
93
- });
94
-
95
- /**
96
- * `data-blok-interface="view"` makes the wrapper a soft isolation root, which
97
- * is what gives the rendered blocks the same appearance as a read-only
98
- * editor: the scoped preflight and the token/colour layers key on the bare
99
- * `[data-blok-interface]` attribute, so classes emitted outside it compute
100
- * differently. The value is `view`, never `blok` — the latter carries
101
- * `all: initial !important`, which would block host typography entirely.
102
- *
103
- * Written BEFORE the `divProps` spread so a caller can still override it.
104
- */
105
- return <div data-blok-interface="view" dir={direction} {...divProps}>{content}</div>;
106
- };