@editneo/react 0.1.0 → 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 +334 -0
- 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
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# @editneo/react
|
|
2
|
+
|
|
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
|
+
|
|
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
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @editneo/react @editneo/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
React 18 or 19 is required as a peer dependency.
|
|
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
|
+
|
|
22
|
+
## Getting Started
|
|
23
|
+
|
|
24
|
+
A minimal working editor:
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { NeoEditor } from "@editneo/react";
|
|
28
|
+
|
|
29
|
+
function App() {
|
|
30
|
+
return <NeoEditor id="my-document" />;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The `id` prop is a unique identifier for the document. It is used to namespace IndexedDB storage and sync rooms.
|
|
35
|
+
|
|
36
|
+
## Components
|
|
37
|
+
|
|
38
|
+
### `<NeoEditor />`
|
|
39
|
+
|
|
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.
|
|
41
|
+
|
|
42
|
+
| Prop | Type | Default | Description |
|
|
43
|
+
| ------------- | ------------------------------------------------- | -------- | --------------------------------------------------------------- |
|
|
44
|
+
| `id` | `string` | required | Unique document identifier |
|
|
45
|
+
| `offline` | `boolean` | `true` | Enable offline persistence via IndexedDB |
|
|
46
|
+
| `syncConfig` | `{ url: string; room: string }` | — | WebSocket server URL and room name for real-time collaboration |
|
|
47
|
+
| `theme` | `{ mode: 'light' \| 'dark'; [key: string]: any }` | — | Theme configuration |
|
|
48
|
+
| `renderBlock` | `(block, defaultRender) => ReactNode` | — | Intercept rendering for custom block types |
|
|
49
|
+
| `className` | `string` | — | CSS class for the outer wrapper |
|
|
50
|
+
| `children` | `ReactNode` | — | Toolbar, menus, or other UI to render inside the editor context |
|
|
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
|
+
|
|
54
|
+
**Usage with collaboration:**
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
<NeoEditor
|
|
58
|
+
id="shared-doc"
|
|
59
|
+
syncConfig={{
|
|
60
|
+
url: "wss://your-yjs-server.com",
|
|
61
|
+
room: "shared-doc",
|
|
62
|
+
}}
|
|
63
|
+
theme={{ mode: "dark" }}
|
|
64
|
+
>
|
|
65
|
+
<Aeropeak />
|
|
66
|
+
<SlashMenu />
|
|
67
|
+
<CursorOverlay />
|
|
68
|
+
</NeoEditor>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Custom block rendering:**
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
<NeoEditor
|
|
75
|
+
id="doc"
|
|
76
|
+
renderBlock={(block, defaultRender) => {
|
|
77
|
+
if (block.type === "spreadsheet") {
|
|
78
|
+
return <SpreadsheetEmbed block={block} />;
|
|
79
|
+
}
|
|
80
|
+
return defaultRender;
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `<NeoCanvas />`
|
|
86
|
+
|
|
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.
|
|
88
|
+
|
|
89
|
+
### `<BlockRenderer />`
|
|
90
|
+
|
|
91
|
+
Routes each block to the correct renderer based on its `type` field. Supports all built-in block types:
|
|
92
|
+
|
|
93
|
+
- **Text blocks:** paragraph, heading-1, heading-2, heading-3
|
|
94
|
+
- **Lists:** bullet-list, ordered-list, todo-list
|
|
95
|
+
- **Media:** image, video
|
|
96
|
+
- **Structural:** quote, callout, divider, code-block
|
|
97
|
+
|
|
98
|
+
If `renderBlock` is provided through the editor context, it is called first, giving you the chance to handle custom types before falling through to the defaults.
|
|
99
|
+
|
|
100
|
+
### `<EditableBlock />`
|
|
101
|
+
|
|
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:
|
|
103
|
+
|
|
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
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Interactive Components
|
|
114
|
+
|
|
115
|
+
### `<Aeropeak />` — Floating Toolbar
|
|
116
|
+
|
|
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.
|
|
118
|
+
|
|
119
|
+
| Prop | Type | Default | Description |
|
|
120
|
+
| ----------- | ----------------------------- | -------- | -------------------------------------------- |
|
|
121
|
+
| `children` | `ReactNode` | — | Custom toolbar content (replaces defaults) |
|
|
122
|
+
| `offset` | `number` | `10` | Vertical offset from the selection in pixels |
|
|
123
|
+
| `animation` | `'fade' \| 'scale' \| 'none'` | `'fade'` | Appearance animation |
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
// Default toolbar
|
|
127
|
+
<Aeropeak />
|
|
128
|
+
|
|
129
|
+
// Custom toolbar
|
|
130
|
+
<Aeropeak offset={12} animation="scale">
|
|
131
|
+
<AeroButton
|
|
132
|
+
icon={<strong>B</strong>}
|
|
133
|
+
label="Bold"
|
|
134
|
+
onClick={(editor) => editor.toggleMark?.('bold')}
|
|
135
|
+
/>
|
|
136
|
+
<Separator />
|
|
137
|
+
<AeroButton
|
|
138
|
+
icon={<em>I</em>}
|
|
139
|
+
label="Italic"
|
|
140
|
+
onClick={(editor) => editor.toggleMark?.('italic')}
|
|
141
|
+
/>
|
|
142
|
+
</Aeropeak>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Compound components:**
|
|
146
|
+
|
|
147
|
+
- `Aeropeak.Bold` / `Aeropeak.Italic` / `Aeropeak.Underline` / `Aeropeak.Strike` / `Aeropeak.Code` / `Aeropeak.Link` — prebuilt buttons
|
|
148
|
+
- `AeroButton` — a button that receives the editor instance when clicked
|
|
149
|
+
- `Separator` — a thin vertical divider between button groups
|
|
150
|
+
|
|
151
|
+
### `<SlashMenu />` — Command Palette
|
|
152
|
+
|
|
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 `/`.
|
|
154
|
+
|
|
155
|
+
| Prop | Type | Default | Description |
|
|
156
|
+
| ---------------- | ------------------------------- | ------- | ------------------------------------------------------ |
|
|
157
|
+
| `customCommands` | `CommandItem[]` | `[]` | Additional commands to show alongside the defaults |
|
|
158
|
+
| `filter` | `(cmd: CommandItem) => boolean` | — | Filter function to hide certain commands |
|
|
159
|
+
| `menuComponent` | `React.ComponentType` | — | Completely replace the menu UI with a custom component |
|
|
160
|
+
|
|
161
|
+
**Built-in commands:** Paragraph, Heading 1-3, Bulleted List, Ordered List, To-do List, Quote, Code Block, Divider, Callout, Image.
|
|
162
|
+
|
|
163
|
+
New blocks are inserted immediately **after** the current block.
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
<SlashMenu
|
|
167
|
+
customCommands={[
|
|
168
|
+
{
|
|
169
|
+
key: "diagram",
|
|
170
|
+
label: "Diagram",
|
|
171
|
+
icon: <span>chart</span>,
|
|
172
|
+
execute: (editor) => editor.addBlock("image"),
|
|
173
|
+
},
|
|
174
|
+
]}
|
|
175
|
+
/>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### `<PDFDropZone />`
|
|
179
|
+
|
|
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.
|
|
181
|
+
|
|
182
|
+
| Prop | Type | Default | Description |
|
|
183
|
+
| --------------- | ------------------------------------------- | ------- | ------------------------------------------------------------------------------------ |
|
|
184
|
+
| `onDrop` | `(files: File[]) => void` | — | Callback when files are dropped. If provided, the default PDF extraction is skipped. |
|
|
185
|
+
| `renderOverlay` | `(props: { isOver: boolean }) => ReactNode` | — | Custom overlay content shown during drag-over |
|
|
186
|
+
| `children` | `ReactNode` | — | Content inside the drop zone |
|
|
187
|
+
|
|
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
|
+
|
|
190
|
+
### `<CursorOverlay />`
|
|
191
|
+
|
|
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 |
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
<NeoEditor id="doc" syncConfig={{ url: "wss://...", room: "doc" }}>
|
|
200
|
+
<CursorOverlay />
|
|
201
|
+
</NeoEditor>
|
|
202
|
+
```
|
|
203
|
+
|
|
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 |
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Hooks
|
|
223
|
+
|
|
224
|
+
### `useEditor()`
|
|
225
|
+
|
|
226
|
+
The primary hook for interacting with the editor. Must be called inside a `<NeoEditor />`. Returns all store state and actions.
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
const {
|
|
230
|
+
blocks, // Record<string, NeoBlock>
|
|
231
|
+
rootBlocks, // string[]
|
|
232
|
+
selection, // { blockId, startOffset, endOffset }
|
|
233
|
+
addBlock, // (type, afterId?) => void
|
|
234
|
+
insertBlock, // alias for addBlock
|
|
235
|
+
insertFullBlock, // (block, afterId?) => void
|
|
236
|
+
updateBlock, // (id, partial) => void
|
|
237
|
+
deleteBlock, // (id) => void
|
|
238
|
+
moveBlock, // (id, afterId) => void
|
|
239
|
+
setBlockType, // (id, type) => void
|
|
240
|
+
toggleMark, // (mark) => void
|
|
241
|
+
setLink, // (url | null) => void
|
|
242
|
+
exportJSON, // () => { blocks, rootBlocks }
|
|
243
|
+
importJSON, // (data) => void
|
|
244
|
+
undo, // () => void
|
|
245
|
+
redo, // () => void
|
|
246
|
+
} = useEditor();
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Throws an error if called outside of `<NeoEditor />`.
|
|
250
|
+
|
|
251
|
+
### `useSelection()`
|
|
252
|
+
|
|
253
|
+
A focused hook that subscribes only to the selection state, minimizing re-renders.
|
|
254
|
+
|
|
255
|
+
```tsx
|
|
256
|
+
const selection = useSelection();
|
|
257
|
+
// { blockId: string | null, startOffset: number, endOffset: number }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### `useSyncStatus()`
|
|
261
|
+
|
|
262
|
+
Returns the current sync connection status.
|
|
263
|
+
|
|
264
|
+
```tsx
|
|
265
|
+
const status = useSyncStatus();
|
|
266
|
+
// 'connected' or 'disconnected'
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Theming
|
|
272
|
+
|
|
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:
|
|
274
|
+
|
|
275
|
+
```css
|
|
276
|
+
:root {
|
|
277
|
+
--neo-font-family: "Inter", system-ui, sans-serif;
|
|
278
|
+
--neo-font-size-body: 16px;
|
|
279
|
+
--neo-code-font: "Fira Code", "Consolas", monospace;
|
|
280
|
+
--neo-accent-color: #3b82f6;
|
|
281
|
+
--neo-bg-canvas: #ffffff;
|
|
282
|
+
--neo-text-primary: #111827;
|
|
283
|
+
--neo-text-secondary: #6b7280;
|
|
284
|
+
--neo-selection-color: #b4d5fe;
|
|
285
|
+
--neo-border-color: #e5e7eb;
|
|
286
|
+
--neo-border-radius: 4px;
|
|
287
|
+
--neo-block-spacing: 4px;
|
|
288
|
+
--neo-content-width: 800px;
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Switch modes via the `theme` prop or via the `data-theme` attribute:
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
<NeoEditor id="doc" theme={{ mode: "dark" }} />
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Exports
|
|
299
|
+
|
|
300
|
+
Everything is exported from the package root:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// Components
|
|
304
|
+
export { NeoEditor, EditorContext } from "./NeoEditor";
|
|
305
|
+
export { NeoCanvas } from "./NeoCanvas";
|
|
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/*";
|
|
319
|
+
|
|
320
|
+
// Interactive UI
|
|
321
|
+
export {
|
|
322
|
+
PDFDropZone,
|
|
323
|
+
CursorOverlay,
|
|
324
|
+
Aeropeak,
|
|
325
|
+
SlashMenu,
|
|
326
|
+
} from "./components/*";
|
|
327
|
+
|
|
328
|
+
// Hooks
|
|
329
|
+
export { useEditor, useSelection, useSyncStatus } from "./hooks";
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## License
|
|
333
|
+
|
|
334
|
+
MIT
|
|
@@ -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":"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"}
|
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"}
|