@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/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
- - **Table settings**: full/content width and row density per table;
17
- diagrams switch between full and content width too Slab-style
18
- - **Diagrams**: a drawing canvas embedded in the document — cards with
19
- attached text slots, bound arrows that follow their cards, elbow and
20
- multi-waypoint routing, labels, color palettes, dark mode
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 settings: width and density
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
- - **Width** — *full* (default: columns share the editor width) or
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().tableWidth` / `setTableWidth` and
123
- `tableDensity` / `setTableDensity` (for custom toolbars), or
124
- `$getTableSettings(node)` / `$setTableSettings(node, { width, density })`
125
- inside `editor.update`. `parseTableSettingsMarker` /
126
- `formatTableSettingsMarker` convert between the comment line and a
127
- `TableSettings` object.
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`, `tableWidth` /
213
- `setTableWidth` and `tableDensity` / `setTableDensity` (settings of the
214
- table containing the selection, `null` outside tables), `canUndo`,
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, fully specified in
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, triangle, pentagon, arrow, line, text —
224
- with stroke/fill palettes. Draw by picking a tool and dragging.
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 a bold **label** on top, **content** in the center, a dim
227
- **footer** at the bottom. Click a selected box (or double-click its
228
- top/middle/bottom strip, or press Enter) to edit a slot.
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 moving a card moves its arrows, endpoints anchored to the border.
231
- Drag an endpoint off/onto a card to detach/re-attach.
232
- - **Routing**: straight (diagonal) by default; toggle **elbow** for right
233
- angles, or drag the dashed "+" handles on a selected connector to add any
234
- number of **waypoints** (they snap to neighbors' axes for clean 90°
235
- bends). One-way / two-way arrowhead toggle; midpoint **labels**.
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
- - **Width**: the toggle at the right end of the canvas toolbar switches
239
- between **full width** (spans the editor) and **content width** (the
240
- canvas fits the rightmost shape and grows as shapes move). Stored as
241
- `"width":"content"` in the payload; omitted when full.
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
- - **[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)** is the authoritative
260
- ` ```drawing ` payload spec, written to be pasted into a prompt (ships in
261
- the npm package next to this README).
262
- - `DRAWING_DATA_JSON_SCHEMA` (exported) is the same contract as JSON Schema
263
- use it to validate generated payloads or as a structured-output/tool
264
- schema.
265
- - `parseDrawingData(json)` is the editor's own lenient parser (invalid
266
- shapes drop out; never throws); `serializeDrawingData` is its inverse.
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`, `TableDensity`, `TableSettings`, `DEFAULT_TABLE_SETTINGS` | Table setting types |
281
- | `$getTableSettings`, `$setTableSettings`, `$getTableWidth`, `$setTableWidth`, `$getTableDensity`, `$setTableDensity`, `$getSelectedTable` | Table setting helpers (inside `editor.update`/`read`) |
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 transformer |
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/components/`; custom nodes in `src/nodes/`;
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 `DRAWING` claim their blocks before `CODE`,
312
- `CHECK_LIST` before `UNORDERED_LIST`
313
- - Drawing geometry (bindings resolver, elbow/waypoint paths, wrapping) is
314
- pure and lives in `src/components/drawingGeometry.ts`
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