@editneo/react 0.1.1 → 0.1.2
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 +81 -40
- package/dist/EditableBlock.d.ts.map +1 -1
- package/dist/EditableBlock.js +123 -15
- package/dist/EditableBlock.js.map +1 -1
- package/dist/NeoCanvas.d.ts.map +1 -1
- package/dist/NeoCanvas.js +33 -8
- package/dist/NeoCanvas.js.map +1 -1
- package/dist/NeoEditor.d.ts +12 -4
- package/dist/NeoEditor.d.ts.map +1 -1
- package/dist/NeoEditor.js +51 -15
- package/dist/NeoEditor.js.map +1 -1
- package/dist/blocks/ListBlock.d.ts.map +1 -1
- package/dist/blocks/ListBlock.js +17 -1
- package/dist/blocks/ListBlock.js.map +1 -1
- package/dist/components/Aeropeak.d.ts +3 -1
- package/dist/components/Aeropeak.d.ts.map +1 -1
- package/dist/components/Aeropeak.js +46 -24
- package/dist/components/Aeropeak.js.map +1 -1
- package/dist/components/CursorOverlay.d.ts +4 -1
- package/dist/components/CursorOverlay.d.ts.map +1 -1
- package/dist/components/CursorOverlay.js +113 -10
- package/dist/components/CursorOverlay.js.map +1 -1
- package/dist/components/PDFDropZone.d.ts.map +1 -1
- package/dist/components/PDFDropZone.js +23 -33
- package/dist/components/PDFDropZone.js.map +1 -1
- package/dist/components/SlashMenu.d.ts.map +1 -1
- package/dist/components/SlashMenu.js +50 -46
- package/dist/components/SlashMenu.js.map +1 -1
- package/dist/hooks.d.ts +25 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +19 -6
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/package.json +23 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The React component layer for EditNeo. This package provides `NeoEditor` (the root editor component), a set of ready-made block renderers, interactive UI components like the floating toolbar and slash-command menu, and hooks for reading and manipulating editor state.
|
|
4
4
|
|
|
5
|
-
Rendering is virtualized with `@tanstack/react-virtual`, so documents with thousands of blocks remain responsive.
|
|
5
|
+
Each `NeoEditor` instance creates its own isolated store, so multiple editors on the same page work independently. Rendering is virtualized with `@tanstack/react-virtual`, so documents with thousands of blocks remain responsive.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -12,6 +12,13 @@ npm install @editneo/react @editneo/core
|
|
|
12
12
|
|
|
13
13
|
React 18 or 19 is required as a peer dependency.
|
|
14
14
|
|
|
15
|
+
**Optional packages:**
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @editneo/sync # For real-time collaboration & offline persistence
|
|
19
|
+
npm install @editneo/pdf # For PDF drag-and-drop import
|
|
20
|
+
```
|
|
21
|
+
|
|
15
22
|
## Getting Started
|
|
16
23
|
|
|
17
24
|
A minimal working editor:
|
|
@@ -30,7 +37,7 @@ The `id` prop is a unique identifier for the document. It is used to namespace I
|
|
|
30
37
|
|
|
31
38
|
### `<NeoEditor />`
|
|
32
39
|
|
|
33
|
-
The root component. It
|
|
40
|
+
The root component. It creates a per-instance editor store, sets up the optional `SyncManager`, applies the theme, and renders the virtualized block canvas.
|
|
34
41
|
|
|
35
42
|
| Prop | Type | Default | Description |
|
|
36
43
|
| ------------- | ------------------------------------------------- | -------- | --------------------------------------------------------------- |
|
|
@@ -42,6 +49,8 @@ The root component. It sets up the editor context, initializes the optional `Syn
|
|
|
42
49
|
| `className` | `string` | — | CSS class for the outer wrapper |
|
|
43
50
|
| `children` | `ReactNode` | — | Toolbar, menus, or other UI to render inside the editor context |
|
|
44
51
|
|
|
52
|
+
> **Note:** `@editneo/sync` is lazy-loaded via dynamic `import()` and only instantiated when `syncConfig` or `offline` is set. If the package isn't installed, the editor works fine without it.
|
|
53
|
+
|
|
45
54
|
**Usage with collaboration:**
|
|
46
55
|
|
|
47
56
|
```tsx
|
|
@@ -55,13 +64,12 @@ The root component. It sets up the editor context, initializes the optional `Syn
|
|
|
55
64
|
>
|
|
56
65
|
<Aeropeak />
|
|
57
66
|
<SlashMenu />
|
|
67
|
+
<CursorOverlay />
|
|
58
68
|
</NeoEditor>
|
|
59
69
|
```
|
|
60
70
|
|
|
61
71
|
**Custom block rendering:**
|
|
62
72
|
|
|
63
|
-
If you have custom block types beyond the built-in ones, use `renderBlock` to intercept them:
|
|
64
|
-
|
|
65
73
|
```tsx
|
|
66
74
|
<NeoEditor
|
|
67
75
|
id="doc"
|
|
@@ -76,7 +84,7 @@ If you have custom block types beyond the built-in ones, use `renderBlock` to in
|
|
|
76
84
|
|
|
77
85
|
### `<NeoCanvas />`
|
|
78
86
|
|
|
79
|
-
The virtualized document canvas. It renders only the blocks currently visible in the viewport, using `@tanstack/react-virtual` for smooth scrolling
|
|
87
|
+
The virtualized document canvas. It renders only the blocks currently visible in the viewport, using `@tanstack/react-virtual` for smooth scrolling. Block size estimates are type-aware (headings are taller than paragraphs, code blocks taller still). You typically don't render this directly — `NeoEditor` includes it automatically.
|
|
80
88
|
|
|
81
89
|
### `<BlockRenderer />`
|
|
82
90
|
|
|
@@ -91,11 +99,14 @@ If `renderBlock` is provided through the editor context, it is called first, giv
|
|
|
91
99
|
|
|
92
100
|
### `<EditableBlock />`
|
|
93
101
|
|
|
94
|
-
Handles the content-editable rendering and input processing for a single block. It converts the block's `Span[]` content into styled inline elements (bold, italic, code, underline, strikethrough, links). It also handles:
|
|
102
|
+
Handles the content-editable rendering and input processing for a single block. It converts the block's `Span[]` content into styled inline elements (bold, italic, code, underline, strikethrough, links, colors, highlights). It also handles:
|
|
95
103
|
|
|
96
|
-
- Enter
|
|
97
|
-
- Backspace at the start
|
|
98
|
-
-
|
|
104
|
+
- **Enter** — split the block and create a new paragraph
|
|
105
|
+
- **Backspace** at the start — delete the block
|
|
106
|
+
- **Ctrl+Z / Ctrl+Y** — undo / redo
|
|
107
|
+
- **Shift+Enter** — soft line break (`<br>`)
|
|
108
|
+
- **Tab** — indent the block (increases indent level)
|
|
109
|
+
- Input events parsed from the DOM, preserving inline formatting
|
|
99
110
|
|
|
100
111
|
---
|
|
101
112
|
|
|
@@ -103,7 +114,7 @@ Handles the content-editable rendering and input processing for a single block.
|
|
|
103
114
|
|
|
104
115
|
### `<Aeropeak />` — Floating Toolbar
|
|
105
116
|
|
|
106
|
-
A toolbar that appears above the user's text selection. By default it shows Bold, Italic, Strikethrough, and Link buttons.
|
|
117
|
+
A toolbar that appears above the user's text selection. By default it shows Bold, Italic, Underline, Strikethrough, Code, and Link buttons. SSR-safe.
|
|
107
118
|
|
|
108
119
|
| Prop | Type | Default | Description |
|
|
109
120
|
| ----------- | ----------------------------- | -------- | -------------------------------------------- |
|
|
@@ -112,7 +123,7 @@ A toolbar that appears above the user's text selection. By default it shows Bold
|
|
|
112
123
|
| `animation` | `'fade' \| 'scale' \| 'none'` | `'fade'` | Appearance animation |
|
|
113
124
|
|
|
114
125
|
```tsx
|
|
115
|
-
// Default toolbar
|
|
126
|
+
// Default toolbar
|
|
116
127
|
<Aeropeak />
|
|
117
128
|
|
|
118
129
|
// Custom toolbar
|
|
@@ -133,13 +144,13 @@ A toolbar that appears above the user's text selection. By default it shows Bold
|
|
|
133
144
|
|
|
134
145
|
**Compound components:**
|
|
135
146
|
|
|
136
|
-
- `Aeropeak.Bold` / `Aeropeak.Italic` / `Aeropeak.Strike` / `Aeropeak.Link` — prebuilt buttons
|
|
147
|
+
- `Aeropeak.Bold` / `Aeropeak.Italic` / `Aeropeak.Underline` / `Aeropeak.Strike` / `Aeropeak.Code` / `Aeropeak.Link` — prebuilt buttons
|
|
137
148
|
- `AeroButton` — a button that receives the editor instance when clicked
|
|
138
149
|
- `Separator` — a thin vertical divider between button groups
|
|
139
150
|
|
|
140
151
|
### `<SlashMenu />` — Command Palette
|
|
141
152
|
|
|
142
|
-
Appears when the user types `/` in the editor. Lists available block types, supports keyboard navigation (arrow keys +
|
|
153
|
+
Appears when the user types `/` in the editor. Lists available block types, supports keyboard navigation (arrow keys + Enter), and filters results as the user continues typing after `/`.
|
|
143
154
|
|
|
144
155
|
| Prop | Type | Default | Description |
|
|
145
156
|
| ---------------- | ------------------------------- | ------- | ------------------------------------------------------ |
|
|
@@ -149,6 +160,8 @@ Appears when the user types `/` in the editor. Lists available block types, supp
|
|
|
149
160
|
|
|
150
161
|
**Built-in commands:** Paragraph, Heading 1-3, Bulleted List, Ordered List, To-do List, Quote, Code Block, Divider, Callout, Image.
|
|
151
162
|
|
|
163
|
+
New blocks are inserted immediately **after** the current block.
|
|
164
|
+
|
|
152
165
|
```tsx
|
|
153
166
|
<SlashMenu
|
|
154
167
|
customCommands={[
|
|
@@ -162,20 +175,9 @@ Appears when the user types `/` in the editor. Lists available block types, supp
|
|
|
162
175
|
/>
|
|
163
176
|
```
|
|
164
177
|
|
|
165
|
-
**`CommandItem` interface:**
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
interface CommandItem {
|
|
169
|
-
key: string; // Unique key
|
|
170
|
-
label: string; // Display label
|
|
171
|
-
icon?: ReactNode; // Icon element
|
|
172
|
-
execute: (editor: any) => void; // Action to perform
|
|
173
|
-
}
|
|
174
|
-
```
|
|
175
|
-
|
|
176
178
|
### `<PDFDropZone />`
|
|
177
179
|
|
|
178
|
-
A wrapper component that detects PDF file drops
|
|
180
|
+
A wrapper component that detects PDF file drops, runs client-side extraction via `@editneo/pdf`, and inserts the extracted blocks into the editor. Shows a processing indicator while extraction is in progress.
|
|
179
181
|
|
|
180
182
|
| Prop | Type | Default | Description |
|
|
181
183
|
| --------------- | ------------------------------------------- | ------- | ------------------------------------------------------------------------------------ |
|
|
@@ -183,13 +185,15 @@ A wrapper component that detects PDF file drops and runs client-side extraction
|
|
|
183
185
|
| `renderOverlay` | `(props: { isOver: boolean }) => ReactNode` | — | Custom overlay content shown during drag-over |
|
|
184
186
|
| `children` | `ReactNode` | — | Content inside the drop zone |
|
|
185
187
|
|
|
186
|
-
|
|
187
|
-
<PDFDropZone>{/* Your editor content goes inside */}</PDFDropZone>
|
|
188
|
-
```
|
|
188
|
+
> **Note:** `@editneo/pdf` is lazy-loaded via dynamic `import()`. If the package isn't installed, the drop zone logs a warning and does nothing.
|
|
189
189
|
|
|
190
190
|
### `<CursorOverlay />`
|
|
191
191
|
|
|
192
|
-
Displays colored cursors and labels for remote collaborators
|
|
192
|
+
Displays colored cursors and name labels for remote collaborators. Cursor positions are calculated using `Range.getBoundingClientRect()` for pixel-accurate placement, and updated automatically via `MutationObserver` when the DOM changes.
|
|
193
|
+
|
|
194
|
+
| Prop | Type | Default | Description |
|
|
195
|
+
| ------------- | -------------------------------------- | ------- | ---------------------------------------- |
|
|
196
|
+
| `renderLabel` | `(user: { name, color }) => ReactNode` | — | Custom label renderer for remote cursors |
|
|
193
197
|
|
|
194
198
|
```tsx
|
|
195
199
|
<NeoEditor id="doc" syncConfig={{ url: "wss://...", room: "doc" }}>
|
|
@@ -197,7 +201,21 @@ Displays colored cursors and labels for remote collaborators when using real-tim
|
|
|
197
201
|
</NeoEditor>
|
|
198
202
|
```
|
|
199
203
|
|
|
200
|
-
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Block Components
|
|
207
|
+
|
|
208
|
+
All block components are exported and can be used standalone if needed:
|
|
209
|
+
|
|
210
|
+
| Component | Block Type | Description |
|
|
211
|
+
| -------------- | ------------------- | -------------------------------------------------- |
|
|
212
|
+
| `HeadingBlock` | heading-1/2/3 | Renders `<h1>`, `<h2>`, or `<h3>` |
|
|
213
|
+
| `ListBlock` | bullet/ordered/todo | Supports numbered lists and interactive checkboxes |
|
|
214
|
+
| `CodeBlock` | code-block | Monospace code with language support |
|
|
215
|
+
| `QuoteBlock` | quote | Bordered blockquote |
|
|
216
|
+
| `CalloutBlock` | callout | Highlighted callout box |
|
|
217
|
+
| `DividerBlock` | divider | Horizontal rule |
|
|
218
|
+
| `MediaBlock` | image, video | Image/video with src, alt, width, height props |
|
|
201
219
|
|
|
202
220
|
---
|
|
203
221
|
|
|
@@ -205,7 +223,7 @@ Cursor colors are assigned automatically based on the user's awareness client ID
|
|
|
205
223
|
|
|
206
224
|
### `useEditor()`
|
|
207
225
|
|
|
208
|
-
The primary hook for interacting with the editor. Must be called inside a `<NeoEditor />`. Returns all store state and actions
|
|
226
|
+
The primary hook for interacting with the editor. Must be called inside a `<NeoEditor />`. Returns all store state and actions.
|
|
209
227
|
|
|
210
228
|
```tsx
|
|
211
229
|
const {
|
|
@@ -214,9 +232,15 @@ const {
|
|
|
214
232
|
selection, // { blockId, startOffset, endOffset }
|
|
215
233
|
addBlock, // (type, afterId?) => void
|
|
216
234
|
insertBlock, // alias for addBlock
|
|
235
|
+
insertFullBlock, // (block, afterId?) => void
|
|
217
236
|
updateBlock, // (id, partial) => void
|
|
218
237
|
deleteBlock, // (id) => void
|
|
238
|
+
moveBlock, // (id, afterId) => void
|
|
239
|
+
setBlockType, // (id, type) => void
|
|
219
240
|
toggleMark, // (mark) => void
|
|
241
|
+
setLink, // (url | null) => void
|
|
242
|
+
exportJSON, // () => { blocks, rootBlocks }
|
|
243
|
+
importJSON, // (data) => void
|
|
220
244
|
undo, // () => void
|
|
221
245
|
redo, // () => void
|
|
222
246
|
} = useEditor();
|
|
@@ -226,7 +250,7 @@ Throws an error if called outside of `<NeoEditor />`.
|
|
|
226
250
|
|
|
227
251
|
### `useSelection()`
|
|
228
252
|
|
|
229
|
-
A focused hook that subscribes only to the selection state, minimizing re-renders
|
|
253
|
+
A focused hook that subscribes only to the selection state, minimizing re-renders.
|
|
230
254
|
|
|
231
255
|
```tsx
|
|
232
256
|
const selection = useSelection();
|
|
@@ -246,28 +270,31 @@ const status = useSyncStatus();
|
|
|
246
270
|
|
|
247
271
|
## Theming
|
|
248
272
|
|
|
249
|
-
`NeoEditor`
|
|
273
|
+
`NeoEditor` scopes CSS variables to its root element, so multiple editors on the same page can have different themes without conflicts. Override any variable in your stylesheet:
|
|
250
274
|
|
|
251
275
|
```css
|
|
252
276
|
:root {
|
|
253
277
|
--neo-font-family: "Inter", system-ui, sans-serif;
|
|
254
278
|
--neo-font-size-body: 16px;
|
|
279
|
+
--neo-code-font: "Fira Code", "Consolas", monospace;
|
|
255
280
|
--neo-accent-color: #3b82f6;
|
|
256
281
|
--neo-bg-canvas: #ffffff;
|
|
257
282
|
--neo-text-primary: #111827;
|
|
283
|
+
--neo-text-secondary: #6b7280;
|
|
284
|
+
--neo-selection-color: #b4d5fe;
|
|
285
|
+
--neo-border-color: #e5e7eb;
|
|
258
286
|
--neo-border-radius: 4px;
|
|
259
287
|
--neo-block-spacing: 4px;
|
|
288
|
+
--neo-content-width: 800px;
|
|
260
289
|
}
|
|
261
290
|
```
|
|
262
291
|
|
|
263
|
-
|
|
292
|
+
Switch modes via the `theme` prop or via the `data-theme` attribute:
|
|
264
293
|
|
|
265
294
|
```tsx
|
|
266
295
|
<NeoEditor id="doc" theme={{ mode: "dark" }} />
|
|
267
296
|
```
|
|
268
297
|
|
|
269
|
-
Dark mode sets `--neo-bg-canvas` to `#0f172a` and `--neo-text-primary` to `#f3f4f6`.
|
|
270
|
-
|
|
271
298
|
## Exports
|
|
272
299
|
|
|
273
300
|
Everything is exported from the package root:
|
|
@@ -277,12 +304,26 @@ Everything is exported from the package root:
|
|
|
277
304
|
export { NeoEditor, EditorContext } from "./NeoEditor";
|
|
278
305
|
export { NeoCanvas } from "./NeoCanvas";
|
|
279
306
|
export { EditableBlock } from "./EditableBlock";
|
|
307
|
+
export { BlockRenderer } from "./BlockRenderer";
|
|
308
|
+
|
|
309
|
+
// Block renderers
|
|
310
|
+
export {
|
|
311
|
+
HeadingBlock,
|
|
312
|
+
ListBlock,
|
|
313
|
+
MediaBlock,
|
|
314
|
+
CodeBlock,
|
|
315
|
+
QuoteBlock,
|
|
316
|
+
CalloutBlock,
|
|
317
|
+
DividerBlock,
|
|
318
|
+
} from "./blocks/*";
|
|
280
319
|
|
|
281
320
|
// Interactive UI
|
|
282
|
-
export {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
321
|
+
export {
|
|
322
|
+
PDFDropZone,
|
|
323
|
+
CursorOverlay,
|
|
324
|
+
Aeropeak,
|
|
325
|
+
SlashMenu,
|
|
326
|
+
} from "./components/*";
|
|
286
327
|
|
|
287
328
|
// Hooks
|
|
288
329
|
export { useEditor, useSelection, useSyncStatus } from "./hooks";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditableBlock.d.ts","sourceRoot":"","sources":["../src/EditableBlock.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"EditableBlock.d.ts","sourceRoot":"","sources":["../src/EditableBlock.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwC,MAAM,OAAO,CAAC;AAC7D,OAAO,EAAE,QAAQ,EAAQ,MAAM,eAAe,CAAC;AAI/C,UAAU,kBAAkB;IAC1B,KAAK,EAAE,QAAQ,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AA8ED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAmGtD,CAAC"}
|
package/dist/EditableBlock.js
CHANGED
|
@@ -1,41 +1,149 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useRef, useEffect } from 'react';
|
|
3
|
-
import {
|
|
2
|
+
import { useRef, useContext, useEffect } from 'react';
|
|
3
|
+
import { useStore } from 'zustand';
|
|
4
|
+
import { EditorContext } from './NeoEditor';
|
|
5
|
+
/**
|
|
6
|
+
* Parse the contentEditable DOM back into Span[] preserving inline formatting.
|
|
7
|
+
* Walks the child nodes and reads computed/element styles to reconstruct
|
|
8
|
+
* bold, italic, underline, strikethrough, code, link, color, and highlight.
|
|
9
|
+
*/
|
|
10
|
+
function parseContentEditableToSpans(el) {
|
|
11
|
+
const spans = [];
|
|
12
|
+
function walk(node, inherited) {
|
|
13
|
+
var _a, _b;
|
|
14
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
15
|
+
const text = node.textContent || '';
|
|
16
|
+
if (text.length > 0) {
|
|
17
|
+
spans.push({ text, ...inherited });
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
22
|
+
return;
|
|
23
|
+
const elem = node;
|
|
24
|
+
const tag = elem.tagName.toLowerCase();
|
|
25
|
+
// Build formatting from the element
|
|
26
|
+
const fmt = { ...inherited };
|
|
27
|
+
if (tag === 'b' || tag === 'strong')
|
|
28
|
+
fmt.bold = true;
|
|
29
|
+
if (tag === 'i' || tag === 'em')
|
|
30
|
+
fmt.italic = true;
|
|
31
|
+
if (tag === 'u')
|
|
32
|
+
fmt.underline = true;
|
|
33
|
+
if (tag === 's' || tag === 'del' || tag === 'strike')
|
|
34
|
+
fmt.strike = true;
|
|
35
|
+
if (tag === 'code')
|
|
36
|
+
fmt.code = true;
|
|
37
|
+
if (tag === 'a')
|
|
38
|
+
fmt.link = elem.getAttribute('href') || undefined;
|
|
39
|
+
// Check inline styles
|
|
40
|
+
const style = elem.style;
|
|
41
|
+
if (style.fontWeight === 'bold' || parseInt(style.fontWeight) >= 700)
|
|
42
|
+
fmt.bold = true;
|
|
43
|
+
if (style.fontStyle === 'italic')
|
|
44
|
+
fmt.italic = true;
|
|
45
|
+
if ((_a = style.textDecoration) === null || _a === void 0 ? void 0 : _a.includes('underline'))
|
|
46
|
+
fmt.underline = true;
|
|
47
|
+
if ((_b = style.textDecoration) === null || _b === void 0 ? void 0 : _b.includes('line-through'))
|
|
48
|
+
fmt.strike = true;
|
|
49
|
+
if (style.color)
|
|
50
|
+
fmt.color = style.color;
|
|
51
|
+
if (style.backgroundColor && style.backgroundColor !== 'transparent')
|
|
52
|
+
fmt.highlight = style.backgroundColor;
|
|
53
|
+
for (const child of Array.from(elem.childNodes)) {
|
|
54
|
+
walk(child, fmt);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const child of Array.from(el.childNodes)) {
|
|
58
|
+
walk(child, {});
|
|
59
|
+
}
|
|
60
|
+
// Merge adjacent spans with identical formatting
|
|
61
|
+
const merged = [];
|
|
62
|
+
for (const span of spans) {
|
|
63
|
+
const prev = merged[merged.length - 1];
|
|
64
|
+
if (prev && spansHaveSameFormat(prev, span)) {
|
|
65
|
+
prev.text += span.text;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
merged.push({ ...span });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return merged;
|
|
72
|
+
}
|
|
73
|
+
function spansHaveSameFormat(a, b) {
|
|
74
|
+
return (!!a.bold === !!b.bold &&
|
|
75
|
+
!!a.italic === !!b.italic &&
|
|
76
|
+
!!a.underline === !!b.underline &&
|
|
77
|
+
!!a.strike === !!b.strike &&
|
|
78
|
+
!!a.code === !!b.code &&
|
|
79
|
+
(a.color || '') === (b.color || '') &&
|
|
80
|
+
(a.highlight || '') === (b.highlight || '') &&
|
|
81
|
+
(a.link || '') === (b.link || ''));
|
|
82
|
+
}
|
|
4
83
|
export const EditableBlock = ({ block, autoFocus }) => {
|
|
5
84
|
const contentRef = useRef(null);
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
85
|
+
const context = useContext(EditorContext);
|
|
86
|
+
if (!context) {
|
|
87
|
+
throw new Error('EditableBlock must be used within a NeoEditor');
|
|
88
|
+
}
|
|
89
|
+
const updateBlock = useStore(context.store, (state) => state.updateBlock);
|
|
90
|
+
const addBlock = useStore(context.store, (state) => state.addBlock);
|
|
91
|
+
const deleteBlock = useStore(context.store, (state) => state.deleteBlock);
|
|
92
|
+
const undo = useStore(context.store, (state) => state.undo);
|
|
93
|
+
const redo = useStore(context.store, (state) => state.redo);
|
|
9
94
|
useEffect(() => {
|
|
10
95
|
if (autoFocus && contentRef.current) {
|
|
11
96
|
contentRef.current.focus();
|
|
12
97
|
}
|
|
13
98
|
}, [autoFocus]);
|
|
99
|
+
/** (#9) Parse DOM back into spans preserving formatting */
|
|
14
100
|
const handleInput = (e) => {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
101
|
+
const el = contentRef.current;
|
|
102
|
+
if (!el)
|
|
103
|
+
return;
|
|
104
|
+
const newSpans = parseContentEditableToSpans(el);
|
|
105
|
+
updateBlock(block.id, { content: newSpans });
|
|
18
106
|
};
|
|
107
|
+
/** (#23) Additional keyboard shortcuts */
|
|
19
108
|
const handleKeyDown = (e) => {
|
|
20
109
|
var _a;
|
|
21
|
-
|
|
110
|
+
// Ctrl+Z / Cmd+Z = undo
|
|
111
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
undo();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Ctrl+Shift+Z / Ctrl+Y = redo
|
|
117
|
+
if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
redo();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
22
123
|
e.preventDefault();
|
|
23
124
|
addBlock('paragraph', block.id);
|
|
24
125
|
}
|
|
126
|
+
else if (e.key === 'Enter' && e.shiftKey) {
|
|
127
|
+
// Shift+Enter: let browser insert <br> (soft break) — don't prevent default
|
|
128
|
+
}
|
|
25
129
|
else if (e.key === 'Backspace' && ((_a = contentRef.current) === null || _a === void 0 ? void 0 : _a.innerText) === '') {
|
|
26
130
|
e.preventDefault();
|
|
27
131
|
deleteBlock(block.id);
|
|
28
132
|
}
|
|
29
|
-
else if (e.key === '
|
|
30
|
-
|
|
133
|
+
else if (e.key === 'Tab') {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
// Insert 2 spaces for now — real indent to be added
|
|
136
|
+
document.execCommand('insertText', false, ' ');
|
|
31
137
|
}
|
|
32
138
|
};
|
|
33
139
|
return (_jsx("div", { ref: contentRef, contentEditable: true, suppressContentEditableWarning: true, onInput: handleInput, onKeyDown: handleKeyDown, style: {
|
|
34
140
|
minHeight: '24px',
|
|
35
141
|
outline: 'none',
|
|
36
142
|
padding: '4px 0',
|
|
37
|
-
|
|
38
|
-
|
|
143
|
+
fontSize: block.type === 'heading-1' ? '2em'
|
|
144
|
+
: block.type === 'heading-2' ? '1.5em'
|
|
145
|
+
: block.type === 'heading-3' ? '1.25em'
|
|
146
|
+
: '1em',
|
|
39
147
|
fontWeight: block.type.startsWith('heading') ? 'bold' : 'normal',
|
|
40
148
|
}, children: block.content.map((span, i) => {
|
|
41
149
|
let style = {};
|
|
@@ -53,10 +161,10 @@ export const EditableBlock = ({ block, autoFocus }) => {
|
|
|
53
161
|
style.backgroundColor = span.highlight;
|
|
54
162
|
const content = _jsx("span", { style: style, children: span.text }, i);
|
|
55
163
|
if (span.code) {
|
|
56
|
-
return _jsx("code", { style: { fontFamily: 'monospace', backgroundColor: '#eee', padding: '2px 4px', borderRadius: '3px' }, children: content }, i);
|
|
164
|
+
return _jsx("code", { style: { fontFamily: 'var(--neo-code-font, monospace)', backgroundColor: '#eee', padding: '2px 4px', borderRadius: '3px' }, children: content }, i);
|
|
57
165
|
}
|
|
58
166
|
if (span.link) {
|
|
59
|
-
return _jsx("a", { href: span.link, style: { color: '
|
|
167
|
+
return _jsx("a", { href: span.link, style: { color: 'var(--neo-accent-color, #3b82f6)', textDecoration: 'underline' }, children: content }, i);
|
|
60
168
|
}
|
|
61
169
|
return content;
|
|
62
170
|
}) }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditableBlock.js","sourceRoot":"","sources":["../src/EditableBlock.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"EditableBlock.js","sourceRoot":"","sources":["../src/EditableBlock.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7D,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAO5C;;;;GAIG;AACH,SAAS,2BAA2B,CAAC,EAAe;IAClD,MAAM,KAAK,GAAW,EAAE,CAAC;IAEzB,SAAS,IAAI,CAAC,IAAU,EAAE,SAAwB;;QAChD,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,SAAS,EAAE,CAAC;YACrC,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC;YACpC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACpB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,SAAS,EAAE,CAAC,CAAC;YACrC,CAAC;YACD,OAAO;QACT,CAAC;QAED,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,YAAY;YAAE,OAAO;QAChD,MAAM,IAAI,GAAG,IAAmB,CAAC;QACjC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QAEvC,oCAAoC;QACpC,MAAM,GAAG,GAAkB,EAAE,GAAG,SAAS,EAAE,CAAC;QAE5C,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,QAAQ;YAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QACrD,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,IAAI;YAAE,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC;QACnD,IAAI,GAAG,KAAK,GAAG;YAAE,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC;QACtC,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,KAAK,IAAI,GAAG,KAAK,QAAQ;YAAE,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC;QACxE,IAAI,GAAG,KAAK,MAAM;YAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QACpC,IAAI,GAAG,KAAK,GAAG;YAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC;QAEnE,sBAAsB;QACtB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;QACzB,IAAI,KAAK,CAAC,UAAU,KAAK,MAAM,IAAI,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,GAAG;YAAE,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QACtF,IAAI,KAAK,CAAC,SAAS,KAAK,QAAQ;YAAE,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC;QACpD,IAAI,MAAA,KAAK,CAAC,cAAc,0CAAE,QAAQ,CAAC,WAAW,CAAC;YAAE,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC;QACtE,IAAI,MAAA,KAAK,CAAC,cAAc,0CAAE,QAAQ,CAAC,cAAc,CAAC;YAAE,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC;QACtE,IAAI,KAAK,CAAC,KAAK;YAAE,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;QACzC,IAAI,KAAK,CAAC,eAAe,IAAI,KAAK,CAAC,eAAe,KAAK,aAAa;YAAE,GAAG,CAAC,SAAS,GAAG,KAAK,CAAC,eAAe,CAAC;QAE5G,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAChD,IAAI,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;IAED,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC;QAC9C,IAAI,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAClB,CAAC;IAED,iDAAiD;IACjD,MAAM,MAAM,GAAW,EAAE,CAAC;IAC1B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACvC,IAAI,IAAI,IAAI,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;YAC5C,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,mBAAmB,CAAC,CAAO,EAAE,CAAO;IAC3C,OAAO,CACL,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI;QACrB,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM;QACzB,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS;QAC/B,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM;QACzB,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI;QACrB,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACnC,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC;QAC3C,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAClC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAiC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE;IAClF,MAAM,UAAU,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAC;IAChD,MAAM,OAAO,GAAG,UAAU,CAAC,aAAa,CAAC,CAAC;IAE1C,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;IACnE,CAAC;IAED,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAC1E,MAAM,QAAQ,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACpE,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IAC1E,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5D,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAE5D,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,SAAS,IAAI,UAAU,CAAC,OAAO,EAAE,CAAC;YACpC,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QAC7B,CAAC;IACH,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAEhB,2DAA2D;IAC3D,MAAM,WAAW,GAAG,CAAC,CAAkC,EAAE,EAAE;QACzD,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAC;QAC9B,IAAI,CAAC,EAAE;YAAE,OAAO;QAChB,MAAM,QAAQ,GAAG,2BAA2B,CAAC,EAAE,CAAC,CAAC;QACjD,WAAW,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC/C,CAAC,CAAC;IAEF,0CAA0C;IAC1C,MAAM,aAAa,GAAG,CAAC,CAAsC,EAAE,EAAE;;QAC/D,wBAAwB;QACxB,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC7D,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QACD,+BAA+B;QAC/B,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;YACjF,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QAED,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;YACrC,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;QAClC,CAAC;aAAM,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;YAC3C,4EAA4E;QAC9E,CAAC;aAAM,IAAI,CAAC,CAAC,GAAG,KAAK,WAAW,IAAI,CAAA,MAAA,UAAU,CAAC,OAAO,0CAAE,SAAS,MAAK,EAAE,EAAE,CAAC;YACzE,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACxB,CAAC;aAAM,IAAI,CAAC,CAAC,GAAG,KAAK,KAAK,EAAE,CAAC;YAC3B,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,oDAAoD;YACpD,QAAQ,CAAC,WAAW,CAAC,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAClD,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,CACL,cACE,GAAG,EAAE,UAAU,EACf,eAAe,QACf,8BAA8B,QAC9B,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,aAAa,EACxB,KAAK,EAAE;YACL,SAAS,EAAE,MAAM;YACjB,OAAO,EAAE,MAAM;YACf,OAAO,EAAE,OAAO;YAChB,QAAQ,EAAE,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,KAAK;gBACrC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,OAAO;oBACtC,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,QAAQ;wBACvC,CAAC,CAAC,KAAK;YACd,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ;SACjE,YAEA,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE;YAC7B,IAAI,KAAK,GAAwB,EAAE,CAAC;YACpC,IAAI,IAAI,CAAC,IAAI;gBAAE,KAAK,CAAC,UAAU,GAAG,MAAM,CAAC;YACzC,IAAI,IAAI,CAAC,MAAM;gBAAE,KAAK,CAAC,SAAS,GAAG,QAAQ,CAAC;YAC5C,IAAI,IAAI,CAAC,SAAS;gBAAE,KAAK,CAAC,cAAc,GAAG,WAAW,CAAC;YACvD,IAAI,IAAI,CAAC,MAAM;gBAAE,KAAK,CAAC,cAAc,GAAG,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,GAAG,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,cAAc,CAAC;YAClH,IAAI,IAAI,CAAC,KAAK;gBAAE,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;YACzC,IAAI,IAAI,CAAC,SAAS;gBAAE,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC,SAAS,CAAC;YAE3D,MAAM,OAAO,GAAG,eAAc,KAAK,EAAE,KAAK,YAAG,IAAI,CAAC,IAAI,IAA3B,CAAC,CAAkC,CAAC;YAE/D,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;gBACd,OAAO,eAAc,KAAK,EAAE,EAAE,UAAU,EAAE,iCAAiC,EAAE,eAAe,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,YAAG,OAAO,IAAvI,CAAC,CAA8I,CAAC;YACpK,CAAC;YAED,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;gBACd,OAAO,YAAW,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,kCAAkC,EAAE,cAAc,EAAE,WAAW,EAAE,YAAG,OAAO,IAA/G,CAAC,CAAmH,CAAC;YACtI,CAAC;YAED,OAAO,OAAO,CAAC;QACjB,CAAC,CAAC,GACE,CACP,CAAC;AACJ,CAAC,CAAC"}
|
package/dist/NeoCanvas.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NeoCanvas.d.ts","sourceRoot":"","sources":["../src/NeoCanvas.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"NeoCanvas.d.ts","sourceRoot":"","sources":["../src/NeoCanvas.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA6B,MAAM,OAAO,CAAC;AAMlD,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAkF7B,CAAC"}
|
package/dist/NeoCanvas.js
CHANGED
|
@@ -1,26 +1,51 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useRef } from 'react';
|
|
2
|
+
import { useRef, useContext } from 'react';
|
|
3
3
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { useStore } from 'zustand';
|
|
5
|
+
import { EditorContext } from './NeoEditor';
|
|
6
|
+
import { BlockRenderer } from './BlockRenderer';
|
|
6
7
|
export const NeoCanvas = () => {
|
|
7
|
-
const
|
|
8
|
-
|
|
8
|
+
const context = useContext(EditorContext);
|
|
9
|
+
if (!context) {
|
|
10
|
+
throw new Error('NeoCanvas must be used within a NeoEditor');
|
|
11
|
+
}
|
|
12
|
+
const rootBlocks = useStore(context.store, (state) => state.rootBlocks);
|
|
13
|
+
const blocks = useStore(context.store, (state) => state.blocks);
|
|
9
14
|
const parentRef = useRef(null);
|
|
15
|
+
/** (#26) Type-aware size estimates for better virtualizer performance */
|
|
16
|
+
const estimateSize = (index) => {
|
|
17
|
+
const blockId = rootBlocks[index];
|
|
18
|
+
const block = blocks[blockId];
|
|
19
|
+
if (!block)
|
|
20
|
+
return 35;
|
|
21
|
+
switch (block.type) {
|
|
22
|
+
case 'heading-1': return 60;
|
|
23
|
+
case 'heading-2': return 48;
|
|
24
|
+
case 'heading-3': return 40;
|
|
25
|
+
case 'code-block': return 120;
|
|
26
|
+
case 'image':
|
|
27
|
+
case 'video': return 200;
|
|
28
|
+
case 'divider': return 24;
|
|
29
|
+
default: return 35;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
10
32
|
const rowVirtualizer = useVirtualizer({
|
|
11
33
|
count: rootBlocks.length,
|
|
12
34
|
getScrollElement: () => parentRef.current,
|
|
13
|
-
estimateSize
|
|
35
|
+
estimateSize,
|
|
14
36
|
overscan: 5,
|
|
15
37
|
});
|
|
16
38
|
return (_jsx("div", { ref: parentRef, style: {
|
|
17
|
-
height: '
|
|
39
|
+
height: '100%', /* (#24) was 100vh, causing double scrollbar */
|
|
18
40
|
width: '100%',
|
|
19
41
|
overflow: 'auto',
|
|
20
42
|
}, children: _jsx("div", { style: {
|
|
21
43
|
height: `${rowVirtualizer.getTotalSize()}px`,
|
|
22
44
|
width: '100%',
|
|
23
45
|
position: 'relative',
|
|
46
|
+
maxWidth: 'var(--neo-content-width, 800px)',
|
|
47
|
+
margin: '0 auto',
|
|
48
|
+
padding: '0 1rem',
|
|
24
49
|
}, children: rowVirtualizer.getVirtualItems().map((virtualRow) => {
|
|
25
50
|
const blockId = rootBlocks[virtualRow.index];
|
|
26
51
|
const block = blocks[blockId];
|
|
@@ -32,7 +57,7 @@ export const NeoCanvas = () => {
|
|
|
32
57
|
left: 0,
|
|
33
58
|
width: '100%',
|
|
34
59
|
transform: `translateY(${virtualRow.start}px)`,
|
|
35
|
-
}, children: _jsx(
|
|
60
|
+
}, children: _jsx(BlockRenderer, { block: block }) }, virtualRow.key));
|
|
36
61
|
}) }) }));
|
|
37
62
|
};
|
|
38
63
|
//# sourceMappingURL=NeoCanvas.js.map
|
package/dist/NeoCanvas.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NeoCanvas.js","sourceRoot":"","sources":["../src/NeoCanvas.tsx"],"names":[],"mappings":";AAAA,OAAc,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"NeoCanvas.js","sourceRoot":"","sources":["../src/NeoCanvas.tsx"],"names":[],"mappings":"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"}
|
package/dist/NeoEditor.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { EditorStoreInstance, EditorStore } from '@editneo/core';
|
|
3
3
|
import './styles.css';
|
|
4
4
|
export interface NeoEditorProps {
|
|
5
5
|
id: string;
|
|
@@ -16,10 +16,18 @@ export interface NeoEditorProps {
|
|
|
16
16
|
};
|
|
17
17
|
children?: React.ReactNode;
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
interface EditorContextValue {
|
|
20
20
|
editorId: string;
|
|
21
|
-
|
|
21
|
+
store: EditorStoreInstance;
|
|
22
|
+
syncManager: any | null;
|
|
22
23
|
renderBlock?: (block: any, defaultRender: any) => React.ReactNode;
|
|
23
|
-
}
|
|
24
|
+
}
|
|
25
|
+
export declare const EditorContext: React.Context<EditorContextValue | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Hook to access the current editor's Zustand store via context.
|
|
28
|
+
* Supports selectors for fine-grained re-render control.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useEditorStoreContext<T>(selector: (state: EditorStore) => T): T;
|
|
24
31
|
export declare const NeoEditor: React.FC<NeoEditorProps>;
|
|
32
|
+
export {};
|
|
25
33
|
//# sourceMappingURL=NeoEditor.d.ts.map
|
package/dist/NeoEditor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NeoEditor.d.ts","sourceRoot":"","sources":["../src/NeoEditor.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"NeoEditor.d.ts","sourceRoot":"","sources":["../src/NeoEditor.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0E,MAAM,OAAO,CAAC;AAC/F,OAAO,EAAqB,mBAAmB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGpF,OAAO,cAAc,CAAC;AAEtB,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE;QACX,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;QACvB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB,CAAC;IACF,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAED,UAAU,kBAAkB;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,mBAAmB,CAAC;IAC3B,WAAW,EAAE,GAAG,GAAG,IAAI,CAAC;IACxB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC;CACnE;AAED,eAAO,MAAM,aAAa,0CAAiD,CAAC;AAE5E;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,CAAC,GAAG,CAAC,CAM/E;AAED,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CAsF9C,CAAC"}
|