@zuilib/text-editor 0.2.0 → 0.3.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/CHANGELOG.md +58 -0
- package/DRAWING_FORMAT.md +149 -0
- package/README.md +208 -130
- package/dist/index.d.ts +254 -9
- package/dist/index.js +956 -708
- package/dist/styles.css +6 -0
- package/package.json +10 -8
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Changelog — @zuilib/text-editor
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- **Custom toolbars**
|
|
6
|
+
- `toolbar` prop accepts a render function receiving the default groups
|
|
7
|
+
(`format`, `insert`, `history`) so you can add your own buttons
|
|
8
|
+
- Compound components: `MarkdownEditor.Root` / `.Content` / `.Toolbar` /
|
|
9
|
+
`.Outline` plus `.FormatButtons` / `.InsertButtons` / `.HistoryButtons` /
|
|
10
|
+
`.ToolbarButton` / `.ToolbarDivider` — place the toolbar anywhere under
|
|
11
|
+
`Root`
|
|
12
|
+
- `useMarkdownEditor()` headless hook: `editor`, `activeFormats`,
|
|
13
|
+
`toggleFormat`, `insertTable`, `insertDrawing`, `undo`/`redo`
|
|
14
|
+
- Undo/redo buttons (`HistoryButtons`; in the default toolbar via the
|
|
15
|
+
function form only — default toolbar layout unchanged)
|
|
16
|
+
- Internal: `LexicalComposer` is now mounted in `edit-raw` mode too, so
|
|
17
|
+
hooks keep working across mode switches
|
|
18
|
+
|
|
19
|
+
## 0.2.1
|
|
20
|
+
|
|
21
|
+
- `DRAWING_FORMAT.md`: authoritative spec of the ```drawing JSON payload,
|
|
22
|
+
written for programmatic/LLM consumption; ships in the npm package
|
|
23
|
+
- New export `DRAWING_DATA_JSON_SCHEMA` (JSON Schema draft-07) for
|
|
24
|
+
validating or structured-output-generating drawing payloads
|
|
25
|
+
- New exports `parseDrawingData` / `serializeDrawingData`
|
|
26
|
+
- This changelog; README overhaul
|
|
27
|
+
|
|
28
|
+
## 0.2.0
|
|
29
|
+
|
|
30
|
+
- **Outline** (`outline` prop, default off): docked, collapsible
|
|
31
|
+
table-of-contents sidebar — live heading list, click-to-scroll, current
|
|
32
|
+
section highlighted
|
|
33
|
+
- **Section folding** (`foldable` prop, default on): collapse everything
|
|
34
|
+
under a heading via a gutter chevron; view-layer only (markdown
|
|
35
|
+
untouched); auto-expands if the cursor enters a folded section
|
|
36
|
+
|
|
37
|
+
## 0.1.0
|
|
38
|
+
|
|
39
|
+
- **Tables**: GFM pipe tables round-trip through markdown; artifact-style
|
|
40
|
+
rendering; in-place editing via Lexical `TablePlugin`; new peer deps
|
|
41
|
+
`@lexical/table` and `@lexical/utils`
|
|
42
|
+
- **Toolbar** (`toolbar` prop, default on): inline formatting, insert
|
|
43
|
+
table, insert drawing
|
|
44
|
+
- **Drawing canvas**: Excalidraw-style diagrams embedded as ```drawing
|
|
45
|
+
fenced JSON blocks — see `DRAWING_FORMAT.md`
|
|
46
|
+
- Shapes: rectangle, ellipse, triangle, pentagon, arrow, line, text
|
|
47
|
+
- Boxes are cards with three attached text slots (label / content /
|
|
48
|
+
footer) that move, resize, and wrap with the shape
|
|
49
|
+
- Connectors bind to boxes and track them; one-way or two-way arrows;
|
|
50
|
+
midpoint labels; straight, elbow, or waypoint-routed paths
|
|
51
|
+
- Stroke/fill palettes; dark mode via canvas color inversion
|
|
52
|
+
- Round-trip test suite (`pnpm test`)
|
|
53
|
+
|
|
54
|
+
## 0.0.2 and earlier
|
|
55
|
+
|
|
56
|
+
- Markdown editor wrapping Lexical: `edit-md` / `edit-raw` / `view` modes,
|
|
57
|
+
markdown shortcuts, checklists, fenced code highlighting, YAML
|
|
58
|
+
frontmatter block, controlled `value`/`onChange`
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# The `drawing` block format
|
|
2
|
+
|
|
3
|
+
`@zuilib/text-editor` documents are plain markdown. Diagrams are embedded as
|
|
4
|
+
a fenced code block with the language `drawing`, containing a single JSON
|
|
5
|
+
object:
|
|
6
|
+
|
|
7
|
+
````md
|
|
8
|
+
```drawing
|
|
9
|
+
{"version":1,"height":320,"shapes":[ ... ]}
|
|
10
|
+
```
|
|
11
|
+
````
|
|
12
|
+
|
|
13
|
+
This document is the authoritative spec of that JSON payload. It is written
|
|
14
|
+
so it can be handed to a program or an LLM that needs to **generate or edit
|
|
15
|
+
drawings**. A machine-readable JSON Schema of the same rules is exported
|
|
16
|
+
from the package as `DRAWING_DATA_JSON_SCHEMA`.
|
|
17
|
+
|
|
18
|
+
Malformed payloads never crash the editor: anything that fails validation
|
|
19
|
+
degrades to an empty canvas (invalid shapes are dropped individually).
|
|
20
|
+
|
|
21
|
+
## Coordinate system
|
|
22
|
+
|
|
23
|
+
- Origin `(0,0)` is the canvas's top-left; x grows right, y grows down.
|
|
24
|
+
- Units are CSS pixels.
|
|
25
|
+
- The canvas is as wide as the editor (fluid). Keep shapes within roughly
|
|
26
|
+
x ∈ [0, 700] to be safe on typical layouts.
|
|
27
|
+
- `height` is the canvas height in pixels (minimum 80; 300–400 is typical).
|
|
28
|
+
|
|
29
|
+
## Top-level object
|
|
30
|
+
|
|
31
|
+
| Field | Type | Notes |
|
|
32
|
+
|-----------|-----------|-----------------------------------------|
|
|
33
|
+
| `version` | `1` | Literal `1` |
|
|
34
|
+
| `height` | `number` | Canvas height in px |
|
|
35
|
+
| `shapes` | `Shape[]` | Render order: later shapes draw on top |
|
|
36
|
+
|
|
37
|
+
## Shape object
|
|
38
|
+
|
|
39
|
+
All shapes share these required fields:
|
|
40
|
+
|
|
41
|
+
| Field | Type | Meaning |
|
|
42
|
+
|---------------|----------|--------------------------------------------------------------------|
|
|
43
|
+
| `id` | `string` | Unique within the drawing; bindings reference it |
|
|
44
|
+
| `type` | `string` | `rect` \| `ellipse` \| `triangle` \| `pentagon` \| `arrow` \| `line` \| `text` |
|
|
45
|
+
| `x`, `y` | `number` | Boxes/text: top-left of the bounding box. Connectors: start point. |
|
|
46
|
+
| `w`, `h` | `number` | Boxes/text: size (non-negative). Connectors: delta to the end point (`end = (x+w, y+h)`; may be negative). |
|
|
47
|
+
| `stroke` | `string` | CSS color of the outline and of all text on the shape |
|
|
48
|
+
| `fill` | `string` | CSS color of the interior; `"transparent"` for none |
|
|
49
|
+
| `strokeWidth` | `number` | Use `2` |
|
|
50
|
+
|
|
51
|
+
### Boxes (`rect`, `ellipse`, `triangle`, `pentagon`) — "cards"
|
|
52
|
+
|
|
53
|
+
Boxes are card-like: they can carry up to three text slots that render
|
|
54
|
+
inside the shape and move/resize/wrap with it:
|
|
55
|
+
|
|
56
|
+
| Field | Renders as |
|
|
57
|
+
|----------|-----------------------------------------------|
|
|
58
|
+
| `label` | Small bold heading at the top of the box |
|
|
59
|
+
| `text` | Main content, centered |
|
|
60
|
+
| `footer` | Small dim line at the bottom |
|
|
61
|
+
|
|
62
|
+
Text wraps automatically to the box width; `\n` forces a line break.
|
|
63
|
+
Triangles are apex-up isoceles; pentagons are apex-up, inscribed in the
|
|
64
|
+
bounding box. Prefer these slots over floating `text` shapes — slot text is
|
|
65
|
+
attached to the card.
|
|
66
|
+
|
|
67
|
+
### `text` — free-floating annotation
|
|
68
|
+
|
|
69
|
+
`text` holds the content. `w`/`h` are advisory (the editor recomputes them
|
|
70
|
+
from the content); position is the top-left of the first line. Use only for
|
|
71
|
+
annotations that belong to no box (the editor deletes a text shape when its
|
|
72
|
+
content is emptied).
|
|
73
|
+
|
|
74
|
+
### Connectors (`arrow`, `line`)
|
|
75
|
+
|
|
76
|
+
Geometry: from `(x, y)` to `(x+w, y+h)`. `arrow` has a head at the end;
|
|
77
|
+
`line` has none.
|
|
78
|
+
|
|
79
|
+
| Field | Type | Meaning |
|
|
80
|
+
|-----------------|------------|-------------------------------------------------------------------------|
|
|
81
|
+
| `text` | `string` | Label rendered at the path midpoint on a small backing plate |
|
|
82
|
+
| `bidirectional` | `boolean` | Arrows only: heads on both ends |
|
|
83
|
+
| `startBinding` | `string` | id of the box the start attaches to |
|
|
84
|
+
| `endBinding` | `string` | id of the box the end attaches to |
|
|
85
|
+
| `routing` | `"elbow"` | Orthogonal auto-routing with up to two right-angle bends |
|
|
86
|
+
| `elbow` | `number` | 0–1: middle-segment position along the span (default 0.5) |
|
|
87
|
+
| `waypoints` | `Point[]` | Explicit intermediate path points (`{x,y}`), ordered start→end |
|
|
88
|
+
|
|
89
|
+
**Bindings.** When `startBinding`/`endBinding` name a box id, the editor
|
|
90
|
+
re-anchors that endpoint onto the box's border (aimed at the other end, with
|
|
91
|
+
a small gap) and keeps it attached as the box moves. Generated endpoints
|
|
92
|
+
therefore only need to be approximately right — place them near the intended
|
|
93
|
+
boxes and the resolver snaps them. Don't bind both ends of one connector to
|
|
94
|
+
the same box. Bindings to ids that don't exist are silently dropped.
|
|
95
|
+
|
|
96
|
+
**Routing.** Omit `routing` for a straight (possibly diagonal) connector.
|
|
97
|
+
`"elbow"` renders an orthogonal path: it runs along the dominant axis, turns
|
|
98
|
+
through a perpendicular middle segment (positioned by `elbow`), and turns
|
|
99
|
+
again. If `waypoints` is non-empty it defines the path (`start → waypoints…
|
|
100
|
+
→ end`) and `routing`/`elbow` are ignored. Waypoints are absolute canvas
|
|
101
|
+
coordinates; align consecutive points on x or y to get right angles.
|
|
102
|
+
|
|
103
|
+
## Recommended colors
|
|
104
|
+
|
|
105
|
+
Any CSS color works; the editor's palette (dark-mode safe — the canvas
|
|
106
|
+
inverts colors Excalidraw-style in dark themes):
|
|
107
|
+
|
|
108
|
+
- Strokes: `#1e1e1e` (default), `#e03131` red, `#2f9e44` green, `#1971c2` blue, `#f08c00` orange
|
|
109
|
+
- Fills: `transparent`, `#ffc9c9`, `#b2f2bb`, `#a5d8ff`, `#ffec99`
|
|
110
|
+
|
|
111
|
+
Pair a colored stroke with its pastel fill (`#1971c2` + `#a5d8ff`, etc.).
|
|
112
|
+
|
|
113
|
+
## Examples
|
|
114
|
+
|
|
115
|
+
Two bound cards with a labeled arrow:
|
|
116
|
+
|
|
117
|
+
````md
|
|
118
|
+
```drawing
|
|
119
|
+
{"version":1,"height":260,"shapes":[
|
|
120
|
+
{"id":"web","type":"rect","x":40,"y":70,"w":170,"h":100,"stroke":"#1971c2","fill":"#a5d8ff","strokeWidth":2,"label":"CLIENT","text":"Web App","footer":"React"},
|
|
121
|
+
{"id":"api","type":"rect","x":330,"y":70,"w":170,"h":100,"stroke":"#2f9e44","fill":"#b2f2bb","strokeWidth":2,"label":"SERVICE","text":"API","footer":"Kotlin"},
|
|
122
|
+
{"id":"e1","type":"arrow","x":216,"y":120,"w":108,"h":0,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"startBinding":"web","endBinding":"api","text":"REST"}
|
|
123
|
+
]}
|
|
124
|
+
```
|
|
125
|
+
````
|
|
126
|
+
|
|
127
|
+
An elbow arrow and a hand-routed multi-bend line:
|
|
128
|
+
|
|
129
|
+
````md
|
|
130
|
+
```drawing
|
|
131
|
+
{"version":1,"height":300,"shapes":[
|
|
132
|
+
{"id":"a","type":"rect","x":40,"y":40,"w":150,"h":80,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"text":"A"},
|
|
133
|
+
{"id":"b","type":"ellipse","x":420,"y":180,"w":150,"h":80,"stroke":"#e03131","fill":"#ffc9c9","strokeWidth":2,"text":"B"},
|
|
134
|
+
{"id":"e1","type":"arrow","x":196,"y":80,"w":224,"h":140,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"startBinding":"a","endBinding":"b","routing":"elbow"},
|
|
135
|
+
{"id":"e2","type":"line","x":115,"y":126,"w":380,"h":54,"stroke":"#1971c2","fill":"transparent","strokeWidth":2,"startBinding":"a","endBinding":"b","waypoints":[{"x":115,"y":260},{"x":495,"y":260}]}
|
|
136
|
+
]}
|
|
137
|
+
```
|
|
138
|
+
````
|
|
139
|
+
|
|
140
|
+
## Generation checklist
|
|
141
|
+
|
|
142
|
+
1. One JSON object, single ```drawing fence, no comments or trailing commas.
|
|
143
|
+
2. Every `id` unique; every binding references an existing box id.
|
|
144
|
+
3. Boxes ≥ 120×70 when they carry text slots; leave ~60px between cards for
|
|
145
|
+
connectors and labels.
|
|
146
|
+
4. Box text goes in `label`/`text`/`footer` — not in floating `text` shapes.
|
|
147
|
+
5. Connectors between cards should bind both ends; approximate endpoints are
|
|
148
|
+
fine (the editor snaps them to borders).
|
|
149
|
+
6. `height` large enough to contain every shape plus ~20px margin.
|
package/README.md
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
# @zuilib/text-editor
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
3
|
+
A markdown editor for ZUI built on [Lexical](https://lexical.dev/). Plain
|
|
4
|
+
markdown in, plain markdown out — with an artifact-style editing experience
|
|
5
|
+
on top: rich tables, an embedded Excalidraw-style diagram canvas, a
|
|
6
|
+
document outline, and collapsible sections.
|
|
7
|
+
|
|
8
|
+
**Features**
|
|
9
|
+
|
|
10
|
+
- **Three modes**: rich WYSIWYG (`edit-md`), raw markdown (`edit-raw`),
|
|
11
|
+
read-only render (`view`) — all driven by one controlled `value` string
|
|
12
|
+
- **Markdown shortcuts** while typing: headings, lists, checklists,
|
|
13
|
+
blockquotes, links, fenced code (with syntax highlighting), tables
|
|
14
|
+
- **Tables**: GFM pipe tables, edited in place (tab between cells, ranges,
|
|
15
|
+
inline formatting), styled like Claude artifacts
|
|
16
|
+
- **Diagrams**: a drawing canvas embedded in the document — cards with
|
|
17
|
+
attached text slots, bound arrows that follow their cards, elbow and
|
|
18
|
+
multi-waypoint routing, labels, color palettes, dark mode
|
|
19
|
+
- **Outline**: optional table-of-contents sidebar with click-to-scroll and
|
|
20
|
+
current-section highlight
|
|
21
|
+
- **Section folding**: collapse everything under a heading, view-layer only
|
|
22
|
+
- **Toolbar**: inline formatting plus insert-table / insert-drawing
|
|
23
|
+
- YAML **frontmatter** block support (`---` at the top)
|
|
24
|
+
|
|
25
|
+
Everything round-trips through the markdown string: tables as GFM,
|
|
26
|
+
diagrams as ` ```drawing ` fenced JSON ([format spec](./DRAWING_FORMAT.md)),
|
|
27
|
+
frontmatter as `---` blocks. Feature history: [CHANGELOG](./CHANGELOG.md).
|
|
28
28
|
|
|
29
29
|
## Installation
|
|
30
30
|
|
|
@@ -34,42 +34,8 @@ pnpm add @zuilib/text-editor @zuilib/core \
|
|
|
34
34
|
@lexical/code @lexical/list @lexical/link @lexical/table @lexical/utils
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
```json
|
|
40
|
-
{
|
|
41
|
-
"dependencies": {
|
|
42
|
-
"@zuilib/text-editor": "workspace:*",
|
|
43
|
-
"@zuilib/core": "workspace:*",
|
|
44
|
-
"lexical": "^0.35.0",
|
|
45
|
-
"@lexical/react": "^0.35.0",
|
|
46
|
-
"@lexical/markdown": "^0.35.0",
|
|
47
|
-
"@lexical/rich-text": "^0.35.0",
|
|
48
|
-
"@lexical/code": "^0.35.0",
|
|
49
|
-
"@lexical/list": "^0.35.0",
|
|
50
|
-
"@lexical/link": "^0.35.0",
|
|
51
|
-
"@lexical/table": "^0.35.0",
|
|
52
|
-
"@lexical/utils": "^0.35.0"
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## Import rules
|
|
58
|
-
|
|
59
|
-
| Rule | Detail |
|
|
60
|
-
|------|--------|
|
|
61
|
-
| **Named export** | `import { MarkdownEditor } from '@zuilib/text-editor'` |
|
|
62
|
-
| **Types** | `import type { MarkdownEditorProps } from '@zuilib/text-editor'` |
|
|
63
|
-
| **Styles** | `import '@zuilib/text-editor/styles.css'` and `@zuilib/core/styles.css` |
|
|
64
|
-
| **Controlled** | Always pass `value` + `onChange` for persisted content |
|
|
65
|
-
| **Mode switches** | Switching `edit-raw` → `edit-md` re-mounts Lexical with latest markdown (cursor-safe) |
|
|
66
|
-
|
|
67
|
-
## Exports
|
|
68
|
-
|
|
69
|
-
| Subpath | Exports |
|
|
70
|
-
|---------|---------|
|
|
71
|
-
| `@zuilib/text-editor` | `MarkdownEditor` (default re-exported as named), `MarkdownEditorProps` |
|
|
72
|
-
| `@zuilib/text-editor/styles.css` | Editor chrome + Lexical theme classes |
|
|
37
|
+
All `lexical`/`@lexical/*` packages are peer dependencies at `^0.35.0`;
|
|
38
|
+
`react`/`react-dom` at `^18 || ^19`.
|
|
73
39
|
|
|
74
40
|
## Quick start
|
|
75
41
|
|
|
@@ -81,32 +47,12 @@ import { MarkdownEditor } from '@zuilib/text-editor'
|
|
|
81
47
|
|
|
82
48
|
function Notes() {
|
|
83
49
|
const [value, setValue] = useState('# Hello\n\n- [ ] Try checklists')
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<MarkdownEditor
|
|
87
|
-
value={value}
|
|
88
|
-
onChange={setValue}
|
|
89
|
-
placeholder="Start writing…"
|
|
90
|
-
/>
|
|
91
|
-
)
|
|
50
|
+
return <MarkdownEditor value={value} onChange={setValue} outline />
|
|
92
51
|
}
|
|
93
52
|
```
|
|
94
53
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
| `mode` | UI | `onChange` |
|
|
98
|
-
|--------|-----|------------|
|
|
99
|
-
| `'edit-md'` (default) | Lexical rich editor + markdown shortcuts | Emits markdown string |
|
|
100
|
-
| `'edit-raw'` | Plain `<textarea>` with markdown source | Emits markdown string |
|
|
101
|
-
| `'view'` | Read-only rendered content | No editing (`onChange` unused) |
|
|
102
|
-
|
|
103
|
-
```tsx
|
|
104
|
-
<MarkdownEditor mode="edit-md" value={md} onChange={setMd} />
|
|
105
|
-
<MarkdownEditor mode="edit-raw" value={md} onChange={setMd} />
|
|
106
|
-
<MarkdownEditor mode="view" value={md} />
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Toggle modes in parent state; the editor preserves content via an internal ref when remounting Lexical after raw editing.
|
|
54
|
+
The component is controlled: pass `value` and persist what `onChange`
|
|
55
|
+
emits. The emitted string is always plain markdown.
|
|
110
56
|
|
|
111
57
|
## Props (`MarkdownEditorProps`)
|
|
112
58
|
|
|
@@ -115,25 +61,29 @@ Toggle modes in parent state; the editor preserves content via an internal ref w
|
|
|
115
61
|
| `value` | `string` | `''` | Markdown source (controlled) |
|
|
116
62
|
| `onChange` | `(value: string) => void` | — | Called on every edit |
|
|
117
63
|
| `mode` | `'edit-md' \| 'edit-raw' \| 'view'` | `'edit-md'` | Editing surface |
|
|
118
|
-
| `placeholder` | `string` | `'Start writing...'` | Empty
|
|
64
|
+
| `placeholder` | `string` | `'Start writing...'` | Empty-state hint |
|
|
119
65
|
| `readOnly` | `boolean` | `false` | Disables editing in Lexical modes |
|
|
120
66
|
| `autoFocus` | `boolean` | `false` | Focus on mount |
|
|
121
67
|
| `className` | `string` | — | Root wrapper class |
|
|
122
|
-
| `toolbar` | `boolean` | `true` | Formatting/insert toolbar
|
|
123
|
-
| `outline` | `boolean` | `false` | Table-of-contents sidebar
|
|
124
|
-
| `foldable` | `boolean` | `true` | Collapse sections under
|
|
125
|
-
|
|
126
|
-
## Markdown features
|
|
68
|
+
| `toolbar` | `boolean \| (items) => ReactNode` | `true` | Formatting/insert toolbar (`edit-md`); function form customises it |
|
|
69
|
+
| `outline` | `boolean` | `false` | Table-of-contents sidebar |
|
|
70
|
+
| `foldable` | `boolean` | `true` | Collapse sections under headings |
|
|
127
71
|
|
|
128
|
-
|
|
72
|
+
### Modes
|
|
129
73
|
|
|
130
|
-
|
|
74
|
+
| `mode` | UI | Notes |
|
|
75
|
+
|--------|-----|-------|
|
|
76
|
+
| `'edit-md'` | Lexical rich editor | Shortcuts, toolbar, tables, canvas |
|
|
77
|
+
| `'edit-raw'` | Plain `<textarea>` | Direct markdown source editing |
|
|
78
|
+
| `'view'` | Read-only render | Outline/folding still work |
|
|
131
79
|
|
|
132
|
-
|
|
80
|
+
Switching `edit-raw` → `edit-md` re-mounts Lexical with the latest text
|
|
81
|
+
(cursor-safe).
|
|
133
82
|
|
|
134
83
|
## Tables
|
|
135
84
|
|
|
136
|
-
|
|
85
|
+
Type a `| a | b |` row or use the toolbar's insert-table button. GFM
|
|
86
|
+
round-trip:
|
|
137
87
|
|
|
138
88
|
```md
|
|
139
89
|
| Metric | Q1 | Q2 |
|
|
@@ -141,72 +91,200 @@ GFM pipe tables round-trip through markdown and render artifact-style (rounded o
|
|
|
141
91
|
| Revenue | $1.2M | $1.8M |
|
|
142
92
|
```
|
|
143
93
|
|
|
144
|
-
|
|
94
|
+
Rendering is artifact-style (rounded outer border, shaded header row).
|
|
95
|
+
Editing is in place: Tab/arrows between cells, cell range selection, inline
|
|
96
|
+
formatting inside cells. Cell content is single-line in markdown; newlines
|
|
97
|
+
are escaped as `\n`.
|
|
145
98
|
|
|
146
|
-
##
|
|
99
|
+
## Toolbar & custom toolbars
|
|
147
100
|
|
|
148
|
-
|
|
101
|
+
In `edit-md` mode a toolbar offers inline formatting (bold, italic,
|
|
102
|
+
strikethrough, inline code) plus **Insert table** and **Insert drawing**.
|
|
103
|
+
Hide it with `toolbar={false}`.
|
|
149
104
|
|
|
150
|
-
|
|
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
|
|
105
|
+
### Extending the toolbar
|
|
161
106
|
|
|
162
|
-
|
|
107
|
+
Pass a function as `toolbar`. It receives the default button groups and
|
|
108
|
+
returns the toolbar to render. Use `MarkdownEditor.ToolbarButton` for your
|
|
109
|
+
own buttons so they match the built-ins and keep the editor selection when
|
|
110
|
+
clicked.
|
|
163
111
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
{
|
|
112
|
+
```tsx
|
|
113
|
+
<MarkdownEditor
|
|
114
|
+
value={value}
|
|
115
|
+
onChange={setValue}
|
|
116
|
+
toolbar={(items) => (
|
|
117
|
+
<MarkdownEditor.Toolbar>
|
|
118
|
+
{items.format}
|
|
119
|
+
<MarkdownEditor.ToolbarDivider />
|
|
120
|
+
{items.insert}
|
|
121
|
+
<MarkdownEditor.ToolbarDivider />
|
|
122
|
+
{items.history}
|
|
123
|
+
<MarkdownEditor.ToolbarButton label="Save" onClick={save}>
|
|
124
|
+
💾
|
|
125
|
+
</MarkdownEditor.ToolbarButton>
|
|
126
|
+
</MarkdownEditor.Toolbar>
|
|
127
|
+
)}
|
|
128
|
+
/>
|
|
167
129
|
```
|
|
168
|
-
````
|
|
169
130
|
|
|
170
|
-
|
|
131
|
+
`MarkdownEditor.Toolbar` hides itself in `view` / `edit-raw` / `readOnly`.
|
|
132
|
+
|
|
133
|
+
### Placing your own toolbar (compound components)
|
|
134
|
+
|
|
135
|
+
When the toolbar must live somewhere else in your layout (an app bar, a
|
|
136
|
+
panel header), compose the editor from its parts. Everything under
|
|
137
|
+
`MarkdownEditor.Root` shares one editor instance, so the toolbar can sit
|
|
138
|
+
anywhere in that subtree.
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<MarkdownEditor.Root value={value} onChange={setValue}>
|
|
142
|
+
<header className="app-bar">
|
|
143
|
+
<MarkdownEditor.Toolbar>
|
|
144
|
+
<MarkdownEditor.FormatButtons />
|
|
145
|
+
<MarkdownEditor.ToolbarDivider />
|
|
146
|
+
<MarkdownEditor.InsertButtons />
|
|
147
|
+
</MarkdownEditor.Toolbar>
|
|
148
|
+
<MyAppButtons />
|
|
149
|
+
</header>
|
|
150
|
+
<MarkdownEditor.Content placeholder="Write…">
|
|
151
|
+
<MarkdownEditor.Outline />
|
|
152
|
+
</MarkdownEditor.Content>
|
|
153
|
+
</MarkdownEditor.Root>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
| Part | Role |
|
|
157
|
+
|------|------|
|
|
158
|
+
| `Root` | Lexical composer + plugins. Takes `value`, `onChange`, `mode`, `readOnly`, `autoFocus`, `className` |
|
|
159
|
+
| `Content` | The editable surface. Takes `placeholder`, `foldable`; children are docked sidebars |
|
|
160
|
+
| `Toolbar` | Container; renders the default groups when empty |
|
|
161
|
+
| `FormatButtons`, `InsertButtons`, `HistoryButtons` | Built-in groups |
|
|
162
|
+
| `ToolbarButton`, `ToolbarDivider` | Primitives for your own items |
|
|
163
|
+
| `Outline` | Table-of-contents sidebar |
|
|
164
|
+
|
|
165
|
+
### Headless: `useMarkdownEditor()`
|
|
166
|
+
|
|
167
|
+
For fully custom UI (e.g. buttons in your own design system), call the hook
|
|
168
|
+
from any component rendered under `MarkdownEditor.Root`:
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
function BoldButton() {
|
|
172
|
+
const { activeFormats, toggleFormat } = useMarkdownEditor()
|
|
173
|
+
return (
|
|
174
|
+
<MyButton pressed={activeFormats.has('bold')} onClick={() => toggleFormat('bold')}>
|
|
175
|
+
B
|
|
176
|
+
</MyButton>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
It returns `editor` (the Lexical instance), `activeFormats`,
|
|
182
|
+
`toggleFormat`, `insertTable`, `insertDrawing`, `canUndo`, `canRedo`,
|
|
183
|
+
`undo`, `redo`.
|
|
184
|
+
|
|
185
|
+
## Diagrams (drawing canvas)
|
|
186
|
+
|
|
187
|
+
The toolbar's insert-drawing button embeds a canvas; the drawing persists
|
|
188
|
+
in the markdown as a ` ```drawing ` fenced JSON block, fully specified in
|
|
189
|
+
**[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)**.
|
|
190
|
+
|
|
191
|
+
- **Shapes**: rectangle, ellipse, triangle, pentagon, arrow, line, text —
|
|
192
|
+
with stroke/fill palettes. Draw by picking a tool and dragging.
|
|
193
|
+
- **Cards**: every box carries three text slots that move, resize, and wrap
|
|
194
|
+
with it — a bold **label** on top, **content** in the center, a dim
|
|
195
|
+
**footer** at the bottom. Click a selected box (or double-click its
|
|
196
|
+
top/middle/bottom strip, or press Enter) to edit a slot.
|
|
197
|
+
- **Bound connectors**: an arrow drawn from one card to another attaches to
|
|
198
|
+
both — moving a card moves its arrows, endpoints anchored to the border.
|
|
199
|
+
Drag an endpoint off/onto a card to detach/re-attach.
|
|
200
|
+
- **Routing**: straight (diagonal) by default; toggle **elbow** for right
|
|
201
|
+
angles, or drag the dashed "+" handles on a selected connector to add any
|
|
202
|
+
number of **waypoints** (they snap to neighbors' axes for clean 90°
|
|
203
|
+
bends). One-way / two-way arrowhead toggle; midpoint **labels**.
|
|
204
|
+
- **Canvas**: resizable height, dot grid, white surface that inverts
|
|
205
|
+
Excalidraw-style in dark mode (`.dark` ancestor class).
|
|
171
206
|
|
|
172
207
|
## Outline & section folding
|
|
173
208
|
|
|
174
|
-
`outline` docks a collapsible table-of-contents sidebar
|
|
209
|
+
`outline` docks a collapsible table-of-contents sidebar: live heading list
|
|
210
|
+
indented by level, click to scroll, current section highlighted. Not part
|
|
211
|
+
of the document — pure UI. Available in `edit-md` and `view`.
|
|
175
212
|
|
|
176
|
-
`foldable` (on
|
|
213
|
+
`foldable` (default on) shows a chevron in the gutter of each heading on
|
|
214
|
+
hover; clicking collapses the section (until the next heading of the same
|
|
215
|
+
or higher level). Folding never changes the markdown; fold state resets on
|
|
216
|
+
remount; a folded section auto-expands if the cursor enters it.
|
|
177
217
|
|
|
178
|
-
##
|
|
218
|
+
## For AI agents / programmatic authoring
|
|
219
|
+
|
|
220
|
+
Documents are plain markdown, so LLMs can generate them — including
|
|
221
|
+
diagrams:
|
|
179
222
|
|
|
180
|
-
|
|
223
|
+
- **[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)** is the authoritative
|
|
224
|
+
` ```drawing ` payload spec, written to be pasted into a prompt (ships in
|
|
225
|
+
the npm package next to this README).
|
|
226
|
+
- `DRAWING_DATA_JSON_SCHEMA` (exported) is the same contract as JSON Schema
|
|
227
|
+
— use it to validate generated payloads or as a structured-output/tool
|
|
228
|
+
schema.
|
|
229
|
+
- `parseDrawingData(json)` is the editor's own lenient parser (invalid
|
|
230
|
+
shapes drop out; never throws); `serializeDrawingData` is its inverse.
|
|
231
|
+
- A ready-made Claude Code skill lives in the monorepo at
|
|
232
|
+
`.claude/skills/text-editor-documents/` — copy it into consuming repos so
|
|
233
|
+
agents there know the dialect.
|
|
234
|
+
|
|
235
|
+
## Exports
|
|
236
|
+
|
|
237
|
+
| Export | Purpose |
|
|
238
|
+
|--------|---------|
|
|
239
|
+
| `MarkdownEditor`, `MarkdownEditorProps` | The component; compound parts as statics (`.Root`, `.Content`, `.Toolbar`, …) |
|
|
240
|
+
| `useMarkdownEditor`, `MarkdownEditorApi` | Headless editor hook |
|
|
241
|
+
| `Toolbar`, `ToolbarButton`, `ToolbarDivider`, `FormatButtons`, `InsertButtons`, `HistoryButtons` | Toolbar primitives |
|
|
242
|
+
| `@zuilib/text-editor/styles.css` | Editor chrome + theme styles (required) |
|
|
243
|
+
| `TABLE` | GFM table markdown transformer |
|
|
244
|
+
| `DRAWING`, `DrawingNode`, `$createDrawingNode`, `$isDrawingNode` | Drawing node + markdown transformer |
|
|
245
|
+
| `FRONTMATTER`, `FrontmatterNode`, `$createFrontmatterNode`, `$isFrontmatterNode` | Frontmatter node + transformer |
|
|
246
|
+
| `DrawingData`, `DrawingShape`, `DrawingShapeType` | Drawing payload types |
|
|
247
|
+
| `parseDrawingData`, `serializeDrawingData` | Drawing payload (de)serialization |
|
|
248
|
+
| `DRAWING_DATA_JSON_SCHEMA` | JSON Schema of the drawing payload |
|
|
249
|
+
|
|
250
|
+
## Form integration
|
|
181
251
|
|
|
182
252
|
```tsx
|
|
183
253
|
<FormField control={form.control} name="body">
|
|
184
254
|
{({ field }) => (
|
|
185
|
-
<MarkdownEditor
|
|
186
|
-
value={field.value}
|
|
187
|
-
onChange={field.onChange}
|
|
188
|
-
mode="edit-md"
|
|
189
|
-
/>
|
|
255
|
+
<MarkdownEditor value={field.value} onChange={field.onChange} />
|
|
190
256
|
)}
|
|
191
257
|
</FormField>
|
|
192
258
|
```
|
|
193
259
|
|
|
194
|
-
|
|
260
|
+
Load both CSS entry points (`@zuilib/core/styles.css`,
|
|
261
|
+
`@zuilib/text-editor/styles.css`) in the app layout. Dark mode follows the
|
|
262
|
+
ZUI convention: a `dark` class on `<html>`.
|
|
195
263
|
|
|
196
264
|
## Architecture notes (for extenders)
|
|
197
265
|
|
|
198
|
-
- Source: `src/
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
266
|
+
- Source: `src/EditorRoot.tsx` (composer + plugins), `src/EditorContent.tsx`,
|
|
267
|
+
`src/MarkdownEditor.tsx` (default composition), `src/useMarkdownEditor.ts`,
|
|
268
|
+
`src/components/Toolbar.tsx`; plugins in `src/plugins/`; drawing
|
|
269
|
+
canvas in `src/components/`; custom nodes in `src/nodes/`;
|
|
270
|
+
transformers in `src/transformers/`
|
|
271
|
+
- Markdown import/export via `@lexical/markdown` transformers; order
|
|
272
|
+
matters: `FRONTMATTER` and `DRAWING` claim their blocks before `CODE`,
|
|
273
|
+
`CHECK_LIST` before `UNORDERED_LIST`
|
|
274
|
+
- Drawing geometry (bindings resolver, elbow/waypoint paths, wrapping) is
|
|
275
|
+
pure and lives in `src/components/drawingGeometry.ts`
|
|
276
|
+
- Tests: `pnpm test` runs a headless-Lexical markdown round-trip suite
|
|
277
|
+
(`tests/roundtrip.mjs`)
|
|
202
278
|
|
|
203
279
|
## Related packages
|
|
204
280
|
|
|
205
281
|
- `@zuilib/core` — design tokens and base styles
|
|
206
282
|
- `@zuilib/form` — react-hook-form wiring
|
|
207
283
|
|
|
208
|
-
## Build (maintainers)
|
|
284
|
+
## Build & release (maintainers)
|
|
209
285
|
|
|
210
286
|
```bash
|
|
211
|
-
pnpm --filter @zuilib/text-editor build
|
|
287
|
+
pnpm --filter @zuilib/text-editor build # tsup → dist/
|
|
288
|
+
pnpm --filter @zuilib/text-editor test # round-trip suite
|
|
289
|
+
pnpm --filter @zuilib/text-editor publish --access public
|
|
212
290
|
```
|