@editneo/core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +185 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# @editneo/core
|
|
2
|
+
|
|
3
|
+
The headless engine behind EditNeo. This package contains the type definitions for the block-based document model, and a Zustand-powered state store with built-in undo/redo history. It has no dependency on React or any UI framework — you can use it anywhere.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @editneo/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Concepts
|
|
12
|
+
|
|
13
|
+
An EditNeo document is a flat map of **blocks**, each identified by a unique ID. A separate ordered array called `rootBlocks` determines the visual order. Every block contains an array of **spans** — small chunks of text that carry inline formatting metadata like bold, italic, or links.
|
|
14
|
+
|
|
15
|
+
This flat structure (rather than a deeply nested tree) makes CRDT-based collaboration straightforward, because each block can be individually addressed and merged.
|
|
16
|
+
|
|
17
|
+
## Types
|
|
18
|
+
|
|
19
|
+
### `BlockType`
|
|
20
|
+
|
|
21
|
+
All supported block types:
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
type BlockType =
|
|
25
|
+
| "paragraph"
|
|
26
|
+
| "heading-1"
|
|
27
|
+
| "heading-2"
|
|
28
|
+
| "heading-3"
|
|
29
|
+
| "bullet-list"
|
|
30
|
+
| "ordered-list"
|
|
31
|
+
| "todo-list"
|
|
32
|
+
| "code-block"
|
|
33
|
+
| "image"
|
|
34
|
+
| "video"
|
|
35
|
+
| "pdf-page"
|
|
36
|
+
| "quote"
|
|
37
|
+
| "divider"
|
|
38
|
+
| "callout";
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `Span`
|
|
42
|
+
|
|
43
|
+
A span is a run of text with optional inline formatting. A single block's content is an array of spans, so mixed formatting within a block is represented by multiple spans side by side.
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
interface Span {
|
|
47
|
+
text: string;
|
|
48
|
+
bold?: boolean;
|
|
49
|
+
italic?: boolean;
|
|
50
|
+
code?: boolean;
|
|
51
|
+
underline?: boolean;
|
|
52
|
+
strike?: boolean;
|
|
53
|
+
color?: string; // CSS color value, e.g. "#ef4444"
|
|
54
|
+
highlight?: string; // Background highlight color
|
|
55
|
+
link?: string; // URL the text links to
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For example, the sentence "Hello **world**" would be represented as:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
[{ text: "Hello " }, { text: "world", bold: true }];
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `NeoBlock`
|
|
66
|
+
|
|
67
|
+
The fundamental unit of the document:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
interface NeoBlock {
|
|
71
|
+
id: string; // UUID
|
|
72
|
+
type: BlockType;
|
|
73
|
+
content: Span[]; // The text content with formatting
|
|
74
|
+
props: Record<string, any>; // Block-specific metadata (e.g. image src, language for code)
|
|
75
|
+
children: string[]; // IDs of nested child blocks
|
|
76
|
+
parentId: string | null; // ID of parent block, or null if root-level
|
|
77
|
+
createdAt: number; // Unix timestamp
|
|
78
|
+
updatedAt: number;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `EditorState`
|
|
83
|
+
|
|
84
|
+
The complete editor state at any point in time:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
interface EditorState {
|
|
88
|
+
blocks: Record<string, NeoBlock>; // All blocks, keyed by ID
|
|
89
|
+
rootBlocks: string[]; // Ordered IDs of top-level blocks
|
|
90
|
+
history: Partial<EditorState>[]; // Undo stack
|
|
91
|
+
historyIndex: number; // Current position in history
|
|
92
|
+
selection: {
|
|
93
|
+
blockId: string | null; // Currently focused block
|
|
94
|
+
startOffset: number;
|
|
95
|
+
endOffset: number;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Editor Store
|
|
101
|
+
|
|
102
|
+
The store is a [Zustand](https://zustand-demo.pmnd.rs/) store. You can use it directly in React via the hook, or access it imperatively from anywhere.
|
|
103
|
+
|
|
104
|
+
### Using the hook (inside React components)
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { useEditorStore } from "@editneo/core";
|
|
108
|
+
|
|
109
|
+
function MyComponent() {
|
|
110
|
+
// Subscribe to specific slices to avoid unnecessary re-renders
|
|
111
|
+
const blocks = useEditorStore((state) => state.blocks);
|
|
112
|
+
const rootBlocks = useEditorStore((state) => state.rootBlocks);
|
|
113
|
+
const selection = useEditorStore((state) => state.selection);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Imperative access (outside React, in tests, etc.)
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { useEditorStore } from "@editneo/core";
|
|
121
|
+
|
|
122
|
+
const state = useEditorStore.getState();
|
|
123
|
+
const { addBlock, updateBlock, deleteBlock, toggleMark, undo, redo } = state;
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Actions
|
|
127
|
+
|
|
128
|
+
#### `addBlock(type, afterId?)`
|
|
129
|
+
|
|
130
|
+
Creates a new empty block and inserts it into the document. If `afterId` is provided and exists in `rootBlocks`, the new block is placed immediately after it. Otherwise it is appended to the end. The previous state is pushed onto the undo stack.
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
addBlock("paragraph"); // Appends a paragraph at the end
|
|
134
|
+
addBlock("heading-1", "block-abc"); // Inserts a heading after block-abc
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### `updateBlock(id, partial)`
|
|
138
|
+
|
|
139
|
+
Merges partial data into an existing block. The `updatedAt` timestamp is set automatically. History is recorded.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
updateBlock("block-abc", {
|
|
143
|
+
content: [{ text: "Updated text", bold: true }],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
updateBlock("block-xyz", {
|
|
147
|
+
props: { language: "typescript" }, // For a code block
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### `deleteBlock(id)`
|
|
152
|
+
|
|
153
|
+
Removes a block from the document. If the block has children, those children are promoted to the root level in the same position. History is recorded.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
deleteBlock("block-abc");
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### `toggleMark(mark)`
|
|
160
|
+
|
|
161
|
+
Toggles an inline formatting mark on the currently selected block's content. If all spans already have the mark, it is removed. Otherwise it is applied to all spans. Supported marks: `'bold'`, `'italic'`, `'underline'`, `'strike'`, `'code'`.
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
toggleMark("bold");
|
|
165
|
+
toggleMark("italic");
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### `undo()` / `redo()`
|
|
169
|
+
|
|
170
|
+
Navigates through the history stack. `undo()` restores the previous state, `redo()` moves forward. Both are no-ops at the boundaries of histoy.
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
undo();
|
|
174
|
+
redo();
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## History Model
|
|
178
|
+
|
|
179
|
+
Every mutating action (`addBlock`, `updateBlock`, `deleteBlock`, `toggleMark`) captures a snapshot of `blocks`, `rootBlocks`, and `selection` before applying the change, and appends it to the `history` array. When `undo()` is called, the editor reverts to the snapshot at `historyIndex`. On `redo()`, it moves forward.
|
|
180
|
+
|
|
181
|
+
If a new action is performed after undoing, the forward history is discarded (standard undo stack behavior).
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|