@zuilib/text-editor 0.0.2 → 0.2.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @zuilib/text-editor
2
2
 
3
- Markdown editor for ZUI built on [Lexical](https://lexical.dev/): rich editing with shortcuts, raw markdown mode, read-only view, checklists, and fenced code highlighting.
3
+ Markdown editor for ZUI built on [Lexical](https://lexical.dev/): rich editing with shortcuts, raw markdown mode, read-only view, checklists, fenced code highlighting, GFM tables, and an embedded drawing canvas for diagrams.
4
4
 
5
5
  ## When to use
6
6
 
@@ -22,6 +22,8 @@ Peer dependencies (install in the consuming app):
22
22
  | `@lexical/code` | `^0.35.0` |
23
23
  | `@lexical/list` | `^0.35.0` |
24
24
  | `@lexical/link` | `^0.35.0` |
25
+ | `@lexical/table` | `^0.35.0` |
26
+ | `@lexical/utils` | `^0.35.0` |
25
27
  | `@zuilib/core` | workspace / published — import core styles |
26
28
 
27
29
  ## Installation
@@ -29,7 +31,7 @@ Peer dependencies (install in the consuming app):
29
31
  ```bash
30
32
  pnpm add @zuilib/text-editor @zuilib/core \
31
33
  lexical @lexical/react @lexical/markdown @lexical/rich-text \
32
- @lexical/code @lexical/list @lexical/link
34
+ @lexical/code @lexical/list @lexical/link @lexical/table @lexical/utils
33
35
  ```
34
36
 
35
37
  Monorepo:
@@ -45,7 +47,9 @@ Monorepo:
45
47
  "@lexical/rich-text": "^0.35.0",
46
48
  "@lexical/code": "^0.35.0",
47
49
  "@lexical/list": "^0.35.0",
48
- "@lexical/link": "^0.35.0"
50
+ "@lexical/link": "^0.35.0",
51
+ "@lexical/table": "^0.35.0",
52
+ "@lexical/utils": "^0.35.0"
49
53
  }
50
54
  }
51
55
  ```
@@ -115,15 +119,62 @@ Toggle modes in parent state; the editor preserves content via an internal ref w
115
119
  | `readOnly` | `boolean` | `false` | Disables editing in Lexical modes |
116
120
  | `autoFocus` | `boolean` | `false` | Focus on mount |
117
121
  | `className` | `string` | — | Root wrapper class |
122
+ | `toolbar` | `boolean` | `true` | Formatting/insert toolbar in `edit-md` mode |
123
+ | `outline` | `boolean` | `false` | Table-of-contents sidebar listing headings |
124
+ | `foldable` | `boolean` | `true` | Collapse sections under their headings |
118
125
 
119
126
  ## Markdown features
120
127
 
121
- **Shortcuts (edit-md):** headings (`#`), lists, blockquote, links, fenced code (via `CodeBlockShortcutPlugin`), checklists (`- [ ]` via `ChecklistShortcutPlugin`).
128
+ **Shortcuts (edit-md):** headings (`#`), lists, blockquote, links, fenced code (via `CodeBlockShortcutPlugin`), checklists (`- [ ]` via `ChecklistShortcutPlugin`), tables (`| a | b |`).
122
129
 
123
- **Built-in plugins:** history (undo/redo), lists, checklists, links, markdown sync (`MarkdownSyncPlugin`), code highlighting (`CodeHighlightPlugin`).
130
+ **Built-in plugins:** history (undo/redo), lists, checklists, links, tables (`TablePlugin`), markdown sync (`MarkdownSyncPlugin`), code highlighting (`CodeHighlightPlugin`), toolbar (`ToolbarPlugin`).
124
131
 
125
132
  **Not included:** file uploads, collaborative editing, or custom Lexical node registration — extend by forking or wrapping `MarkdownEditor`.
126
133
 
