@aionbuilders/nabu 0.1.0-alpha.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/README.md +131 -0
- package/dist/behaviors/index.d.ts +1 -0
- package/dist/behaviors/index.js +1 -0
- package/dist/behaviors/text/RichText.svelte +33 -0
- package/dist/behaviors/text/RichText.svelte.d.ts +11 -0
- package/dist/behaviors/text/index.d.ts +3 -0
- package/dist/behaviors/text/index.js +3 -0
- package/dist/behaviors/text/rich-text.extension.d.ts +2 -0
- package/dist/behaviors/text/rich-text.extension.js +75 -0
- package/dist/behaviors/text/text.behavior.svelte.d.ts +103 -0
- package/dist/behaviors/text/text.behavior.svelte.js +346 -0
- package/dist/blocks/Block.svelte +18 -0
- package/dist/blocks/Block.svelte.d.ts +11 -0
- package/dist/blocks/Nabu.svelte +31 -0
- package/dist/blocks/Nabu.svelte.d.ts +12 -0
- package/dist/blocks/block.svelte.d.ts +143 -0
- package/dist/blocks/block.svelte.js +364 -0
- package/dist/blocks/container.utils.d.ts +28 -0
- package/dist/blocks/container.utils.js +114 -0
- package/dist/blocks/heading/Heading.svelte +42 -0
- package/dist/blocks/heading/Heading.svelte.d.ts +11 -0
- package/dist/blocks/heading/heading.svelte.d.ts +45 -0
- package/dist/blocks/heading/heading.svelte.js +94 -0
- package/dist/blocks/heading/hooks/onBeforeInput.hook.d.ts +3 -0
- package/dist/blocks/heading/hooks/onBeforeInput.hook.js +58 -0
- package/dist/blocks/heading/index.d.ts +7 -0
- package/dist/blocks/heading/index.js +41 -0
- package/dist/blocks/index.d.ts +10 -0
- package/dist/blocks/index.js +12 -0
- package/dist/blocks/list/List.svelte +25 -0
- package/dist/blocks/list/List.svelte.d.ts +11 -0
- package/dist/blocks/list/ListItem.svelte +45 -0
- package/dist/blocks/list/ListItem.svelte.d.ts +11 -0
- package/dist/blocks/list/index.d.ts +10 -0
- package/dist/blocks/list/index.js +41 -0
- package/dist/blocks/list/list-item.svelte.d.ts +50 -0
- package/dist/blocks/list/list-item.svelte.js +213 -0
- package/dist/blocks/list/list.behavior.svelte.d.ts +23 -0
- package/dist/blocks/list/list.behavior.svelte.js +61 -0
- package/dist/blocks/list/list.svelte.d.ts +39 -0
- package/dist/blocks/list/list.svelte.js +139 -0
- package/dist/blocks/megablock.svelte.d.ts +13 -0
- package/dist/blocks/megablock.svelte.js +64 -0
- package/dist/blocks/nabu.svelte.d.ts +121 -0
- package/dist/blocks/nabu.svelte.js +395 -0
- package/dist/blocks/paragraph/Paragraph.svelte +38 -0
- package/dist/blocks/paragraph/Paragraph.svelte.d.ts +11 -0
- package/dist/blocks/paragraph/index.d.ts +7 -0
- package/dist/blocks/paragraph/index.js +44 -0
- package/dist/blocks/paragraph/paragraph.svelte.d.ts +41 -0
- package/dist/blocks/paragraph/paragraph.svelte.js +86 -0
- package/dist/blocks/selection.svelte.d.ts +38 -0
- package/dist/blocks/selection.svelte.js +143 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/utils/extensions.d.ts +69 -0
- package/dist/utils/extensions.js +43 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/selection.svelte.d.ts +219 -0
- package/dist/utils/selection.svelte.js +611 -0
- package/package.json +74 -0
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Nabu — Block Editor Engine
|
|
2
|
+
|
|
3
|
+
> **Alpha** — API in active development. Breaking changes expected.
|
|
4
|
+
|
|
5
|
+
Nabu is a modular, local-first block editor engine for **Svelte 5**. It combines the intuitive cross-block selection of document editors (Google Docs, ProseMirror) with the structural flexibility of block editors (Notion), built on a **Single ContentEditable** architecture and a **CRDT core** (Loro).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @aionbuilders/nabu
|
|
11
|
+
# or
|
|
12
|
+
bun add @aionbuilders/nabu
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Required: Vite Setup
|
|
16
|
+
|
|
17
|
+
Nabu depends on `loro-crdt`, which ships as a WebAssembly module. You must configure your Vite bundler to handle it.
|
|
18
|
+
|
|
19
|
+
**1. Install the required Vite plugins:**
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -D vite-plugin-wasm vite-plugin-top-level-await
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**2. Add them to your `vite.config.js`:**
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { defineConfig } from 'vite';
|
|
29
|
+
import { sveltekit } from '@sveltejs/kit/vite'; // or svelte()
|
|
30
|
+
import wasm from 'vite-plugin-wasm';
|
|
31
|
+
import topLevelAwait from 'vite-plugin-top-level-await';
|
|
32
|
+
|
|
33
|
+
export default defineConfig({
|
|
34
|
+
plugins: [
|
|
35
|
+
sveltekit(),
|
|
36
|
+
wasm(),
|
|
37
|
+
topLevelAwait()
|
|
38
|
+
]
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Without this setup, Nabu will fail to load.
|
|
43
|
+
|
|
44
|
+
## Basic Usage
|
|
45
|
+
|
|
46
|
+
```svelte
|
|
47
|
+
<script>
|
|
48
|
+
import {
|
|
49
|
+
Nabu,
|
|
50
|
+
NabuEditor,
|
|
51
|
+
ParagraphExtension,
|
|
52
|
+
HeadingExtension,
|
|
53
|
+
ListExtension,
|
|
54
|
+
ListItemExtension,
|
|
55
|
+
RichTextExtension
|
|
56
|
+
} from '@aionbuilders/nabu';
|
|
57
|
+
|
|
58
|
+
const engine = new Nabu({
|
|
59
|
+
extensions: [
|
|
60
|
+
ParagraphExtension,
|
|
61
|
+
HeadingExtension,
|
|
62
|
+
ListExtension,
|
|
63
|
+
ListItemExtension,
|
|
64
|
+
RichTextExtension
|
|
65
|
+
]
|
|
66
|
+
});
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<NabuEditor {engine} />
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
That's it. The editor is now functional with paragraphs, headings, lists, and rich text formatting.
|
|
73
|
+
|
|
74
|
+
## Extensions
|
|
75
|
+
|
|
76
|
+
All block types and behaviors are provided as extensions. Pass them to the `Nabu` constructor:
|
|
77
|
+
|
|
78
|
+
| Extension | Provides | Keyboard shortcuts |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `ParagraphExtension` | `paragraph` block | — |
|
|
81
|
+
| `HeadingExtension` | `heading` block (h1–h6) | `# ` → h1, `## ` → h2, ... |
|
|
82
|
+
| `ListExtension` | `list` container (bullet/ordered) | — |
|
|
83
|
+
| `ListItemExtension` | `list-item` block | `Tab` indent, `Shift+Tab` unindent |
|
|
84
|
+
| `RichTextExtension` | Inline marks (bold, italic...) | `Ctrl+B/I/U/E`, `Ctrl+Shift+X` |
|
|
85
|
+
|
|
86
|
+
## Programmatic API
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
// Insert blocks
|
|
90
|
+
engine.insert('paragraph', { text: 'Hello' });
|
|
91
|
+
engine.insert('heading', { level: 1, text: 'Title' });
|
|
92
|
+
engine.insert('list', { listType: 'bullet' });
|
|
93
|
+
|
|
94
|
+
// Undo / Redo
|
|
95
|
+
engine.undo();
|
|
96
|
+
engine.redo();
|
|
97
|
+
|
|
98
|
+
// Serialize
|
|
99
|
+
const markdown = engine.serialize('markdown');
|
|
100
|
+
const json = engine.serialize('json');
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Architecture
|
|
104
|
+
|
|
105
|
+
### Single ContentEditable
|
|
106
|
+
The entire editor lives in a single `contenteditable` root. This enables seamless cross-block selection, native copy-paste, and consistent browser performance.
|
|
107
|
+
|
|
108
|
+
### CRDT Core (Loro)
|
|
109
|
+
All state is stored in a [Loro](https://loro.dev) document — a WASM-based CRDT. This provides:
|
|
110
|
+
- **Local-first**: offline editing out of the box
|
|
111
|
+
- **Collaboration-ready**: conflict-free concurrent edits
|
|
112
|
+
- **Rich text**: native delta/mark support via `LoroText`
|
|
113
|
+
|
|
114
|
+
### Extension System
|
|
115
|
+
Every block type is an extension. You can build custom block types by extending the `Block` or `MegaBlock` base classes and registering them via the `extension()` helper.
|
|
116
|
+
|
|
117
|
+
### Svelte 5 Runes
|
|
118
|
+
Nabu uses `$state` and `$derived` throughout for fine-grained reactivity — only modified blocks re-render.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Status
|
|
123
|
+
|
|
124
|
+
This is an **alpha release**. The core editing experience is functional, but some features are still in development:
|
|
125
|
+
|
|
126
|
+
- [ ] Persistence (IndexedDB)
|
|
127
|
+
- [ ] Floating toolbar UI
|
|
128
|
+
- [ ] Slash command menu
|
|
129
|
+
- [ ] Link support
|
|
130
|
+
|
|
131
|
+
Contributions and feedback welcome.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./text";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './text';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/** @type {{ delta: import('loro-crdt').Delta<string>[] }} */
|
|
3
|
+
let { delta } = $props();
|
|
4
|
+
|
|
5
|
+
/** @param {Record<string, any>} attrs */
|
|
6
|
+
function getClasses(attrs) {
|
|
7
|
+
return [
|
|
8
|
+
attrs.bold && 'nabu-bold',
|
|
9
|
+
attrs.italic && 'nabu-italic',
|
|
10
|
+
attrs.underline && 'nabu-underline',
|
|
11
|
+
attrs.code && 'nabu-code',
|
|
12
|
+
attrs.strikethrough && 'nabu-strikethrough',
|
|
13
|
+
].filter(Boolean).join(' ');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const segments = $derived(delta.length ? delta : [{ insert: '\n' }]);
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
{#each segments as op}
|
|
20
|
+
{#if op.attributes && Object.values(op.attributes).some(Boolean)}
|
|
21
|
+
<span class={getClasses(op.attributes)}>{op.insert}</span>
|
|
22
|
+
{:else}
|
|
23
|
+
{op.insert}
|
|
24
|
+
{/if}
|
|
25
|
+
{/each}
|
|
26
|
+
|
|
27
|
+
<style>
|
|
28
|
+
:global(.nabu-bold) { font-weight: bold; }
|
|
29
|
+
:global(.nabu-italic) { font-style: italic; }
|
|
30
|
+
:global(.nabu-underline) { text-decoration: underline; }
|
|
31
|
+
:global(.nabu-strikethrough) { text-decoration: line-through; }
|
|
32
|
+
:global(.nabu-code) { font-family: monospace; background: rgba(135, 131, 120, 0.15); border-radius: 3px; padding: 0.1em 0.3em; }
|
|
33
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default RichText;
|
|
2
|
+
type RichText = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const RichText: import("svelte").Component<{
|
|
7
|
+
delta: import("loro-crdt").Delta<string>[];
|
|
8
|
+
}, {}, "">;
|
|
9
|
+
type $$ComponentProps = {
|
|
10
|
+
delta: import("loro-crdt").Delta<string>[];
|
|
11
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { Nabu } from '../../blocks/nabu.svelte.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { tick } from 'svelte';
|
|
6
|
+
import { Extension } from '../../utils/extensions.js';
|
|
7
|
+
import { TextBehavior } from './text.behavior.svelte.js';
|
|
8
|
+
|
|
9
|
+
/** @type {Record<string, string>} */
|
|
10
|
+
const MARK_SHORTCUTS = {
|
|
11
|
+
b: 'bold',
|
|
12
|
+
i: 'italic',
|
|
13
|
+
u: 'underline',
|
|
14
|
+
e: 'code',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {Nabu} nabu
|
|
19
|
+
* @param {KeyboardEvent} event
|
|
20
|
+
*/
|
|
21
|
+
function onKeyDown(nabu, event) {
|
|
22
|
+
const ctrl = event.ctrlKey || event.metaKey;
|
|
23
|
+
if (!ctrl) return;
|
|
24
|
+
|
|
25
|
+
const endBlock = nabu.selection.endBlock;
|
|
26
|
+
const startBlock = nabu.selection.startBlock;
|
|
27
|
+
const startFocusData = startBlock?.focus(null, true);
|
|
28
|
+
const endFocusData = endBlock?.focus(null, true);
|
|
29
|
+
|
|
30
|
+
let markName = null;
|
|
31
|
+
if (!event.shiftKey) {
|
|
32
|
+
markName = MARK_SHORTCUTS[event.key.toLowerCase()] ?? null;
|
|
33
|
+
} else if (event.key.toLowerCase() === 'x') {
|
|
34
|
+
markName = 'strikethrough';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!markName) return;
|
|
38
|
+
|
|
39
|
+
// Collect all targeted block/selection pairs
|
|
40
|
+
const targets = [];
|
|
41
|
+
for (const [, block] of nabu.blocks) {
|
|
42
|
+
if (!block.selected) continue;
|
|
43
|
+
const behavior = block.behaviors?.get('text');
|
|
44
|
+
if (!(behavior instanceof TextBehavior)) continue;
|
|
45
|
+
const sel = behavior.selection;
|
|
46
|
+
if (!sel || sel.isCollapsed) continue;
|
|
47
|
+
targets.push({ behavior, sel });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!targets.length) return;
|
|
51
|
+
|
|
52
|
+
// Classic editor behavior: remove if mark is fully active across all targets, apply otherwise
|
|
53
|
+
const isFullyActive = targets.every(({ behavior, sel }) => behavior.isMarkActive(markName, sel));
|
|
54
|
+
for (const { behavior, sel } of targets) {
|
|
55
|
+
if (isFullyActive) {
|
|
56
|
+
behavior.removeMark(markName, sel);
|
|
57
|
+
} else {
|
|
58
|
+
behavior.applyMark(markName, true, sel);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const applied = true;
|
|
62
|
+
|
|
63
|
+
if (applied) {
|
|
64
|
+
nabu.commit();
|
|
65
|
+
if (!startBlock || !endBlock) return;
|
|
66
|
+
nabu.focus({ from: { block: startBlock, offset: startFocusData?.start?.offset }, to: { block: endBlock, offset: endFocusData?.end?.offset } });
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
return nabu.BREAK;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const RichTextExtension = new Extension('rich-text', {
|
|
74
|
+
hooks: { onKeyDown }
|
|
75
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a Loro text delta to inline Markdown.
|
|
3
|
+
* @param {import('loro-crdt').Delta<string>[]} delta
|
|
4
|
+
* @returns {string}
|
|
5
|
+
*/
|
|
6
|
+
export function deltaToMarkdown(delta: import("loro-crdt").Delta<string>[]): string;
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {NabuNode<{type: "paragraph", text: LoroText}>} TextNode
|
|
9
|
+
*/
|
|
10
|
+
export class TextBehavior {
|
|
11
|
+
/** @param {Block} block @param {LoroText} [container] */
|
|
12
|
+
constructor(block: Block, container?: LoroText);
|
|
13
|
+
block: Block;
|
|
14
|
+
nabu: import("../..").Nabu;
|
|
15
|
+
/** @type {LoroText} */
|
|
16
|
+
container: LoroText;
|
|
17
|
+
text: string;
|
|
18
|
+
delta: import("loro-crdt").Delta<string>[];
|
|
19
|
+
selection: {
|
|
20
|
+
from: number;
|
|
21
|
+
to: number;
|
|
22
|
+
isCollapsed: boolean;
|
|
23
|
+
direction: "forward" | "backward" | "none";
|
|
24
|
+
} | null;
|
|
25
|
+
/**
|
|
26
|
+
* @param {HTMLElement} element
|
|
27
|
+
* @param {NabuSelection} globalSelection
|
|
28
|
+
*/
|
|
29
|
+
getSelection(element: HTMLElement, globalSelection: NabuSelection): {
|
|
30
|
+
from: number;
|
|
31
|
+
to: number;
|
|
32
|
+
isCollapsed: boolean;
|
|
33
|
+
direction: "forward" | "backward" | "none";
|
|
34
|
+
} | null;
|
|
35
|
+
/**
|
|
36
|
+
* Calcule l'offset textuel d'un point DOM par rapport au début de ce bloc
|
|
37
|
+
* @param {Node} node
|
|
38
|
+
* @param {number} offset
|
|
39
|
+
* @param {HTMLElement?} [element]
|
|
40
|
+
*/
|
|
41
|
+
calculateOffset(node: Node, offset: number, element?: HTMLElement | null): number;
|
|
42
|
+
/** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [selection] */
|
|
43
|
+
handleBeforeInput(event: InputEvent, selection?: ReturnType<TextBehavior["getSelection"]>): any;
|
|
44
|
+
/** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [selection] */
|
|
45
|
+
handleInsertText(event: InputEvent, selection?: ReturnType<TextBehavior["getSelection"]>): void;
|
|
46
|
+
/** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [sel] */
|
|
47
|
+
handleInsertLineBreak(event: InputEvent, sel?: ReturnType<TextBehavior["getSelection"]>): void;
|
|
48
|
+
/** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [sel] */
|
|
49
|
+
handleDeleteContentBackward(event: InputEvent, sel?: ReturnType<TextBehavior["getSelection"]>): void;
|
|
50
|
+
/** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [sel] */
|
|
51
|
+
handleDeleteContentForward(event: InputEvent, sel?: ReturnType<TextBehavior["getSelection"]>): void;
|
|
52
|
+
/** @param {InputEvent} event @param {ReturnType<TextBehavior["getSelection"]>} [sel] */
|
|
53
|
+
handleInsertParagraph(event: InputEvent, sel?: ReturnType<TextBehavior["getSelection"]>): any;
|
|
54
|
+
/**
|
|
55
|
+
* Retrouve le nœud texte et l'offset DOM pour un offset Modèle donné
|
|
56
|
+
* @param {number} targetOffset
|
|
57
|
+
* @param {HTMLElement?} [element]
|
|
58
|
+
*/
|
|
59
|
+
getDOMPoint(targetOffset: number, element?: HTMLElement | null): {
|
|
60
|
+
node: Node;
|
|
61
|
+
offset: number;
|
|
62
|
+
} | null;
|
|
63
|
+
/** @param {number} index @param {string} text */
|
|
64
|
+
insert(index: number, text: string): void;
|
|
65
|
+
/** @param {Parameters<Block["delete"]>[0]} [deletion] @param {ReturnType<TextBehavior["getSelection"]>} [selection] */
|
|
66
|
+
delete(deletion?: Parameters<Block["delete"]>[0], selection?: ReturnType<TextBehavior["getSelection"]>): void;
|
|
67
|
+
/** @param {import('loro-crdt').Delta<string>[]} data */
|
|
68
|
+
applyDelta(data?: import("loro-crdt").Delta<string>[]): void;
|
|
69
|
+
/**
|
|
70
|
+
* @param {Parameters<Block["split"]>[0]} [options]
|
|
71
|
+
* @param {ReturnType<TextBehavior["getSelection"]>} [selection]
|
|
72
|
+
* @returns {ReturnType<Block["split"]>}
|
|
73
|
+
*/
|
|
74
|
+
split(options?: Parameters<Block["split"]>[0], selection?: ReturnType<TextBehavior["getSelection"]>): ReturnType<Block["split"]>;
|
|
75
|
+
/** @param {string} markName @param {any} value @param {ReturnType<TextBehavior["getSelection"]>} sel */
|
|
76
|
+
applyMark(markName: string, value: any, sel: ReturnType<TextBehavior["getSelection"]>): void;
|
|
77
|
+
/** @param {string} markName @param {ReturnType<TextBehavior["getSelection"]>} sel */
|
|
78
|
+
removeMark(markName: string, sel: ReturnType<TextBehavior["getSelection"]>): void;
|
|
79
|
+
/** @param {string} markName @param {ReturnType<TextBehavior["getSelection"]>} sel */
|
|
80
|
+
isMarkActive(markName: string, sel: ReturnType<TextBehavior["getSelection"]>): boolean;
|
|
81
|
+
/** @param {string} markName @param {any} value @param {ReturnType<TextBehavior["getSelection"]>} sel */
|
|
82
|
+
toggleMark(markName: string, value: any, sel: ReturnType<TextBehavior["getSelection"]>): void;
|
|
83
|
+
/** @returns {string} */
|
|
84
|
+
toMarkdown(): string;
|
|
85
|
+
/**
|
|
86
|
+
* Converts the text delta to a Slate-like JSON format.
|
|
87
|
+
* @returns {{text: string, [mark: string]: any}[]}
|
|
88
|
+
*/
|
|
89
|
+
toJSON(): {
|
|
90
|
+
text: string;
|
|
91
|
+
[mark: string]: any;
|
|
92
|
+
}[];
|
|
93
|
+
/** @param {Block} other */
|
|
94
|
+
absorbs(other: Block): boolean;
|
|
95
|
+
}
|
|
96
|
+
export type TextNode = NabuNode<{
|
|
97
|
+
type: "paragraph";
|
|
98
|
+
text: LoroText;
|
|
99
|
+
}>;
|
|
100
|
+
import type { Block } from "../../blocks";
|
|
101
|
+
import { LoroText } from "loro-crdt";
|
|
102
|
+
import type { NabuSelection } from "../../blocks";
|
|
103
|
+
import type { NabuNode } from "../../blocks";
|