@zuilib/text-editor 0.4.0 → 0.6.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 +100 -0
- package/DRAWING_FORMAT.md +246 -66
- package/README.md +181 -57
- package/dist/index.d.ts +593 -60
- package/dist/index.js +3289 -1363
- package/dist/styles.css +383 -93
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,11 +13,15 @@ document outline, and collapsible sections.
|
|
|
13
13
|
blockquotes, links, fenced code (with syntax highlighting), tables
|
|
14
14
|
- **Tables**: GFM pipe tables, edited in place (tab between cells, ranges,
|
|
15
15
|
inline formatting), styled like Claude artifacts
|
|
16
|
-
- **
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
- **Text measure and block width**: an opt-in readable text column
|
|
17
|
+
(`measure="48rem"`); each table and diagram picks *full* / *text* /
|
|
18
|
+
*content* width, Slab-style; row density per table
|
|
19
|
+
- **Diagrams**: a drawing canvas embedded in the document. Eight box
|
|
20
|
+
shapes (rectangle, ellipse, diamond, note, database, cloud, queue, actor)
|
|
21
|
+
with attached text slots, bound arrows that follow their boxes and attach
|
|
22
|
+
to a chosen side, auto-routed elbows that avoid other boxes, multi-select,
|
|
23
|
+
color palettes, dark mode, Mermaid export. LLMs author diagrams as a
|
|
24
|
+
coordinate-free ` ```diagram ` skeleton that the editor lays out
|
|
21
25
|
- **Outline**: optional table-of-contents sidebar with click-to-scroll and
|
|
22
26
|
current-section highlight
|
|
23
27
|
- **Section folding**: collapse everything under a heading, view-layer only
|
|
@@ -70,6 +74,8 @@ emits. The emitted string is always plain markdown.
|
|
|
70
74
|
| `toolbar` | `boolean \| (items) => ReactNode` | `true` | Formatting/insert toolbar (`edit-md`); function form customises it |
|
|
71
75
|
| `outline` | `boolean` | `false` | Table-of-contents sidebar |
|
|
72
76
|
| `foldable` | `boolean` | `true` | Collapse sections under headings |
|
|
77
|
+
| `measure` | `string` | — | Max width of the text column, a CSS length (`'48rem'` recommended). Sugar for `--zui-text-editor-measure` |
|
|
78
|
+
| `defaultBlockWidth` | `{ table?, drawing? }` | — | `BlockWidth` written when the toolbar inserts a table / drawing |
|
|
73
79
|
|
|
74
80
|
### Modes
|
|
75
81
|
|
|
@@ -98,16 +104,12 @@ Editing is in place: Tab/arrows between cells, cell range selection, inline
|
|
|
98
104
|
formatting inside cells. Cell content is single-line in markdown; newlines
|
|
99
105
|
are escaped as `\n`.
|
|
100
106
|
|
|
101
|
-
### Table
|
|
107
|
+
### Table density
|
|
102
108
|
|
|
103
109
|
Place the caret in a table and a small toolbar floats at its top-right
|
|
104
|
-
corner
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*content* (the table shrinks to fit its columns, left-aligned with the
|
|
108
|
-
text, never wider than the editor).
|
|
109
|
-
- **Density** — *compact*, *comfortable* (default), or *spacious* cell
|
|
110
|
-
padding and font size.
|
|
110
|
+
corner with the table's width (see [Block width and text
|
|
111
|
+
measure](#block-width-and-text-measure)) and its **density** — *compact*,
|
|
112
|
+
*comfortable* (default), or *spacious* cell padding and font size.
|
|
111
113
|
|
|
112
114
|
Non-default settings are persisted as one HTML comment on the line directly
|
|
113
115
|
above the table — other markdown renderers hide it:
|
|
@@ -119,12 +121,70 @@ above the table — other markdown renderers hide it:
|
|
|
119
121
|
| Region | eu-west-1 |
|
|
120
122
|
```
|
|
121
123
|
|
|
122
|
-
Programmatic access: `useMarkdownEditor().
|
|
123
|
-
|
|
124
|
-
`$
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
Programmatic access: `useMarkdownEditor().tableDensity` / `setTableDensity`
|
|
125
|
+
(for custom toolbars), or `$getTableSettings(node)` /
|
|
126
|
+
`$setTableSettings(node, { width, density })` inside `editor.update`.
|
|
127
|
+
`parseTableSettingsMarker` / `formatTableSettingsMarker` convert between
|
|
128
|
+
the comment line and a `TableSettings` object.
|
|
129
|
+
|
|
130
|
+
## Block width and text measure
|
|
131
|
+
|
|
132
|
+
By default every block spans the content pane. Set a **measure** to get a
|
|
133
|
+
readable text column instead: paragraphs, headings, lists, quotes, code,
|
|
134
|
+
frontmatter are capped at that width and centred in the pane, while each
|
|
135
|
+
table and drawing chooses its own width.
|
|
136
|
+
|
|
137
|
+
```tsx
|
|
138
|
+
<MarkdownEditor value={value} onChange={setValue} measure="48rem" />
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The prop is sugar for the CSS custom property `--zui-text-editor-measure`,
|
|
142
|
+
which is the actual contract — set it in a stylesheet on `.zui-text-editor`
|
|
143
|
+
(or any ancestor) and never touch the prop:
|
|
144
|
+
|
|
145
|
+
```css
|
|
146
|
+
.zui-text-editor { --zui-text-editor-measure: 48rem; }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Adopting:** set `measure` (or the variable) and delete any external
|
|
150
|
+
`max-width` / `margin-inline` overrides on `.zui-text-editor-content > *`;
|
|
151
|
+
the library now owns that layout.
|
|
152
|
+
|
|
153
|
+
Every table and drawing has a `BlockWidth`, picked from the floating table
|
|
154
|
+
toolbar or the right end of the drawing toolbar:
|
|
155
|
+
|
|
156
|
+
| `BlockWidth` | Layout | Markdown |
|
|
157
|
+
|--------------|--------|----------|
|
|
158
|
+
| `full` | The content pane, edge to edge (never under the outline sidebar). The default | Omitted |
|
|
159
|
+
| `text` | The text column: edges align with the paragraphs. Identical to `full` until a measure is set | `<!-- width: text -->` / `"width":"text"` |
|
|
160
|
+
| `content` | Shrinks to its columns / shapes, left-aligned with the text, never wider than the column | `<!-- width: content -->` / `"width":"content"` |
|
|
161
|
+
|
|
162
|
+
Absence of a marker always means `full`, in every app. `defaultBlockWidth`
|
|
163
|
+
only changes what the toolbar / `insertTable` / `insertDrawing` write into
|
|
164
|
+
*new* blocks — and that value is written explicitly, even when it is
|
|
165
|
+
`full`, so the document reads the same elsewhere:
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
<MarkdownEditor measure="48rem" defaultBlockWidth={{ table: 'text', drawing: 'text' }} … />
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Headless: `useMarkdownEditor().blockWidth` / `setBlockWidth` act on the
|
|
172
|
+
block containing the selection — the focused drawing, or else the table
|
|
173
|
+
the caret is in. (`tableWidth` / `setTableWidth` still work and are
|
|
174
|
+
deprecated in favour of these.)
|
|
175
|
+
|
|
176
|
+
Styling: the layout is driven by low-specificity `:where()` rules and two
|
|
177
|
+
derived custom properties on `.zui-text-editor-main` —
|
|
178
|
+
`--zui-text-editor-gutter` (the pane padding, `2rem` without a measure)
|
|
179
|
+
and `--zui-text-editor-bleed` (how far a full-width block extends past the
|
|
180
|
+
column, `0` without a measure). Each top-level block reads
|
|
181
|
+
`--zui-text-editor-block-bleed`, so a single class rule overrides any block
|
|
182
|
+
kind: `.zui-code { --zui-text-editor-block-bleed: var(--zui-text-editor-bleed) }`
|
|
183
|
+
makes code blocks full width; `.zui-table { --zui-text-editor-block-bleed: 0px }`
|
|
184
|
+
keeps every table inside the column. Block classes: `.zui-paragraph`,
|
|
185
|
+
`.zui-heading` (+ `.zui-heading-1`…`6`), `.zui-list` (+ `-ordered` /
|
|
186
|
+
`-unordered`, `.zui-checklist`), `.zui-quote`, `.zui-code`,
|
|
187
|
+
`.zui-frontmatter`, `.zui-table`, `.zui-drawing`.
|
|
128
188
|
|
|
129
189
|
## Toolbar & custom toolbars
|
|
130
190
|
|
|
@@ -209,36 +269,89 @@ function BoldButton() {
|
|
|
209
269
|
```
|
|
210
270
|
|
|
211
271
|
It returns `editor` (the Lexical instance), `activeFormats`,
|
|
212
|
-
`toggleFormat`, `insertTable`, `insertDrawing`, `
|
|
213
|
-
`
|
|
214
|
-
|
|
215
|
-
`canRedo`, `undo`, `redo`.
|
|
272
|
+
`toggleFormat`, `insertTable`, `insertDrawing`, `blockWidth` /
|
|
273
|
+
`setBlockWidth` (width of the table or drawing containing the selection,
|
|
274
|
+
`null` outside both), `tableDensity` / `setTableDensity` (`null` outside
|
|
275
|
+
tables), `canUndo`, `canRedo`, `undo`, `redo`. `tableWidth` /
|
|
276
|
+
`setTableWidth` remain as deprecated aliases limited to tables.
|
|
216
277
|
|
|
217
278
|
## Diagrams (drawing canvas)
|
|
218
279
|
|
|
219
280
|
The toolbar's insert-drawing button embeds a canvas; the drawing persists
|
|
220
|
-
in the markdown as a ` ```drawing ` fenced JSON block
|
|
221
|
-
**[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)**.
|
|
281
|
+
in the markdown as a ` ```drawing ` fenced JSON block (format version 2),
|
|
282
|
+
fully specified in **[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)**.
|
|
222
283
|
|
|
223
|
-
- **Shapes**: rectangle, ellipse,
|
|
224
|
-
|
|
284
|
+
- **Shapes**: rectangle, ellipse, diamond, note (sticky note), database
|
|
285
|
+
(cylinder), cloud, queue, actor (stick figure), arrow, line, text. The
|
|
286
|
+
toolbar shows the common tools and a "more shapes" popover for the rest.
|
|
287
|
+
Draw by picking a tool and dragging.
|
|
225
288
|
- **Cards**: every box carries three text slots that move, resize, and wrap
|
|
226
|
-
with it
|
|
227
|
-
**footer** at the bottom
|
|
228
|
-
top/middle/bottom strip, or
|
|
289
|
+
with it: a bold **label** on top, **content** in the center, a dim
|
|
290
|
+
**footer** at the bottom (actor has only the name under the figure).
|
|
291
|
+
Click a selected box (or double-click its top/middle/bottom strip, or
|
|
292
|
+
press Enter) to edit a slot.
|
|
229
293
|
- **Bound connectors**: an arrow drawn from one card to another attaches to
|
|
230
|
-
both
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
294
|
+
both; moving a card moves its arrows, endpoints anchored to the outline.
|
|
295
|
+
An endpoint either auto-aims at the other end or sticks to a fixed point
|
|
296
|
+
on the box (a side, or a ratio inside the box). Drag an endpoint off/onto
|
|
297
|
+
a card to detach/re-attach.
|
|
298
|
+
- **Routing**: straight (diagonal) by default; toggle **elbow** for an
|
|
299
|
+
auto-routed right-angled path that leaves the box perpendicular to its
|
|
300
|
+
attach side and avoids the other boxes, or drag the dashed "+" handles on
|
|
301
|
+
a selected connector to add any number of **waypoints** (they snap to
|
|
302
|
+
neighbors' axes for clean 90° bends). One-way / two-way arrowhead toggle;
|
|
303
|
+
midpoint **labels**.
|
|
304
|
+
- **Multi-select**: shift-click, or drag a marquee on empty canvas, then
|
|
305
|
+
move, delete, or recolor the selection together. A contextual property
|
|
306
|
+
bar shows the options for whatever is selected.
|
|
307
|
+
- **Copy as Mermaid**: a canvas button copies the drawing as a Mermaid
|
|
308
|
+
`flowchart` (also available as `drawingToMermaid(data)`).
|
|
236
309
|
- **Canvas**: resizable height, dot grid, white surface that inverts
|
|
237
|
-
Excalidraw-style in dark mode (`.dark` ancestor class).
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
310
|
+
Excalidraw-style in dark mode (`.dark` ancestor class). An optional
|
|
311
|
+
`canvasWidth` scales the drawing to fit narrower layouts.
|
|
312
|
+
- **Width**: the buttons at the right end of the canvas toolbar switch
|
|
313
|
+
between **full** (spans the pane), **text** (aligns with the text column,
|
|
314
|
+
see [Block width and text measure](#block-width-and-text-measure)) and
|
|
315
|
+
**content** (the canvas fits the rightmost shape and grows as shapes
|
|
316
|
+
move). Stored as `"width":"text"` / `"width":"content"` in the payload;
|
|
317
|
+
omitted when full.
|
|
318
|
+
|
|
319
|
+
A stored drawing looks like this:
|
|
320
|
+
|
|
321
|
+
````md
|
|
322
|
+
```drawing
|
|
323
|
+
{"version":2,"canvasHeight":260,"shapes":[
|
|
324
|
+
{"id":"web","type":"rect","x":40,"y":70,"w":170,"h":100,"stroke":"#1971c2","fill":"#a5d8ff","strokeWidth":2,"label":"CLIENT","text":"Web App"},
|
|
325
|
+
{"id":"api","type":"rect","x":330,"y":70,"w":170,"h":100,"stroke":"#2f9e44","fill":"#b2f2bb","strokeWidth":2,"label":"SERVICE","text":"API"},
|
|
326
|
+
{"id":"e1","type":"arrow","x":216,"y":120,"w":108,"h":0,"stroke":"#1e1e1e","fill":"transparent","strokeWidth":2,"startBinding":{"id":"web"},"endBinding":{"id":"api"},"text":"REST"}
|
|
327
|
+
]}
|
|
328
|
+
```
|
|
329
|
+
````
|
|
330
|
+
|
|
331
|
+
### The ` ```diagram ` skeleton
|
|
332
|
+
|
|
333
|
+
Generators (LLMs, scripts) do not need coordinates. A ` ```diagram ` block
|
|
334
|
+
holds a skeleton: boxes, connectors, colors. On import the editor lays it
|
|
335
|
+
out along `direction` (`right` by default, or `down`), sizes boxes to their
|
|
336
|
+
text, and stores the result as a normal ` ```drawing ` block (one-way
|
|
337
|
+
expansion).
|
|
338
|
+
|
|
339
|
+
````md
|
|
340
|
+
```diagram
|
|
341
|
+
{"boxes":[
|
|
342
|
+
{"id":"web","label":"CLIENT","text":"Web App","color":"blue"},
|
|
343
|
+
{"id":"api","label":"SERVICE","text":"API","color":"green"},
|
|
344
|
+
{"id":"db","type":"cylinder","text":"Postgres"}
|
|
345
|
+
],"connectors":[
|
|
346
|
+
{"from":"web","to":"api","text":"REST"},
|
|
347
|
+
{"from":"api","to":"db","routing":"elbow"}
|
|
348
|
+
]}
|
|
349
|
+
```
|
|
350
|
+
````
|
|
351
|
+
|
|
352
|
+
The full skeleton spec (attach sides, explicit positions, free texts) is
|
|
353
|
+
in [DRAWING_FORMAT.md](./DRAWING_FORMAT.md). Programmatic access:
|
|
354
|
+
`parseDrawingSkeleton`, `expandSkeleton`, `DRAWING_SKELETON_JSON_SCHEMA`.
|
|
242
355
|
|
|
243
356
|
## Outline & section folding
|
|
244
357
|
|
|
@@ -256,14 +369,19 @@ remount; a folded section auto-expands if the cursor enters it.
|
|
|
256
369
|
Documents are plain markdown, so LLMs can generate them — including
|
|
257
370
|
diagrams:
|
|
258
371
|
|
|
259
|
-
-
|
|
260
|
-
` ```drawing `
|
|
261
|
-
|
|
262
|
-
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
- `
|
|
266
|
-
|
|
372
|
+
- Emit ` ```diagram ` skeleton blocks: no coordinates, the editor lays them
|
|
373
|
+
out. Use ` ```drawing ` (format v2) only to edit an existing concrete
|
|
374
|
+
payload.
|
|
375
|
+
- **[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)** is the authoritative spec of
|
|
376
|
+
both payloads, written to be pasted into a prompt (ships in the npm
|
|
377
|
+
package next to this README).
|
|
378
|
+
- `DRAWING_SKELETON_JSON_SCHEMA` and `DRAWING_DATA_JSON_SCHEMA` (exported)
|
|
379
|
+
are the same contracts as JSON Schema: use them to validate generated
|
|
380
|
+
payloads or as structured-output/tool schemas.
|
|
381
|
+
- `parseDrawingSkeleton(json)` / `expandSkeleton(skeleton)` turn a skeleton
|
|
382
|
+
into `DrawingData`. `parseDrawingData(json)` is the editor's own lenient
|
|
383
|
+
parser (invalid shapes drop out; never throws); `serializeDrawingData` is
|
|
384
|
+
its inverse. `drawingToMermaid(data)` exports a Mermaid `flowchart`.
|
|
267
385
|
- A ready-made Claude Code skill lives in the monorepo at
|
|
268
386
|
`.claude/skills/text-editor-documents/` — copy it into consuming repos so
|
|
269
387
|
agents there know the dialect.
|
|
@@ -277,14 +395,18 @@ diagrams:
|
|
|
277
395
|
| `Toolbar`, `ToolbarButton`, `ToolbarDivider`, `FormatButtons`, `InsertButtons`, `HistoryButtons` | Toolbar primitives |
|
|
278
396
|
| `@zuilib/text-editor/styles.css` | Editor chrome + theme styles (required) |
|
|
279
397
|
| `TABLE` | GFM table markdown transformer |
|
|
280
|
-
| `BlockWidth`, `
|
|
281
|
-
|
|
|
398
|
+
| `BlockWidth`, `BLOCK_WIDTHS`, `isBlockWidth`, `BlockWidthDefaults` | Block width (`full` / `text` / `content`) type, values, guard, insertion defaults |
|
|
399
|
+
| `TableDensity`, `TableSettings`, `TableSettingsOptions`, `DEFAULT_TABLE_SETTINGS` | Table setting types |
|
|
400
|
+
| `$getTableSettings`, `$setTableSettings`, `$isTableWidthExplicit`, `$getTableWidth`, `$setTableWidth`, `$getTableDensity`, `$setTableDensity`, `$getSelectedTable` | Table setting helpers (inside `editor.update`/`read`) |
|
|
282
401
|
| `parseTableSettingsMarker`, `formatTableSettingsMarker` | Marker comment ⇄ `TableSettings` |
|
|
283
|
-
| `DRAWING`, `DrawingNode`, `$createDrawingNode`, `$isDrawingNode` | Drawing node + markdown
|
|
402
|
+
| `DRAWING`, `DIAGRAM`, `DrawingNode`, `$createDrawingNode`, `$isDrawingNode` | Drawing node + markdown transformers (` ```drawing ` and ` ```diagram `) |
|
|
284
403
|
| `FRONTMATTER`, `FrontmatterNode`, `$createFrontmatterNode`, `$isFrontmatterNode` | Frontmatter node + transformer |
|
|
285
404
|
| `DrawingData`, `DrawingShape`, `DrawingShapeType` | Drawing payload types |
|
|
286
405
|
| `parseDrawingData`, `serializeDrawingData` | Drawing payload (de)serialization |
|
|
287
406
|
| `DRAWING_DATA_JSON_SCHEMA` | JSON Schema of the drawing payload |
|
|
407
|
+
| `DrawingSkeleton`, `SkeletonBox`, `SkeletonConnector` | Skeleton types |
|
|
408
|
+
| `parseDrawingSkeleton`, `expandSkeleton`, `DRAWING_SKELETON_JSON_SCHEMA` | Skeleton parsing, expansion to `DrawingData`, JSON Schema |
|
|
409
|
+
| `drawingToMermaid` | Mermaid `flowchart` export |
|
|
288
410
|
|
|
289
411
|
## Form integration
|
|
290
412
|
|
|
@@ -305,13 +427,15 @@ ZUI convention: a `dark` class on `<html>`.
|
|
|
305
427
|
- Source: `src/EditorRoot.tsx` (composer + plugins), `src/EditorContent.tsx`,
|
|
306
428
|
`src/MarkdownEditor.tsx` (default composition), `src/useMarkdownEditor.ts`,
|
|
307
429
|
`src/components/Toolbar.tsx`; plugins in `src/plugins/`; drawing
|
|
308
|
-
canvas in `src/
|
|
430
|
+
canvas in `src/drawing/`; custom nodes in `src/nodes/`;
|
|
309
431
|
transformers in `src/transformers/`
|
|
310
432
|
- Markdown import/export via `@lexical/markdown` transformers; order
|
|
311
|
-
matters: `FRONTMATTER` and `
|
|
312
|
-
`CHECK_LIST` before `UNORDERED_LIST`
|
|
313
|
-
-
|
|
314
|
-
|
|
433
|
+
matters: `FRONTMATTER`, `DRAWING` and `DIAGRAM` claim their blocks before
|
|
434
|
+
`CODE`, `CHECK_LIST` before `UNORDERED_LIST`
|
|
435
|
+
- `src/drawing/` keeps the format (`types.ts`, `schema.ts`), shape
|
|
436
|
+
definitions (`shapes/`), pure geometry (`geometry.ts`), the Mermaid
|
|
437
|
+
exporter (`mermaid.ts`) and the React canvas (`canvas/`) apart, so the
|
|
438
|
+
router, bindings and skeleton expansion run without a DOM
|
|
315
439
|
- Tests: `pnpm test` runs a headless-Lexical markdown round-trip suite
|
|
316
440
|
(`tests/roundtrip.mjs`)
|
|
317
441
|
|