@octanejs/blocknote 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Gannaway and Octane contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,142 @@
1
+ # @octanejs/blocknote
2
+
3
+ Headless [BlockNote](https://www.blocknotejs.org) editor for Octane. It covers the editor surface of `@blocknote/react@0.53.0` without the default UI, and uses `@blocknote/core` unchanged.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @octanejs/blocknote
9
+ pnpm add @octanejs/blocknote
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ ```tsx
15
+ import { BlockNoteViewRaw, useCreateBlockNote } from '@octanejs/blocknote';
16
+
17
+ export function Editor() @{
18
+ const editor = useCreateBlockNote({
19
+ initialContent: [{ type: 'paragraph', content: 'Hello' }],
20
+ });
21
+
22
+ <BlockNoteViewRaw editor={editor} onChange={() => console.log(editor.document)} />
23
+ }
24
+ ```
25
+
26
+ `BlockNoteViewRaw` imports `@blocknote/core/style.css`. Toolbars, menus, and other UI are yours to build. Render them as children, read the editor with `useBlockNoteEditor()`, and use `renderEditor={false}` with `<BlockNoteViewEditor />` to control where the editable area goes.
27
+
28
+ ## Exports
29
+
30
+ - `BlockNoteViewRaw`, `BlockNoteViewEditor`, `BlockNoteViewProps`
31
+ - `BlockNoteContext`, `useBlockNoteContext`, `BlockNoteContextValue`
32
+ - `useCreateBlockNote`, `useBlockNoteEditor`
33
+ - `useEditorChange`, `useEditorSelectionChange`, `usePrefersColorScheme`
34
+ - `PortalElementsMap`, `PortalTarget`
35
+
36
+ ## Building UI
37
+
38
+ This package has no default UI. Each BlockNote menu and toolbar is a framework-neutral `@blocknote/core` extension that exposes a store. The store has `subscribe(listener)` and `state`, so Octane's own `useSyncExternalStore` can read it without any extra dependency.
39
+
40
+ ### Menus and toolbars
41
+
42
+ ```tsx
43
+ import { SuggestionMenu, filterSuggestionItems, getDefaultSlashMenuItems } from '@blocknote/core';
44
+ import { useBlockNoteEditor } from '@octanejs/blocknote';
45
+ import { useEffect, useSyncExternalStore } from 'octane';
46
+
47
+ export function SlashMenu() @{
48
+ const editor = useBlockNoteEditor();
49
+ const menu = editor.getExtension(SuggestionMenu)!;
50
+
51
+ useEffect(() => {
52
+ menu.addSuggestionMenu({ triggerCharacter: '/' });
53
+ return () => menu.removeSuggestionMenu('/');
54
+ }, [menu]);
55
+
56
+ const state = useSyncExternalStore(menu.store.subscribe, () => menu.store.state);
57
+ const items = state?.show
58
+ ? filterSuggestionItems(getDefaultSlashMenuItems(editor), state.query)
59
+ : [];
60
+
61
+ @if (state?.show) {
62
+ <ul
63
+ class="slash-menu"
64
+ style={{ position: 'fixed', left: state.referencePos.left, top: state.referencePos.bottom }}
65
+ >
66
+ @for (const item of items; key item.key) {
67
+ <li
68
+ onMouseDown={(event: MouseEvent) => {
69
+ event.preventDefault(); // keep focus in the editor
70
+ menu.clearQuery();
71
+ menu.closeMenu();
72
+ item.onItemClick();
73
+ }}
74
+ >
75
+ {item.title}
76
+ </li>
77
+ }
78
+ </ul>
79
+ }
80
+ }
81
+
82
+ // <BlockNoteViewRaw editor={editor}><SlashMenu /></BlockNoteViewRaw>
83
+ ```
84
+
85
+ `FormattingToolbar`, `SideMenu`, `LinkToolbar`, `FilePanel`, and `TableHandles` follow the same pattern. For collision-aware placement, position the element from `referencePos` with a library such as `@octanejs/floating-ui`.
86
+
87
+ The extension stores are TanStack Stores. If your app already uses `@octanejs/tanstack-store`, `useStore(menu.store, (state) => state?.query)` also works and adds selector-based updates.
88
+
89
+ ### Custom blocks
90
+
91
+ Core's `createBlockSpec` renders to DOM. To render an Octane component inside a block, mount a root on a node you own and unmount it in `destroy`:
92
+
93
+ ```tsx
94
+ import { BlockNoteSchema, createBlockSpec, defaultBlockSpecs } from '@blocknote/core';
95
+ import { createRoot } from 'octane';
96
+
97
+ function CalloutIcon(props: { emoji: string }) @{
98
+ <span class="callout-icon">{props.emoji}</span>
99
+ }
100
+
101
+ const Callout = createBlockSpec(
102
+ { type: 'callout', propSchema: { emoji: { default: '💡' } }, content: 'inline' },
103
+ {
104
+ render: (block) => {
105
+ const dom = document.createElement('div');
106
+ const icon = document.createElement('span');
107
+ const contentDOM = document.createElement('div');
108
+ dom.className = 'callout';
109
+ icon.contentEditable = 'false';
110
+ dom.append(icon, contentDOM);
111
+ const root = createRoot(icon);
112
+ root.render(CalloutIcon, { emoji: block.props.emoji });
113
+ return { dom, contentDOM, destroy: () => root.unmount() };
114
+ },
115
+ },
116
+ );
117
+
118
+ export const schema = BlockNoteSchema.create({
119
+ blockSpecs: { ...defaultBlockSpecs, callout: Callout() },
120
+ });
121
+
122
+ // useCreateBlockNote({ schema })
123
+ ```
124
+
125
+ Each block root is separate from your app tree, so it does not see your contexts (theme, router, stores). Pass what it needs through props or module state. Context-aware block and inline content specs are planned.
126
+
127
+ Both recipes run as real-browser tests in `tests/browser/harness`.
128
+
129
+ ## Server rendering
130
+
131
+ Server rendering is supported. The server emits the view shell, and the editor mounts into it on the client after hydration. Document content is not part of the server HTML. Without a `theme` prop the server renders the light scheme, and the system scheme applies after hydration. Pass `theme` to fix it on both sides.
132
+
133
+ ## Known differences
134
+
135
+ - No default UI. `BlockNoteView`, `BlockNoteDefaultUI`, `ComponentsContext`, and the toolbar, menu, side-menu, table-handle, and comment components are not provided. Upstream disables all of them when no components context exists, so `BlockNoteViewRaw` here matches that upstream configuration.
136
+ - `BlockNoteViewRaw` does not accept the default UI flags (`formattingToolbar`, `slashMenu`, and so on). `portalElements` reads only `default`.
137
+ - Custom React block, inline content, and style specs are not provided.
138
+ - Event handlers receive native DOM events, as everywhere in Octane.
139
+
140
+ ## Provenance
141
+
142
+ Independently authored and MIT licensed. See [UPSTREAM.md](./UPSTREAM.md).
package/UPSTREAM.md ADDED
@@ -0,0 +1,41 @@
1
+ # Upstream provenance
2
+
3
+ `@octanejs/blocknote` provides the headless editor surface of `@blocknote/react@0.53.0` for Octane.
4
+
5
+ | Field | Pin |
6
+ | ------------------------ | ----------------------------------------------------------------------------------------------------- |
7
+ | Package | `@blocknote/react` |
8
+ | Version | `0.53.0` exactly |
9
+ | Repository | `https://github.com/TypeCellOS/BlockNote` |
10
+ | Release commit (gitHead) | `d5e8c96e2ad5f6832da60bc3c9acd3f6c16c25c5` |
11
+ | npm integrity | `sha512-W8HHfKnt3mf2ugb1n9XAsjR7XAAX4GdZl+SE3h4DPcHdTnpfQCX7B2+tIP6CFKm6jW+BUZ2Y2fpDrLgJEtGtGw==` |
12
+ | Framework-neutral core | `@blocknote/core@0.53.0` (runtime dependency, MPL-2.0, not vendored) |
13
+
14
+ ## Source boundary and licensing
15
+
16
+ `@blocknote/react` is licensed MPL-2.0, which is outside this repository's
17
+ approved copy list (MIT, Unlicense, BSD-3-Clause, Apache-2.0). No upstream
18
+ source, declaration, stylesheet, or test is vendored, copied, or adapted here.
19
+
20
+ The Octane adapter is independently authored and MIT licensed. It was written
21
+ from a behavior and API contract derived from BlockNote's public documentation
22
+ and public API names, by an implementer who did not read the `@blocknote/react`
23
+ source. `@blocknote/core` stays an ordinary MPL-2.0 dependency and supplies the
24
+ editor, schemas, and `@blocknote/core/style.css`.
25
+
26
+ An earlier private scaffold of this package was generated by mechanically
27
+ copying upstream source. That scaffold was removed in full before this surface
28
+ was written.
29
+
30
+ ## Supported surface
31
+
32
+ Headless only: `BlockNoteViewRaw`, `BlockNoteViewEditor`, `BlockNoteContext`,
33
+ `useBlockNoteContext`, `useCreateBlockNote`, `useBlockNoteEditor`,
34
+ `useEditorChange`, `useEditorSelectionChange`, and `usePrefersColorScheme`.
35
+ This matches upstream `BlockNoteViewRaw` rendered without a components context,
36
+ where every default UI element is disabled.
37
+
38
+ Not provided: the default UI (`BlockNoteView` from UI-kit packages,
39
+ `BlockNoteDefaultUI`, `ComponentsContext`, toolbars, menus, side menu, table
40
+ handles, comments), custom React block/inline/style specs, and the
41
+ `@blocknote/react/style.css` stylesheet.
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@octanejs/blocknote",
3
+ "devDependencies": {
4
+ "@blocknote/react": "0.53.0",
5
+ "@tsrx/react": "^0.3.7",
6
+ "esbuild": "^0.28.1",
7
+ "react": "19.2.7",
8
+ "react-dom": "19.2.7",
9
+ "vitest": "^4.1.10",
10
+ "octane": "0.5.0"
11
+ },
12
+ "version": "0.1.0",
13
+ "license": "MIT",
14
+ "type": "module",
15
+ "engines": {
16
+ "node": ">=22.22.2"
17
+ },
18
+ "description": "BlockNote for the octane renderer — an Octane binding for @blocknote/react block-based rich text editors.",
19
+ "author": {
20
+ "name": "Dominic Gannaway",
21
+ "email": "dg@domgan.com"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/octanejs/octane.git",
29
+ "directory": "packages/blocknote"
30
+ },
31
+ "main": "src/index.ts",
32
+ "module": "src/index.ts",
33
+ "types": "src/index.ts",
34
+ "files": [
35
+ "src",
36
+ "README.md",
37
+ "UPSTREAM.md",
38
+ "LICENSE"
39
+ ],
40
+ "sideEffects": false,
41
+ "exports": {
42
+ ".": "./src/index.ts",
43
+ "./package.json": "./package.json"
44
+ },
45
+ "dependencies": {
46
+ "@blocknote/core": "0.53.0"
47
+ },
48
+ "peerDependencies": {
49
+ "octane": "^0.5.0"
50
+ },
51
+ "octane": {
52
+ "hookSlots": {
53
+ "manual": [
54
+ "src"
55
+ ]
56
+ }
57
+ },
58
+ "scripts": {
59
+ "typecheck": "tsrx-tsc --noEmit -p tsconfig.supported.json",
60
+ "test": "vitest run --root ../.. --project blocknote"
61
+ }
62
+ }
@@ -0,0 +1,39 @@
1
+ // Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
2
+ import type {
3
+ BlockNoteEditor,
4
+ BlockNoteSchema,
5
+ BlockSchema,
6
+ DefaultBlockSchema,
7
+ DefaultInlineContentSchema,
8
+ DefaultStyleSchema,
9
+ InlineContentSchema,
10
+ StyleSchema,
11
+ } from '@blocknote/core';
12
+ import { createContext, useContext } from 'octane';
13
+
14
+ /** The value a `BlockNoteViewRaw` shares with its descendants. */
15
+ export type BlockNoteContextValue<
16
+ B extends BlockSchema = DefaultBlockSchema,
17
+ I extends InlineContentSchema = DefaultInlineContentSchema,
18
+ S extends StyleSchema = DefaultStyleSchema,
19
+ > = {
20
+ editor?: BlockNoteEditor<B, I, S>;
21
+ colorSchemePreference?: 'light' | 'dark';
22
+ setContentEditableProps?: (props: Record<string, any> | undefined) => void;
23
+ };
24
+
25
+ export const BlockNoteContext = createContext<BlockNoteContextValue<any, any, any> | undefined>(
26
+ undefined,
27
+ );
28
+
29
+ /**
30
+ * Read the nearest BlockNote context. The optional schema only narrows the
31
+ * returned editor type; it is not checked at runtime.
32
+ */
33
+ export function useBlockNoteContext<
34
+ B extends BlockSchema = DefaultBlockSchema,
35
+ I extends InlineContentSchema = DefaultInlineContentSchema,
36
+ S extends StyleSchema = DefaultStyleSchema,
37
+ >(_schema?: BlockNoteSchema<B, I, S>): BlockNoteContextValue<B, I, S> | undefined {
38
+ return useContext(BlockNoteContext) as BlockNoteContextValue<B, I, S> | undefined;
39
+ }
@@ -0,0 +1,188 @@
1
+ // Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
2
+ import '@blocknote/core/style.css';
3
+
4
+ import type {
5
+ BlockNoteEditor,
6
+ BlockSchema,
7
+ DefaultBlockSchema,
8
+ DefaultInlineContentSchema,
9
+ DefaultStyleSchema,
10
+ InlineContentSchema,
11
+ StyleSchema,
12
+ } from '@blocknote/core';
13
+ import {
14
+ createContext,
15
+ type OctaneNode,
16
+ useCallback,
17
+ useContext,
18
+ useEffect,
19
+ useMemo,
20
+ useState,
21
+ } from 'octane';
22
+ import type { Octane } from 'octane/jsx-runtime';
23
+
24
+ import {
25
+ BlockNoteContext,
26
+ type BlockNoteContextValue,
27
+ useBlockNoteContext,
28
+ } from './BlockNoteContext';
29
+ import { useEditorChange } from './hooks/useEditorChange';
30
+ import { useEditorSelectionChange } from './hooks/useEditorSelectionChange';
31
+ import { usePrefersColorScheme } from './hooks/usePrefersColorScheme';
32
+ import { type PortalElementsMap, resolvePortalTarget } from './portalTarget';
33
+
34
+ export type BlockNoteViewProps<
35
+ B extends BlockSchema = DefaultBlockSchema,
36
+ I extends InlineContentSchema = DefaultInlineContentSchema,
37
+ S extends StyleSchema = DefaultStyleSchema,
38
+ > =
39
+ {
40
+ editor: BlockNoteEditor<B, I, S>;
41
+ theme?: 'light' | 'dark';
42
+ /** @default true */
43
+ editable?: boolean;
44
+ onChange?: Parameters<BlockNoteEditor<B, I, S>['onChange']>[0];
45
+ onSelectionChange?: () => void;
46
+ /**
47
+ * Render the editable surface as the container's first child. Set to
48
+ * `false` to place `BlockNoteViewEditor` among `children` yourself.
49
+ * @default true
50
+ */
51
+ renderEditor?: boolean;
52
+ autoFocus?: boolean;
53
+ portalElements?: PortalElementsMap;
54
+ className?: string;
55
+ children?: OctaneNode;
56
+ ref?: React.Ref<HTMLDivElement>;
57
+ } & Omit<
58
+ Octane.HTMLAttributes<HTMLDivElement>,
59
+ 'onChange' | 'onSelectionChange' | 'children' | 'class' | 'className'
60
+ >;
61
+
62
+ /** What `BlockNoteViewRaw` hands to the `BlockNoteViewEditor` it contains. */
63
+ type ViewEditorSettings = {
64
+ autoFocus: boolean | undefined;
65
+ editable: boolean | undefined;
66
+ portalTarget: HTMLElement | null;
67
+ contentEditableProps: Record<string, any> | undefined;
68
+ };
69
+
70
+ const ViewEditorContext = createContext<ViewEditorSettings | undefined>(undefined);
71
+
72
+ const ignore = () => {};
73
+
74
+ export function BlockNoteViewRaw<
75
+ B extends BlockSchema = DefaultBlockSchema,
76
+ I extends InlineContentSchema = DefaultInlineContentSchema,
77
+ S extends StyleSchema = DefaultStyleSchema,
78
+ >(props: BlockNoteViewProps<B, I, S>) @{
79
+ const {
80
+ editor,
81
+ theme,
82
+ editable,
83
+ onChange,
84
+ onSelectionChange,
85
+ renderEditor,
86
+ autoFocus,
87
+ portalElements,
88
+ className,
89
+ children,
90
+ ref,
91
+ ...rest
92
+ } = props;
93
+
94
+ const outerContext = useBlockNoteContext();
95
+ const systemColorScheme = usePrefersColorScheme();
96
+ const scheme: 'light' | 'dark' =
97
+ theme ?? outerContext?.colorSchemePreference ??
98
+ (systemColorScheme === 'dark' ? 'dark' : 'light');
99
+
100
+ const [contentEditableProps, setContentEditableProps] = useState<Record<string, any> | undefined>(
101
+ undefined,
102
+ );
103
+
104
+ useEditorChange((onChange ?? ignore) as Parameters<typeof useEditorChange>[0], editor);
105
+ useEditorSelectionChange(onSelectionChange ?? ignore, editor);
106
+
107
+ useEffect(() => {
108
+ const portalElement = editor.portalElement;
109
+ if (!portalElement) {
110
+ throw new Error('Portal element not found');
111
+ }
112
+
113
+ portalElement.className = className ? `bn-root ${scheme} ${className}` : `bn-root ${scheme}`;
114
+ portalElement.setAttribute('data-color-scheme', scheme);
115
+ }, [editor, scheme, className]);
116
+
117
+ const portalTargetOption = portalElements?.default;
118
+ const portalTarget = useMemo(() => resolvePortalTarget(portalTargetOption), [portalTargetOption]);
119
+
120
+ const contextValue = useMemo<BlockNoteContextValue<B, I, S>>(
121
+ () => ({
122
+ ...outerContext,
123
+ editor,
124
+ colorSchemePreference: scheme,
125
+ setContentEditableProps,
126
+ }),
127
+ [outerContext, editor, scheme, setContentEditableProps],
128
+ );
129
+
130
+ const settings = useMemo<ViewEditorSettings>(
131
+ () => ({ autoFocus, editable, portalTarget, contentEditableProps }),
132
+ [autoFocus, editable, portalTarget, contentEditableProps],
133
+ );
134
+
135
+ <BlockNoteContext value={contextValue}>
136
+ <ViewEditorContext value={settings}>
137
+ <div
138
+ {...rest}
139
+ class={['bn-root bn-container', scheme, className]}
140
+ data-color-scheme={scheme}
141
+ ref={ref ?? null}
142
+ >
143
+ @if (renderEditor !== false) {
144
+ <BlockNoteViewEditor>{children}</BlockNoteViewEditor>
145
+ } @else {
146
+ <>
147
+ {children}
148
+ </>
149
+ }
150
+ </div>
151
+ </ViewEditorContext>
152
+ </BlockNoteContext>
153
+ }
154
+
155
+ /** The editable surface. Renders only inside a `BlockNoteViewRaw`. */
156
+ export function BlockNoteViewEditor(props: { children?: OctaneNode }) @{
157
+ const settings = useContext(ViewEditorContext);
158
+ const editor = useBlockNoteContext()?.editor;
159
+
160
+ if (!settings || !editor) {
161
+ throw new Error('BlockNoteViewEditor must be rendered inside a BlockNoteViewRaw component');
162
+ }
163
+
164
+ const { autoFocus, editable, portalTarget, contentEditableProps } = settings;
165
+
166
+ // A new callback identity detaches the old ref (unmount) and attaches the new
167
+ // one (mount). Remounting on `editable` changes is deliberate: TipTap drops the
168
+ // host's tabIndex when editability flips on a mounted view.
169
+ const mountEditor = useCallback((element: HTMLDivElement | null) => {
170
+ editor.isEditable = editable !== false;
171
+ if (element) {
172
+ editor.mount(element, { portalTarget });
173
+ } else {
174
+ editor.unmount();
175
+ }
176
+ }, [editable, editor, portalTarget]);
177
+
178
+ <>
179
+ <div
180
+ aria-autocomplete="list"
181
+ aria-haspopup="listbox"
182
+ data-bn-autofocus={autoFocus}
183
+ {...contentEditableProps}
184
+ ref={mountEditor}
185
+ />
186
+ {props.children}
187
+ </>
188
+ }
@@ -0,0 +1,33 @@
1
+ // Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
2
+ import type {
3
+ BlockNoteEditor,
4
+ BlockNoteSchema,
5
+ BlockSchema,
6
+ DefaultBlockSchema,
7
+ DefaultInlineContentSchema,
8
+ DefaultStyleSchema,
9
+ InlineContentSchema,
10
+ StyleSchema,
11
+ } from '@blocknote/core';
12
+
13
+ import { useBlockNoteContext } from '../BlockNoteContext';
14
+ import { splitSlot } from '../internal';
15
+
16
+ /** Read the editor from the nearest `BlockNoteViewRaw` or `BlockNoteContext`. */
17
+ export function useBlockNoteEditor<
18
+ B extends BlockSchema = DefaultBlockSchema,
19
+ I extends InlineContentSchema = DefaultInlineContentSchema,
20
+ S extends StyleSchema = DefaultStyleSchema,
21
+ >(schema?: BlockNoteSchema<B, I, S>): BlockNoteEditor<B, I, S>;
22
+ export function useBlockNoteEditor(...args: unknown[]): BlockNoteEditor<any, any, any> {
23
+ const [userArgs] = splitSlot(args);
24
+ const context = useBlockNoteContext(userArgs[0] as BlockNoteSchema<any, any, any> | undefined);
25
+
26
+ if (!context?.editor) {
27
+ throw new Error(
28
+ 'useBlockNoteEditor was called outside of a BlockNoteContext provider or BlockNoteViewRaw component',
29
+ );
30
+ }
31
+
32
+ return context.editor;
33
+ }
@@ -0,0 +1,37 @@
1
+ // Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
2
+ import {
3
+ BlockNoteEditor,
4
+ type BlockNoteEditorOptions,
5
+ type CustomBlockNoteSchema,
6
+ type DefaultBlockSchema,
7
+ type DefaultInlineContentSchema,
8
+ type DefaultStyleSchema,
9
+ } from '@blocknote/core';
10
+ import { useMemo } from 'octane';
11
+
12
+ import { splitSlot, subSlot } from '../internal';
13
+
14
+ type DependencyList = readonly unknown[];
15
+
16
+ const noDeps: DependencyList = [];
17
+
18
+ /** Create a BlockNote editor once, or again whenever `deps` change. */
19
+ export function useCreateBlockNote<
20
+ Options extends Partial<BlockNoteEditorOptions<any, any, any>> | undefined,
21
+ >(
22
+ options?: Options,
23
+ deps?: DependencyList,
24
+ ): Options extends { schema: CustomBlockNoteSchema<infer B, infer I, infer S> }
25
+ ? BlockNoteEditor<B, I, S>
26
+ : BlockNoteEditor<DefaultBlockSchema, DefaultInlineContentSchema, DefaultStyleSchema>;
27
+ export function useCreateBlockNote(...args: unknown[]): BlockNoteEditor<any, any, any> {
28
+ const [userArgs, slot] = splitSlot(args);
29
+ const options = userArgs[0] as Partial<BlockNoteEditorOptions<any, any, any>> | undefined;
30
+ const deps = (userArgs[1] as DependencyList | undefined) ?? noDeps;
31
+
32
+ return useMemo(
33
+ () => BlockNoteEditor.create(options ?? {}),
34
+ deps as unknown[],
35
+ subSlot(slot, 'create'),
36
+ );
37
+ }
@@ -0,0 +1,28 @@
1
+ // Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
2
+ import type { BlockNoteEditor } from '@blocknote/core';
3
+ import { useEffect } from 'octane';
4
+
5
+ import { useBlockNoteContext } from '../BlockNoteContext';
6
+ import { splitSlot, subSlot } from '../internal';
7
+
8
+ type ChangeCallback = Parameters<BlockNoteEditor<any, any, any>['onChange']>[0];
9
+
10
+ /** Subscribe to content changes of `editor`, or of the context editor when omitted. */
11
+ export function useEditorChange(
12
+ callback: ChangeCallback,
13
+ editor?: BlockNoteEditor<any, any, any>,
14
+ ): void;
15
+ export function useEditorChange(...args: unknown[]): void {
16
+ const [userArgs, slot] = splitSlot(args);
17
+ const callback = userArgs[0] as ChangeCallback;
18
+ const context = useBlockNoteContext();
19
+ const editor = (userArgs[1] as BlockNoteEditor<any, any, any> | undefined) ?? context?.editor;
20
+
21
+ if (!editor) {
22
+ throw new Error(
23
+ 'useEditorChange was called outside of a BlockNoteContext provider or BlockNoteViewRaw component',
24
+ );
25
+ }
26
+
27
+ useEffect(() => editor.onChange(callback), [editor, callback], subSlot(slot, 'subscribe'));
28
+ }
@@ -0,0 +1,32 @@
1
+ // Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
2
+ import type { BlockNoteEditor } from '@blocknote/core';
3
+ import { useEffect } from 'octane';
4
+
5
+ import { useBlockNoteContext } from '../BlockNoteContext';
6
+ import { splitSlot, subSlot } from '../internal';
7
+
8
+ /** Subscribe to selection changes of `editor`, or of the context editor when omitted. */
9
+ export function useEditorSelectionChange(
10
+ callback: () => void,
11
+ editor?: BlockNoteEditor<any, any, any>,
12
+ includeSelectionChangedByRemote?: boolean,
13
+ ): void;
14
+ export function useEditorSelectionChange(...args: unknown[]): void {
15
+ const [userArgs, slot] = splitSlot(args);
16
+ const callback = userArgs[0] as () => void;
17
+ const includeSelectionChangedByRemote = userArgs[2] as boolean | undefined;
18
+ const context = useBlockNoteContext();
19
+ const editor = (userArgs[1] as BlockNoteEditor<any, any, any> | undefined) ?? context?.editor;
20
+
21
+ if (!editor) {
22
+ throw new Error(
23
+ 'useEditorSelectionChange was called outside of a BlockNoteContext provider or BlockNoteViewRaw component',
24
+ );
25
+ }
26
+
27
+ useEffect(
28
+ () => editor.onSelectionChange(callback, includeSelectionChangedByRemote),
29
+ [editor, callback, includeSelectionChangedByRemote],
30
+ subSlot(slot, 'subscribe'),
31
+ );
32
+ }
@@ -0,0 +1,54 @@
1
+ // Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
2
+ import { useSyncExternalStore } from 'octane';
3
+
4
+ import { splitSlot, subSlot } from '../internal';
5
+
6
+ export type ColorSchemePreference = 'dark' | 'light' | 'no-preference';
7
+
8
+ const darkQuery = '(prefers-color-scheme: dark)';
9
+ const lightQuery = '(prefers-color-scheme: light)';
10
+
11
+ function readPreference(): ColorSchemePreference {
12
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
13
+ return 'no-preference';
14
+ }
15
+
16
+ if (window.matchMedia(darkQuery).matches) {
17
+ return 'dark';
18
+ }
19
+
20
+ return window.matchMedia(lightQuery).matches ? 'light' : 'no-preference';
21
+ }
22
+
23
+ function subscribe(onChange: () => void): () => void {
24
+ if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
25
+ return () => {};
26
+ }
27
+
28
+ const queries = [window.matchMedia(darkQuery), window.matchMedia(lightQuery)];
29
+ for (const query of queries) {
30
+ query.addEventListener('change', onChange);
31
+ }
32
+ return () => {
33
+ for (const query of queries) {
34
+ query.removeEventListener('change', onChange);
35
+ }
36
+ };
37
+ }
38
+
39
+ // The server cannot see the media query. Hydration renders this snapshot too,
40
+ // so server HTML and the first client render agree; the store then re-reads
41
+ // the live preference after hydration.
42
+ const serverPreference = (): ColorSchemePreference => 'no-preference';
43
+
44
+ /** Track the system `prefers-color-scheme` media query. */
45
+ export function usePrefersColorScheme(): ColorSchemePreference;
46
+ export function usePrefersColorScheme(...args: unknown[]): ColorSchemePreference {
47
+ const [, slot] = splitSlot(args);
48
+ return useSyncExternalStore(
49
+ subscribe,
50
+ readPreference,
51
+ serverPreference,
52
+ subSlot(slot, 'preference'),
53
+ );
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ // Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
2
+ export {
3
+ BlockNoteContext,
4
+ useBlockNoteContext,
5
+ type BlockNoteContextValue,
6
+ } from './BlockNoteContext';
7
+ export {
8
+ BlockNoteViewRaw,
9
+ BlockNoteViewEditor,
10
+ type BlockNoteViewProps,
11
+ } from './BlockNoteViewRaw.tsrx';
12
+ export type { PortalElementsMap, PortalTarget } from './portalTarget';
13
+ export { useCreateBlockNote } from './hooks/useCreateBlockNote';
14
+ export { useBlockNoteEditor } from './hooks/useBlockNoteEditor';
15
+ export { useEditorChange } from './hooks/useEditorChange';
16
+ export { useEditorSelectionChange } from './hooks/useEditorSelectionChange';
17
+ export { usePrefersColorScheme } from './hooks/usePrefersColorScheme';
@@ -0,0 +1,18 @@
1
+ // Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
2
+ // Slot mechanics for @octanejs/blocknote's plain-`.ts` hooks. The Octane
3
+ // compiler appends a per-call-site Symbol as the final argument of every
4
+ // custom-hook call; these helpers separate it from the public arguments and
5
+ // derive one stable sub-slot per base hook a binding hook composes.
6
+
7
+ import { createSubSlot } from 'octane';
8
+
9
+ export const subSlot = createSubSlot({
10
+ tagPrefix: ':@octanejs/blocknote:',
11
+ slotlessPrefix: '@octanejs/blocknote:',
12
+ });
13
+
14
+ /** Split a compiler-owned trailing slot from a custom hook's user arguments. */
15
+ export function splitSlot(args: readonly unknown[]): [readonly unknown[], symbol | undefined] {
16
+ const tail = args.at(-1);
17
+ return typeof tail === 'symbol' ? [args.slice(0, -1), tail] : [args, undefined];
18
+ }
@@ -0,0 +1,33 @@
1
+ // Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
2
+ /** Where floating UI portals mount: an element, a CSS selector, or `null` for `document.body`. */
3
+ export type PortalTarget = HTMLElement | string | null;
4
+
5
+ export type PortalElementsMap = {
6
+ default?: PortalTarget;
7
+ };
8
+
9
+ /**
10
+ * Resolve a public portal target to the element `editor.mount` expects.
11
+ * `undefined` yields `null`, which lets `mount` pick its own default.
12
+ */
13
+ export function resolvePortalTarget(target: PortalTarget | undefined): HTMLElement | null {
14
+ if (target === undefined || typeof document === 'undefined') {
15
+ return null;
16
+ }
17
+
18
+ if (target === null) {
19
+ return document.body;
20
+ }
21
+
22
+ if (typeof target !== 'string') {
23
+ return target;
24
+ }
25
+
26
+ const element = document.querySelector<HTMLElement>(target);
27
+ if (element === null) {
28
+ console.warn(
29
+ `@octanejs/blocknote: portal target "${target}" matched no element; using the default portal target.`,
30
+ );
31
+ }
32
+ return element;
33
+ }