134
+ ## Tables
135
+
136
+ GFM pipe tables round-trip through markdown and render artifact-style (rounded outer border, shaded header row, per-cell rules):
137
+
138
+ ```md
139
+ | Metric | Q1 | Q2 |
140
+ | --- | --- | --- |
141
+ | Revenue | $1.2M | $1.8M |
142
+ ```
143
+
144
+ Insert a 3×3 table from the toolbar, or type a `| a | b |` row. Tab/arrow navigation, cell selection, and row/column operations come from Lexical's `TablePlugin`. Inline formatting (`**bold**`, `*italic*`, `` `code` ``) works inside cells.
145
+
146
+ ## Drawing canvas
147
+
148
+ The toolbar's "Insert drawing" button embeds an Excalidraw-style canvas for simple diagrams: rectangles, ellipses, triangles, pentagons, arrows, lines, and text labels, with a stroke and fill color palette.
149
+
150
+ - Draw a shape by picking a tool and dragging; the tool returns to **Select** after each shape
151
+ - Select to move, resize (corner handles), or re-point arrows/lines (endpoint handles)
152
+ - Color swatches restyle the selected shape (or set defaults for the next one)
153
+ - `Delete`/`Backspace` removes the selected shape; drag the bottom pill to resize the canvas
154
+ - Boxes (rects/ellipses) have three built-in text slots that move with the shape: a bold **label** at the top, main **content** in the center, and a dimmer **footer** at the bottom. Double-click the top strip of a box to edit the label, the middle for content, the bottom strip for the footer (or click a selected box again, or press Enter for content). Clearing a slot removes it; the box stays.
155
+ - **Arrows bind to boxes**: draw an arrow starting or ending on a box and it attaches to the box border — moving or resizing the box drags the arrow along. Endpoints re-anchor toward the other end automatically. Drag an endpoint off a box to detach it; drop it on another box to re-attach. Deleting a box releases its arrows.
156
+ - A selected arrow shows a **direction toggle** in the toolbar: one-way (head at the end) or two-way (heads on both ends).
157
+ - A selected connector also shows a **routing toggle**: straight (freely diagonal) or **elbow** — an orthogonal path with right-angle bends. The elbow's middle segment has a drag handle to reposition the bend; its position is stored as a fraction of the span so it stays put while bound boxes move.
158
+ - **Waypoints** for arbitrary multi-segment paths: a selected connector shows dashed "+" handles at each segment midpoint — drag one to insert a bend point there. Waypoints snap to their neighbors' axes near-alignment so right angles are easy; drag freely for diagonals. Double-click a waypoint to remove it. Adding a point to an elbow connector converts its corners into editable waypoints. Selecting a routing mode from the toolbar clears waypoints.
159
+ - **Arrows and lines can carry a label**: double-click the connector (or select it and press Enter) to edit text that rides the midpoint on a small backing plate, and moves as the connector moves.
160
+ - The standalone **Text** tool is for free-floating annotations (e.g. labeling an arrow); double-click to edit
161
+
162
+ Drawings persist inside the markdown as a fenced block, so the document stays a plain string:
163
+
164
+ ````md
165
+ ```drawing
166
+ {"version":1,"height":320,"shapes":[...]}
167
+ ```
168
+ ````
169
+
170
+ In `view` mode (or `readOnly`) the canvas renders the shapes without any editing chrome.
171
+
172
+ ## Outline & section folding
173
+
174
+ `outline` docks a collapsible table-of-contents sidebar on the right: it lists the document's headings live (indented by level), scrolls to a heading on click, and highlights the section currently in view. Pure UI — nothing is added to the document. Works in `edit-md` and `view` modes (not `edit-raw`).
175
+
176
+ `foldable` (on by default) adds a chevron in the left gutter of every heading (visible on hover). Clicking it collapses the section — everything up to the next heading of the same or higher level — marked by a trailing `…` on the heading. Folding is view-layer only: the markdown string is unaffected, and fold state resets on remount. If the cursor enters a folded section (e.g. via arrow keys), it auto-expands so content can never be edited invisibly.
177
+
127
178
  ## Form integration
128
179
 
129
180
  With `@zuilib/form`:
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ElementNode, NodeKey, EditorConfig, SerializedElementNode, LexicalNode } from 'lexical';
3
- import { MultilineElementTransformer } from '@lexical/markdown';
2
+ import { ElementNode, NodeKey, EditorConfig, SerializedElementNode, LexicalNode, DecoratorNode, Spread, SerializedLexicalNode, DOMExportOutput, DOMConversionMap, LexicalEditor } from 'lexical';
3
+ import { MultilineElementTransformer, ElementTransformer } from '@lexical/markdown';
4
+ import { ReactElement } from 'react';
4
5
 
