@yurikilian/lex4 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 +387 -0
- package/package.json +4 -2
package/README.md
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# Lex4
|
|
4
|
+
|
|
5
|
+
**A paginated A4 document editor for React**
|
|
6
|
+
|
|
7
|
+
> Meta **Lex**ical × **A4** page rules
|
|
8
|
+
|
|
9
|
+
[](https://github.com/yurikilian/lex4/actions/workflows/ci.yml)
|
|
10
|
+
[](https://www.npmjs.com/package/@yurikilian/lex4)
|
|
11
|
+
[](./LICENSE)
|
|
12
|
+
[](https://www.typescriptlang.org/)
|
|
13
|
+
[](https://react.dev/)
|
|
14
|
+
|
|
15
|
+
[Live Demo](https://yurikilian.github.io/lex4/) · [npm Package](https://www.npmjs.com/package/@yurikilian/lex4) · [Report Bug](https://github.com/yurikilian/lex4/issues)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
A paginated document editor built as a **reusable React library** on top of [Meta Lexical](https://lexical.dev/). Every page is a true discrete A4 page — no fake pages, no CSS hacks, no single-editor visual tricks.
|
|
22
|
+
|
|
23
|
+
<div align="center">
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
## ✨ Features
|
|
30
|
+
|
|
31
|
+
- **True A4 pagination** — every page is exactly 794 × 1123 CSS pixels (210mm × 297mm at 96 DPI)
|
|
32
|
+
- **Automatic content flow** — overflow splits at block boundaries, underflow pulls content back
|
|
33
|
+
- **Rich text formatting** — bold, italic, underline, strikethrough, alignment, lists, indentation
|
|
34
|
+
- **Headers & footers** — global toggle with per-page editable regions and page counters
|
|
35
|
+
- **Multiple font families** — Arial, Times New Roman, Courier New, Georgia, Verdana and more
|
|
36
|
+
- **Session history sidebar** — Word-style action timeline with full undo/redo
|
|
37
|
+
- **Serializable document model** — typed AST export/import for backend persistence
|
|
38
|
+
- **Read-only mode** — disable editing while keeping the document viewable
|
|
39
|
+
- **Zero config** — drop in the component and start editing
|
|
40
|
+
|
|
41
|
+
## 📸 Screenshots
|
|
42
|
+
|
|
43
|
+
<details>
|
|
44
|
+
<summary><strong>Empty Editor</strong> — clean A4 page ready for editing</summary>
|
|
45
|
+
|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
</details>
|
|
49
|
+
|
|
50
|
+
<details>
|
|
51
|
+
<summary><strong>Headers & Footers</strong> — global toggle with editable regions</summary>
|
|
52
|
+
|
|
53
|
+

|
|
54
|
+
|
|
55
|
+
</details>
|
|
56
|
+
|
|
57
|
+
<details>
|
|
58
|
+
<summary><strong>Multi-Page Document</strong> — automatic content flow across pages</summary>
|
|
59
|
+
|
|
60
|
+

|
|
61
|
+
|
|
62
|
+
</details>
|
|
63
|
+
|
|
64
|
+
<details>
|
|
65
|
+
<summary><strong>Toolbar</strong> — full formatting controls</summary>
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+
|
|
69
|
+
</details>
|
|
70
|
+
|
|
71
|
+
## 📦 Installation
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install @yurikilian/lex4
|
|
75
|
+
# or
|
|
76
|
+
pnpm add @yurikilian/lex4
|
|
77
|
+
# or
|
|
78
|
+
yarn add @yurikilian/lex4
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Peer Dependencies
|
|
82
|
+
|
|
83
|
+
The library requires React 18+ and Lexical 0.22+ as peer dependencies:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm install react react-dom lexical @lexical/react @lexical/rich-text @lexical/list @lexical/history @lexical/selection @lexical/utils @lexical/clipboard @lexical/html
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 🚀 Quick Start
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { Lex4Editor } from '@yurikilian/lex4';
|
|
93
|
+
import '@yurikilian/lex4/style.css';
|
|
94
|
+
|
|
95
|
+
function App() {
|
|
96
|
+
return (
|
|
97
|
+
<Lex4Editor
|
|
98
|
+
onDocumentChange={(doc) => console.log(doc)}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### With Initial Document
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
import { Lex4Editor, createEmptyDocument } from '@yurikilian/lex4';
|
|
108
|
+
import '@yurikilian/lex4/style.css';
|
|
109
|
+
|
|
110
|
+
function App() {
|
|
111
|
+
const initialDoc = createEmptyDocument();
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Lex4Editor
|
|
115
|
+
initialDocument={initialDoc}
|
|
116
|
+
headerFooterEnabled={true}
|
|
117
|
+
onDocumentChange={(doc) => saveToBackend(doc)}
|
|
118
|
+
onHeaderFooterToggle={(enabled) => console.log('Headers:', enabled)}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Read-Only Viewer
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
<Lex4Editor
|
|
128
|
+
initialDocument={savedDocument}
|
|
129
|
+
readOnly={true}
|
|
130
|
+
/>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## 📖 API Reference
|
|
134
|
+
|
|
135
|
+
### `<Lex4Editor />` Component
|
|
136
|
+
|
|
137
|
+
The main editor component. Drop it into any React application.
|
|
138
|
+
|
|
139
|
+
| Prop | Type | Default | Description |
|
|
140
|
+
|------|------|---------|-------------|
|
|
141
|
+
| `initialDocument` | `Lex4Document` | Empty document | Pre-populate the editor with saved content |
|
|
142
|
+
| `onDocumentChange` | `(doc: Lex4Document) => void` | — | Called on every document mutation |
|
|
143
|
+
| `headerFooterEnabled` | `boolean` | `false` | Initial header/footer toggle state |
|
|
144
|
+
| `onHeaderFooterToggle` | `(enabled: boolean) => void` | — | Called when the user toggles headers/footers |
|
|
145
|
+
| `readOnly` | `boolean` | `false` | Disable editing (view-only mode) |
|
|
146
|
+
| `captureHistoryShortcutsOnWindow` | `boolean` | `true` | Capture ⌘Z/⌘⇧Z at the window level |
|
|
147
|
+
| `className` | `string` | — | Additional CSS class for the editor root |
|
|
148
|
+
|
|
149
|
+
### Types
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
import type { SerializedEditorState } from 'lexical';
|
|
153
|
+
|
|
154
|
+
type PageCounterMode = 'none' | 'header' | 'footer' | 'both';
|
|
155
|
+
|
|
156
|
+
/** Top-level document state — serialize this to persist documents */
|
|
157
|
+
interface Lex4Document {
|
|
158
|
+
pages: PageState[];
|
|
159
|
+
headerFooterEnabled: boolean;
|
|
160
|
+
pageCounterMode: PageCounterMode;
|
|
161
|
+
defaultHeaderState: SerializedEditorState | null;
|
|
162
|
+
defaultFooterState: SerializedEditorState | null;
|
|
163
|
+
defaultHeaderHeight: number;
|
|
164
|
+
defaultFooterHeight: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** State for a single page */
|
|
168
|
+
interface PageState {
|
|
169
|
+
id: string;
|
|
170
|
+
bodyState: SerializedEditorState | null;
|
|
171
|
+
headerState: SerializedEditorState | null;
|
|
172
|
+
footerState: SerializedEditorState | null;
|
|
173
|
+
headerHeight: number;
|
|
174
|
+
footerHeight: number;
|
|
175
|
+
bodySyncVersion: number;
|
|
176
|
+
headerSyncVersion: number;
|
|
177
|
+
footerSyncVersion: number;
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Helper Functions
|
|
182
|
+
|
|
183
|
+
| Function | Signature | Description |
|
|
184
|
+
|----------|-----------|-------------|
|
|
185
|
+
| `createEmptyDocument()` | `() => Lex4Document` | Creates a blank document with one empty A4 page |
|
|
186
|
+
| `createEmptyPage()` | `(id?: string) => PageState` | Creates a single empty page |
|
|
187
|
+
|
|
188
|
+
### Constants
|
|
189
|
+
|
|
190
|
+
| Constant | Value | Description |
|
|
191
|
+
|----------|-------|-------------|
|
|
192
|
+
| `A4_WIDTH_PX` | `794` | A4 width in CSS pixels at 96 DPI |
|
|
193
|
+
| `A4_HEIGHT_PX` | `1123` | A4 height in CSS pixels at 96 DPI |
|
|
194
|
+
| `A4_WIDTH_MM` | `210` | A4 width in millimeters |
|
|
195
|
+
| `A4_HEIGHT_MM` | `297` | A4 height in millimeters |
|
|
196
|
+
| `MAX_HEADER_HEIGHT_PX` | `225` | Maximum header height (20% of page) |
|
|
197
|
+
| `MAX_FOOTER_HEIGHT_PX` | `225` | Maximum footer height (20% of page) |
|
|
198
|
+
|
|
199
|
+
### Hooks
|
|
200
|
+
|
|
201
|
+
These hooks are exported for advanced use cases where you need to build custom page layouts:
|
|
202
|
+
|
|
203
|
+
| Hook | Description |
|
|
204
|
+
|------|-------------|
|
|
205
|
+
| `usePagination` | Core pagination logic — overflow/underflow detection and page management |
|
|
206
|
+
| `useOverflowDetection` | Monitors content height and triggers reflow when content exceeds the page body |
|
|
207
|
+
| `useHeaderFooter` | Header/footer state management and chrome template application |
|
|
208
|
+
|
|
209
|
+
## 🏗️ Architecture
|
|
210
|
+
|
|
211
|
+
### Multi-Editor Discrete Page Model
|
|
212
|
+
|
|
213
|
+
Unlike most web-based "paginated" editors that use a single editor with CSS visual breaks, Lex4 uses a **true multi-editor architecture** where each page body is an independent Lexical editor instance coordinated by a unified document state:
|
|
214
|
+
|
|
215
|
+
<div align="center">
|
|
216
|
+
|
|
217
|
+

|
|
218
|
+
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
### Content Flow Engine
|
|
222
|
+
|
|
223
|
+
The pagination engine is built as **pure functions** that transform page state arrays:
|
|
224
|
+
|
|
225
|
+
<div align="center">
|
|
226
|
+
|
|
227
|
+

|
|
228
|
+
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
### Key Invariants
|
|
232
|
+
|
|
233
|
+
- Every page is **exactly** A4 (794 × 1123 px at 96 DPI) — no dynamic heights
|
|
234
|
+
- Header and footer regions **never** overlap body content
|
|
235
|
+
- Overflow always creates **full A4 pages**, never partial pages
|
|
236
|
+
- At least **one page** always exists — the document is never empty
|
|
237
|
+
- Blocks move **whole** between pages (no mid-block splitting)
|
|
238
|
+
|
|
239
|
+
## 📁 Project Structure
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
lex4/
|
|
243
|
+
├── packages/
|
|
244
|
+
│ └── editor/ # lex4 — the publishable library
|
|
245
|
+
│ ├── src/
|
|
246
|
+
│ │ ├── components/ # React components (Lex4Editor, PageView, Toolbar, etc.)
|
|
247
|
+
│ │ ├── constants/ # A4 dimensions, layout math
|
|
248
|
+
│ │ ├── context/ # DocumentProvider, document reducer, actions
|
|
249
|
+
│ │ ├── engine/ # Pagination logic — pure functions (reflow, overflow, paginate)
|
|
250
|
+
│ │ ├── hooks/ # usePagination, useOverflowDetection, useHeaderFooter
|
|
251
|
+
│ │ ├── lexical/ # Editor config, plugins (paste, history), custom commands
|
|
252
|
+
│ │ ├── types/ # TypeScript interfaces (Lex4Document, PageState, etc.)
|
|
253
|
+
│ │ └── utils/ # Editor state manipulation helpers
|
|
254
|
+
│ └── dist/ # Built output (ESM + CJS + types + CSS)
|
|
255
|
+
├── demo/ # Demo app (deployed to GitHub Pages)
|
|
256
|
+
├── e2e/ # Playwright end-to-end tests
|
|
257
|
+
├── .github/workflows/ # CI, npm publish, GitHub Pages deployment
|
|
258
|
+
└── docs/screenshots/ # Auto-generated screenshots for README
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## 🛠️ Development
|
|
262
|
+
|
|
263
|
+
### Prerequisites
|
|
264
|
+
|
|
265
|
+
- **Node.js** ≥ 18
|
|
266
|
+
- **pnpm** ≥ 9
|
|
267
|
+
|
|
268
|
+
### Setup
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
# Clone the repo
|
|
272
|
+
git clone https://github.com/yurikilian/lex4.git
|
|
273
|
+
cd lex4
|
|
274
|
+
|
|
275
|
+
# Install dependencies
|
|
276
|
+
pnpm install
|
|
277
|
+
|
|
278
|
+
# Build the library
|
|
279
|
+
pnpm build
|
|
280
|
+
|
|
281
|
+
# Start the demo app at http://localhost:3000
|
|
282
|
+
pnpm dev
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Commands
|
|
286
|
+
|
|
287
|
+
| Command | Description |
|
|
288
|
+
|---------|-------------|
|
|
289
|
+
| `pnpm dev` | Start the demo app dev server |
|
|
290
|
+
| `pnpm build` | Build the `@yurikilian/lex4` library |
|
|
291
|
+
| `pnpm test` | Run unit tests (Vitest) |
|
|
292
|
+
| `pnpm test:e2e` | Run E2E tests (Playwright) |
|
|
293
|
+
| `pnpm lint` | Type-check all packages |
|
|
294
|
+
|
|
295
|
+
### Running E2E Tests
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
# Install Playwright browsers (first time only)
|
|
299
|
+
pnpm --filter e2e exec playwright install chromium
|
|
300
|
+
|
|
301
|
+
# Run all E2E tests
|
|
302
|
+
pnpm test:e2e
|
|
303
|
+
|
|
304
|
+
# Run with headed browser
|
|
305
|
+
pnpm --filter e2e test:headed
|
|
306
|
+
|
|
307
|
+
# Run with Playwright UI
|
|
308
|
+
pnpm --filter e2e test:ui
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Test Suite
|
|
312
|
+
|
|
313
|
+
| Category | Framework | Count | Description |
|
|
314
|
+
|----------|-----------|-------|-------------|
|
|
315
|
+
| Unit | Vitest | 89 | Engine logic, reducers, utilities, component rendering |
|
|
316
|
+
| E2E | Playwright | 80 | Full user flows — typing, formatting, pagination, header/footer, history |
|
|
317
|
+
|
|
318
|
+
## 🔧 Build & Bundle
|
|
319
|
+
|
|
320
|
+
The library is built with **Vite in library mode**, producing:
|
|
321
|
+
|
|
322
|
+
| Output | Path | Description |
|
|
323
|
+
|--------|------|-------------|
|
|
324
|
+
| ESM | `dist/lex4-editor.js` | ES module for modern bundlers |
|
|
325
|
+
| CJS | `dist/lex4-editor.cjs` | CommonJS for Node.js / legacy bundlers |
|
|
326
|
+
| Types | `dist/index.d.ts` | Full TypeScript declarations |
|
|
327
|
+
| CSS | `dist/style.css` | Compiled Tailwind styles |
|
|
328
|
+
| Source maps | `dist/*.map` | Debugging support |
|
|
329
|
+
|
|
330
|
+
React, ReactDOM, and all `@lexical/*` packages are **externalized** — they are not bundled and must be provided by the consuming application.
|
|
331
|
+
|
|
332
|
+
## 🚢 Publishing to npm
|
|
333
|
+
|
|
334
|
+
Releases are automated via GitHub Actions. To publish a new version:
|
|
335
|
+
|
|
336
|
+
1. Update the version in `packages/editor/package.json`
|
|
337
|
+
2. Commit and push to `main`
|
|
338
|
+
3. Create a [GitHub Release](https://github.com/yurikilian/lex4/releases/new) with a tag matching the version (e.g. `v0.2.0`)
|
|
339
|
+
4. The publish workflow runs CI, then publishes to npm with provenance
|
|
340
|
+
|
|
341
|
+
> **Note:** You need to add an `NPM_TOKEN` secret to the repository settings.
|
|
342
|
+
|
|
343
|
+
## 🌐 Demo Deployment
|
|
344
|
+
|
|
345
|
+
The demo app is automatically deployed to **GitHub Pages** on every push to `main`:
|
|
346
|
+
|
|
347
|
+
🔗 **[https://yurikilian.github.io/lex4/](https://yurikilian.github.io/lex4/)**
|
|
348
|
+
|
|
349
|
+
To deploy manually, trigger the workflow from the Actions tab.
|
|
350
|
+
|
|
351
|
+
## 🧩 Tech Stack
|
|
352
|
+
|
|
353
|
+
| Technology | Role |
|
|
354
|
+
|------------|------|
|
|
355
|
+
| [TypeScript](https://www.typescriptlang.org/) | Static typing |
|
|
356
|
+
| [React 18](https://react.dev/) | UI framework |
|
|
357
|
+
| [Meta Lexical](https://lexical.dev/) | Rich text editing engine |
|
|
358
|
+
| [Vite](https://vitejs.dev/) | Library build (ESM + CJS) and dev server |
|
|
359
|
+
| [Tailwind CSS](https://tailwindcss.com/) | Styling |
|
|
360
|
+
| [Vitest](https://vitest.dev/) | Unit testing |
|
|
361
|
+
| [Playwright](https://playwright.dev/) | End-to-end testing |
|
|
362
|
+
| [pnpm](https://pnpm.io/) | Package manager (monorepo workspaces) |
|
|
363
|
+
| [GitHub Actions](https://github.com/features/actions) | CI/CD, npm publish, Pages deployment |
|
|
364
|
+
|
|
365
|
+
## ⚠️ Known Limitations
|
|
366
|
+
|
|
367
|
+
| Limitation | Details |
|
|
368
|
+
|------------|---------|
|
|
369
|
+
| **No mid-block splitting** | Blocks (paragraphs, list items) move whole between pages. A single block larger than a page body will overflow visually. |
|
|
370
|
+
| **Heuristic initial pagination** | Block heights are estimated at 24px per line until the first render. `ResizeObserver` corrects this on mount. |
|
|
371
|
+
| **No collaborative editing** | The document model is designed for single-user editing. Real-time collaboration (e.g. CRDT/OT) is out of scope. |
|
|
372
|
+
| **No table support** | Tables are not supported as block types. |
|
|
373
|
+
|
|
374
|
+
## 🤝 Contributing
|
|
375
|
+
|
|
376
|
+
Contributions are welcome! Please:
|
|
377
|
+
|
|
378
|
+
1. Fork the repository
|
|
379
|
+
2. Create a feature branch (`git checkout -b feat/my-feature`)
|
|
380
|
+
3. Commit your changes with clear messages
|
|
381
|
+
4. Push to your fork and open a Pull Request
|
|
382
|
+
|
|
383
|
+
Please ensure `pnpm lint && pnpm build && pnpm test` pass before submitting.
|
|
384
|
+
|
|
385
|
+
## 📄 License
|
|
386
|
+
|
|
387
|
+
[MIT](./LICENSE) © [Yuri Kilian](https://github.com/yurikilian)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yurikilian/lex4",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"./style.css": "./dist/style.css"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
|
-
"dist"
|
|
38
|
+
"dist",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
39
41
|
],
|
|
40
42
|
"peerDependencies": {
|
|
41
43
|
"react": "^18.0.0 || ^19.0.0",
|