@yurikilian/lex4 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.
Files changed (2) hide show
  1. package/README.md +387 -0
  2. 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
+ [![CI](https://github.com/yurikilian/lex4/actions/workflows/ci.yml/badge.svg)](https://github.com/yurikilian/lex4/actions/workflows/ci.yml)
10
+ [![npm version](https://img.shields.io/npm/v/@yurikilian/lex4.svg)](https://www.npmjs.com/package/@yurikilian/lex4)
11
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](./LICENSE)
12
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.5-blue.svg)](https://www.typescriptlang.org/)
13
+ [![React](https://img.shields.io/badge/React-18%2B-61DAFB.svg)](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
+ ![Editor with formatted content](docs/screenshots/editor-with-content.png)
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
+ ![Empty editor](docs/screenshots/editor-empty.png)
47
+
48
+ </details>
49
+
50
+ <details>
51
+ <summary><strong>Headers & Footers</strong> — global toggle with editable regions</summary>
52
+
53
+ ![Editor with headers and footers](docs/screenshots/editor-header-footer.png)
54
+
55
+ </details>
56
+
57
+ <details>
58
+ <summary><strong>Multi-Page Document</strong> — automatic content flow across pages</summary>
59
+
60
+ ![Multi-page document](docs/screenshots/editor-multi-page.png)
61
+
62
+ </details>
63
+
64
+ <details>
65
+ <summary><strong>Toolbar</strong> — full formatting controls</summary>
66
+
67
+ ![Toolbar](docs/screenshots/toolbar.png)
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
+ ![Component tree](docs/screenshots/arch-component-tree.png)
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
+ ![Content flow](docs/screenshots/arch-content-flow.png)
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.0",
3
+ "version": "0.1.1",
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",