5
6
  type Props = Readonly<{
6
7
  value?: string;
@@ -10,8 +11,14 @@ type Props = Readonly<{
10
11
  className?: string;
11
12
  mode?: 'edit-raw' | 'edit-md' | 'view';
12
13
  autoFocus?: boolean;
14
+ /** Show the formatting/insert toolbar in edit-md mode (default true) */
15
+ toolbar?: boolean;
16
+ /** Show a table-of-contents sidebar listing the document's headings */
17
+ outline?: boolean;
18
+ /** Allow collapsing sections under their headings (default true) */
19
+ foldable?: boolean;
13
20
  }>;
14
- declare function MarkdownEditor({ value, onChange, placeholder, readOnly, className, mode, autoFocus, }: Props): react_jsx_runtime.JSX.Element;
21
+ declare function MarkdownEditor({ value, onChange, placeholder, readOnly, className, mode, autoFocus, toolbar, outline, foldable, }: Props): react_jsx_runtime.JSX.Element;
15
22
 
16
23
  type SerializedFrontmatterNode = SerializedElementNode;
17
24
  /**
@@ -54,4 +61,90 @@ declare function $isFrontmatterNode(node: LexicalNode | null | undefined): node
54
61
  */
55
62
  declare const FRONTMATTER: MultilineElementTransformer;
56
63
 
57
- export { $createFrontmatterNode, $isFrontmatterNode, FRONTMATTER, FrontmatterNode, MarkdownEditor, type Props as MarkdownEditorProps, type SerializedFrontmatterNode };
64
+ type DrawingShapeType = 'rect' | 'ellipse' | 'triangle' | 'pentagon' | 'arrow' | 'line' | 'text';
65
+ type DrawingShape = Readonly<{
66
+ id: string;
67
+ type: DrawingShapeType;
68
+ /** Top-left corner (rect/ellipse/text) or start point (arrow/line) */
69
+ x: number;
70
+ y: number;
71
+ /** Size (rect/ellipse) or delta to the end point (arrow/line) */
72
+ w: number;
73
+ h: number;
74
+ stroke: string;
75
+ fill: string;
76
+ strokeWidth: number;
77
+ /** Standalone text, main (center) content of a box, or connector label */
78
+ text?: string;
79
+ /** Small heading rendered at the top of a box */
80
+ label?: string;
81
+ /** Small line rendered at the bottom of a box */
82
+ footer?: string;
83
+ /** Arrow/line: id of the box the start point is attached to */
84
+ startBinding?: string;
85
+ /** Arrow/line: id of the box the end point is attached to */
86
+ endBinding?: string;
87
+ /** Arrow: render arrowheads on both ends */
88
+ bidirectional?: boolean;
89
+ /** Connector routing: right-angled elbow path instead of a straight line */
90
+ routing?: 'elbow';
91
+ /** Elbow middle-segment position as a fraction of the span (default 0.5) */
92
+ elbow?: number;
93
+ /**
94
+ * Connector: intermediate path points between start and end (absolute
95
+ * canvas coordinates). Takes precedence over `routing`.
96
+ */
97
+ waypoints?: ReadonlyArray<{
98
+ x: number;
99
+ y: number;
100
+ }>;
101
+ }>;
102
+ type DrawingData = Readonly<{
103
+ version: 1;
104
+ height: number;
105
+ shapes: readonly DrawingShape[];
106
+ }>;
107
+
108
+ type SerializedDrawingNode = Spread<{
109
+ data: string;
110
+ }, SerializedLexicalNode>;
111
+ /**
112
+ * A block-level decorator node embedding a vector drawing (rectangles,
113
+ * ellipses, arrows, lines, text) edited on an Excalidraw-style canvas.
114
+ *
115
+ * The drawing is stored as a JSON string and round-trips through markdown as
116
+ * a ```drawing fenced code block, so documents remain plain markdown.
117
+ */
118
+ declare class DrawingNode extends DecoratorNode<ReactElement> {
119
+ __data: string;
120
+ static getType(): string;
121
+ static clone(node: DrawingNode): DrawingNode;
122
+ constructor(data: string, key?: NodeKey);
123
+ static importJSON(serializedNode: SerializedDrawingNode): DrawingNode;
124
+ exportJSON(): SerializedDrawingNode;
125
+ createDOM(config: EditorConfig): HTMLElement;
126
+ updateDOM(): false;
127
+ exportDOM(): DOMExportOutput;
128
+ static importDOM(): DOMConversionMap | null;
129
+ getData(): DrawingData;
130
+ setData(data: DrawingData): void;
131
+ isInline(): false;
132
+ decorate(_editor: LexicalEditor, _config: EditorConfig): ReactElement;
133
+ }
134
+ declare function $createDrawingNode(data?: string): DrawingNode;
135
+ declare function $isDrawingNode(node: LexicalNode | null | undefined): node is DrawingNode;
136
+ /**
137
+ * Markdown transformer persisting drawings as fenced ```drawing blocks with a
138
+ * JSON payload. Must be ordered before the built-in CODE transformer so it
139
+ * claims the fence first on import.
140
+ */
141
+ declare const DRAWING: MultilineElementTransformer;
142
+
143
+ /**
144
+ * Markdown transformer for GFM tables. Adapted from the Lexical playground.
145
+ * Rows are matched line-by-line on import and stitched into a single
146
+ * TableNode; the divider row promotes the row above it to a header row.
147
+ */
148
+ declare const TABLE: ElementTransformer;
149
+
150
+ export { $createDrawingNode, $createFrontmatterNode, $isDrawingNode, $isFrontmatterNode, DRAWING, type DrawingData, DrawingNode, type DrawingShape, type DrawingShapeType, FRONTMATTER, FrontmatterNode, MarkdownEditor, type Props as MarkdownEditorProps, type SerializedDrawingNode, type SerializedFrontmatterNode, TABLE };