@zuilib/text-editor 0.5.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 CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog — @zuilib/text-editor
2
2
 
3
+ ## 0.6.0
4
+
5
+ **Text measure and `text` block width.** Additive: markdown, JSON, class
6
+ names and props keep their meaning; with no configuration the layout is
7
+ unchanged.
8
+
9
+ - **Measure** (opt in): `measure="48rem"` on `MarkdownEditor` /
10
+ `MarkdownEditor.Root`, or `--zui-text-editor-measure: 48rem` in a
11
+ stylesheet — the CSS custom property is the contract, the prop is sugar.
12
+ Text blocks (paragraphs, headings, lists, quotes, code, frontmatter) are
13
+ capped at the measure and centred in the content pane; the placeholder
14
+ and fold chevrons follow the column. The pane itself is never capped and
15
+ full-width blocks stop short of the outline sidebar
16
+ - **`BlockWidth` gains `'text'`**: `'full' | 'text' | 'content'`. `text`
17
+ aligns a table or drawing with the text column; without a measure it is
18
+ visually identical to `full` (resolved in CSS through the variable's
19
+ fallback). `full` (default, omitted) and `content` (shrink to fit) are
20
+ unchanged
21
+ - **Persistence**: tables `<!-- width: text -->` (combinable with
22
+ `density`); drawings and ` ```diagram ` skeletons `"width":"text"`.
23
+ `DRAWING_DATA_JSON_SCHEMA`, `DRAWING_SKELETON_JSON_SCHEMA` and
24
+ `DRAWING_FORMAT.md` updated. Unknown width values are ignored (treated as
25
+ `full`) instead of being rejected, so documents from newer versions still
26
+ open — `parseTableSettingsMarker` now returns `{}` (rather than `null`)
27
+ for a marker whose only known key carries an unknown value, and the
28
+ transformer consumes that line
29
+ - **Insertion defaults**: `defaultBlockWidth={{ table?, drawing? }}` on
30
+ Root applies when the toolbar / `useMarkdownEditor()` inserts a new table
31
+ or drawing. The chosen width is written to the markdown explicitly, even
32
+ `full`, so the document reads the same in an app with other defaults; a
33
+ width named in the source (`<!-- width: full -->`, `"width":"full"`) is
34
+ preserved on round trip
35
+ - **UI / headless**: the floating table toolbar and the drawing toolbar
36
+ offer all three widths with distinct icons (`BLOCK_WIDTH_OPTIONS`).
37
+ `useMarkdownEditor()` gains `blockWidth` / `setBlockWidth` for the block
38
+ containing the selection — the focused drawing or the table the caret is
39
+ in; `insertTable` / `insertDrawing` accept a `width` option. `tableWidth`
40
+ / `setTableWidth` keep working and are deprecated
41
+ - **CSS**: the new layout lives in zero-specificity `:where()` rules. The
42
+ content pane exposes `--zui-text-editor-gutter` and
43
+ `--zui-text-editor-bleed`; every top-level block reads
44
+ `--zui-text-editor-block-bleed`, so one class rule overrides any block
45
+ kind. New block classes (added next to the existing utility classes):
46
+ `.zui-paragraph`, `.zui-heading` / `.zui-heading-1`…`6`, `.zui-list` /
47
+ `.zui-list-ordered` / `.zui-list-unordered`, `.zui-quote`, `.zui-code`;
48
+ `.zui-drawing-canvas` gains `.is-text-width`. Nothing is renamed
49
+ - New exports: `BLOCK_WIDTHS`, `isBlockWidth`, `BlockWidthDefaults`,
50
+ `TableSettingsOptions`, `$isTableWidthExplicit`; `$setTableSettings` and
51
+ `formatTableSettingsMarker` take an optional `{ explicitWidth }`
52
+ - **Adopting**: set `measure` (or the CSS variable) and delete any external
53
+ `max-width` / `margin-inline` overrides on `.zui-text-editor-content > *`
54
+
3
55
  ## 0.5.0
4
56
 
5
57
  **Diagram builder rewrite.** The drawing canvas, its payload format and the
package/DRAWING_FORMAT.md CHANGED
@@ -41,9 +41,13 @@ skeleton, which is a separate block type.
41
41
  narrower layouts (SVG viewBox), so coordinates can assume that width.
42
42
  When omitted the canvas is fluid and as wide as the editor; keep shapes
43
43
  within roughly x in [0, 700] to be safe on typical layouts.
44
- - `"width":"content"` sizes the block to fit the rightmost shape (plus a
45
- margin), left-aligned with the text. Use it for small diagrams that
46
- should not stretch across the page. Default `"full"`, omit it.
44
+ - `width` is the block's horizontal sizing in the page, independent of the
45
+ coordinates: `"full"` (default, omit it) spans the editor pane;
46
+ `"text"` aligns the block with the text column (the same as `"full"`
47
+ unless the app sets a text measure); `"content"` sizes the block to fit
48
+ the rightmost shape (plus a margin), left-aligned with the text. Use
49
+ `"content"` for small diagrams that should not stretch across the page.
50
+ Unknown values are ignored (treated as `"full"`).
47
51
 
48
52
  ## Top-level object
49
53
 
@@ -52,7 +56,7 @@ skeleton, which is a separate block type.
52
56
  | `version` | `2` | Literal `2` |
53
57
  | `canvasHeight` | `number` | Canvas height in px (min 80) |
54
58
  | `canvasWidth` | `number` | Optional logical width in px (min 120); scales to fit when set |
55
- | `width` | `"full"` \| `"content"` | Optional; default `"full"`. `"content"` fits the shapes' extent |
59
+ | `width` | `"full"` \| `"text"` \| `"content"` | Optional; default `"full"`. `"text"` aligns with the text column, `"content"` fits the shapes' extent |
56
60
  | `shapes` | `Shape[]` | Render order: later shapes draw on top |
57
61
 
58
62
  ## Shape object
@@ -197,7 +201,7 @@ one-way: once opened, the document stores the concrete ```drawing block.
197
201
  ```
198
202
  {
199
203
  "direction"?: "right" | "down", // auto-layout flow, default right
200
- "canvasWidth"?: n, "canvasHeight"?: n, "width"?: "content",
204
+ "canvasWidth"?: n, "canvasHeight"?: n, "width"?: "full" | "text" | "content",
201
205
  "boxes": [ { "id", "type"?: <box type, default rect>, "label"?, "text"?, "footer"?,
202
206
  "x"?, "y"?, // omitted: auto layout by connector rank
203
207
  "w"?, "h"?, // omitted: sized to fit the text
@@ -224,6 +228,8 @@ Rules:
224
228
  them. Boxes without `w`/`h` are sized to fit their text. `canvasHeight`
225
229
  is computed when omitted.
226
230
  - `texts` are free annotations and need explicit coordinates.
231
+ - `width` is copied to the expanded drawing (see [Coordinate
232
+ system](#coordinate-system)); omit it for `"full"`.
227
233
 
228
234
  A three-box flow:
229
235
 
package/README.md CHANGED
@@ -13,8 +13,9 @@ 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
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
18
19
  - **Diagrams**: a drawing canvas embedded in the document. Eight box
19
20
  shapes (rectangle, ellipse, diamond, note, database, cloud, queue, actor)
20
21
  with attached text slots, bound arrows that follow their boxes and attach
@@ -73,6 +74,8 @@ emits. The emitted string is always plain markdown.
73
74
  | `toolbar` | `boolean \| (items) => ReactNode` | `true` | Formatting/insert toolbar (`edit-md`); function form customises it |
74
75
  | `outline` | `boolean` | `false` | Table-of-contents sidebar |
75
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 |
76
79
 
77
80
  ### Modes
78
81
 
@@ -101,16 +104,12 @@ Editing is in place: Tab/arrows between cells, cell range selection, inline
101
104
  formatting inside cells. Cell content is single-line in markdown; newlines
102
105
  are escaped as `\n`.
103
106
 
104
- ### Table settings: width and density
107
+ ### Table density
105
108
 
106
109
  Place the caret in a table and a small toolbar floats at its top-right
107
- corner:
108
-
109
- - **Width** — *full* (default: columns share the editor width) or
110
- *content* (the table shrinks to fit its columns, left-aligned with the
111
- text, never wider than the editor).
112
- - **Density** — *compact*, *comfortable* (default), or *spacious* cell
113
- 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.
114
113
 
115
114
  Non-default settings are persisted as one HTML comment on the line directly
116
115
  above the table — other markdown renderers hide it:
@@ -122,12 +121,70 @@ above the table — other markdown renderers hide it:
122
121
  | Region | eu-west-1 |
123
122
  ```
124
123
 
125
- Programmatic access: `useMarkdownEditor().tableWidth` / `setTableWidth` and
126
- `tableDensity` / `setTableDensity` (for custom toolbars), or
127
- `$getTableSettings(node)` / `$setTableSettings(node, { width, density })`
128
- inside `editor.update`. `parseTableSettingsMarker` /
129
- `formatTableSettingsMarker` convert between the comment line and a
130
- `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`.
131
188
 
132
189
  ## Toolbar & custom toolbars
133
190
 
@@ -212,10 +269,11 @@ function BoldButton() {
212
269
  ```
213
270
 
214
271
  It returns `editor` (the Lexical instance), `activeFormats`,
215
- `toggleFormat`, `insertTable`, `insertDrawing`, `tableWidth` /
216
- `setTableWidth` and `tableDensity` / `setTableDensity` (settings of the
217
- table containing the selection, `null` outside tables), `canUndo`,
218
- `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.
219
277
 
220
278
  ## Diagrams (drawing canvas)
221
279
 
@@ -251,10 +309,12 @@ fully specified in **[DRAWING_FORMAT.md](./DRAWING_FORMAT.md)**.
251
309
  - **Canvas**: resizable height, dot grid, white surface that inverts
252
310
  Excalidraw-style in dark mode (`.dark` ancestor class). An optional
253
311
  `canvasWidth` scales the drawing to fit narrower layouts.
254
- - **Width**: the toggle at the right end of the canvas toolbar switches
255
- between **full width** (spans the editor) and **content width** (the
256
- canvas fits the rightmost shape and grows as shapes move). Stored as
257
- `"width":"content"` in the payload; omitted when full.
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.
258
318
 
259
319
  A stored drawing looks like this:
260
320
 
@@ -335,8 +395,9 @@ diagrams:
335
395
  | `Toolbar`, `ToolbarButton`, `ToolbarDivider`, `FormatButtons`, `InsertButtons`, `HistoryButtons` | Toolbar primitives |
336
396
  | `@zuilib/text-editor/styles.css` | Editor chrome + theme styles (required) |
337
397
  | `TABLE` | GFM table markdown transformer |
338
- | `BlockWidth`, `TableDensity`, `TableSettings`, `DEFAULT_TABLE_SETTINGS` | Table setting types |
339
- | `$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`) |
340
401
  | `parseTableSettingsMarker`, `formatTableSettingsMarker` | Marker comment ⇄ `TableSettings` |
341
402
  | `DRAWING`, `DIAGRAM`, `DrawingNode`, `$createDrawingNode`, `$isDrawingNode` | Drawing node + markdown transformers (` ```drawing ` and ` ```diagram `) |
342
403
  | `FRONTMATTER`, `FrontmatterNode`, `$createFrontmatterNode`, `$isFrontmatterNode` | Frontmatter node + transformer |
package/dist/index.d.ts CHANGED
@@ -4,7 +4,33 @@ import { LexicalEditor, TextFormatType, ElementNode, NodeKey, EditorConfig, Seri
4
4
  import { TableNode } from '@lexical/table';
5
5
  import { MultilineElementTransformer, ElementTransformer } from '@lexical/markdown';
6
6
 
7
+ /**
8
+ * Horizontal sizing of a block that would otherwise span the editor:
9
+ *
10
+ * - `full` — the container width (the content pane, minus its gutters).
11
+ * The default; omitted when serialised.
12
+ * - `text` — the text column: the block's edges align with the paragraphs
13
+ * around it. Only differs from `full` when a measure is set
14
+ * (`--zui-text-editor-measure` / the `measure` prop); otherwise the two
15
+ * are visually identical.
16
+ * - `content` — shrinks to fit what is inside (table columns, drawing
17
+ * shapes), left-aligned with the text and never wider than the column.
18
+ */
19
+ type BlockWidth = 'full' | 'text' | 'content';
20
+ declare const BLOCK_WIDTHS: readonly BlockWidth[];
21
+ declare function isBlockWidth(value: unknown): value is BlockWidth;
22
+ /**
23
+ * Width written into the markdown when a new table / drawing is inserted
24
+ * (`MarkdownEditor.Root`'s `defaultBlockWidth`). Only affects insertion:
25
+ * existing markdown without a width marker always means `full`.
26
+ */
27
+ type BlockWidthDefaults = Readonly<{
28
+ table?: BlockWidth;
29
+ drawing?: BlockWidth;
30
+ }>;
31
+
7
32
  type EditorMode = 'edit-raw' | 'edit-md' | 'view';
33
+
8
34
  type EditorRootProps = Readonly<{
9
35
  value?: string;
10
36
  onChange?: (value: string) => void;
@@ -12,6 +38,22 @@ type EditorRootProps = Readonly<{
12
38
  className?: string;
13
39
  mode?: EditorMode;
14
40
  autoFocus?: boolean;
41
+ /**
42
+ * Text measure: the maximum width of the text column, as a CSS length
43
+ * (`'48rem'` recommended). Text blocks are capped at this width and
44
+ * centred; tables and drawings choose per block whether to align with
45
+ * the column (`text`), span the pane (`full`) or hug their content.
46
+ * Sugar for setting `--zui-text-editor-measure` on the root; the
47
+ * variable can equally be set from a stylesheet. Unset: no measure, every
48
+ * block spans the pane.
49
+ */
50
+ measure?: string;
51
+ /**
52
+ * Width written into the markdown when the toolbar or
53
+ * `useMarkdownEditor()` inserts a new table / drawing. Only affects
54
+ * insertion — markdown without a width marker always means `full`.
55
+ */
56
+ defaultBlockWidth?: BlockWidthDefaults;
15
57
  children: ReactNode;
16
58
  }>;
17
59
  /**
@@ -19,7 +61,7 @@ type EditorRootProps = Readonly<{
19
61
  * beneath it (`Content`, `Toolbar`, `Outline`, or your own components using
20
62
  * `useMarkdownEditor`) shares the same editor instance.
21
63
  */
22
- declare function EditorRoot({ value, onChange, readOnly, className, mode, autoFocus, children, }: EditorRootProps): ReactElement;
64
+ declare function EditorRoot({ value, onChange, readOnly, className, mode, autoFocus, measure, defaultBlockWidth, children, }: EditorRootProps): ReactElement;
23
65
 
24
66
  type EditorContentProps = Readonly<{
25
67
  placeholder?: string;
@@ -41,13 +83,6 @@ declare function EditorContent({ placeholder, foldable, children, }: EditorConte
41
83
  */
42
84
  declare function OutlinePlugin(): ReactElement;
43
85
 
44
- /**
45
- * Horizontal sizing of a block that would otherwise span the editor:
46
- * `full` stretches to the content column, `content` shrinks to fit what is
47
- * inside (table columns, drawing shapes) and left-aligns with the text.
48
- */
49
- type BlockWidth = 'full' | 'content';
50
-
51
86
  /** Cell padding / font size preset */
52
87
  type TableDensity = 'compact' | 'comfortable' | 'spacious';
53
88
  type TableSettings = Readonly<{
@@ -55,8 +90,14 @@ type TableSettings = Readonly<{
55
90
  density: TableDensity;
56
91
  }>;
57
92
  declare const DEFAULT_TABLE_SETTINGS: TableSettings;
93
+ type TableSettingsOptions = Readonly<{
94
+ /** Persist the width even when it is `full` */
95
+ explicitWidth?: boolean;
96
+ }>;
58
97
  declare function $getTableSettings(table: TableNode): TableSettings;
59
- declare function $setTableSettings(table: TableNode, settings: Partial<TableSettings>): void;
98
+ declare function $setTableSettings(table: TableNode, settings: Partial<TableSettings>, options?: TableSettingsOptions): void;
99
+ /** Whether the table's width should be persisted even when it is `full` */
100
+ declare function $isTableWidthExplicit(table: TableNode): boolean;
60
101
  declare function $getTableWidth(table: TableNode): BlockWidth;
61
102
  declare function $setTableWidth(table: TableNode, width: BlockWidth): void;
62
103
  declare function $getTableDensity(table: TableNode): TableDensity;
@@ -65,10 +106,17 @@ declare function $setTableDensity(table: TableNode, density: TableDensity): void
65
106
  declare function $getSelectedTable(): TableNode | null;
66
107
  /** @deprecated Use `formatTableSettingsMarker({ ...DEFAULT_TABLE_SETTINGS, width: 'content' })` */
67
108
  declare const TABLE_WIDTH_MARKER = "<!-- width: content -->";
68
- /** Settings encoded in a marker line, or null if the text is not a marker */
109
+ /**
110
+ * Settings encoded in a marker line, or null if the text is not a marker.
111
+ * A marker naming a known setting with an unknown value yields that setting
112
+ * omitted (possibly an empty object): the line is still a settings marker.
113
+ */
69
114
  declare function parseTableSettingsMarker(text: string): Partial<TableSettings> | null;
70
- /** Marker line for the given settings, or null when everything is default */
71
- declare function formatTableSettingsMarker(settings: TableSettings): string | null;
115
+ /**
116
+ * Marker line for the given settings, or null when everything is default.
117
+ * With `explicitWidth` the width is listed even when it is `full`.
118
+ */
119
+ declare function formatTableSettingsMarker(settings: TableSettings, options?: TableSettingsOptions): string | null;
72
120
 
73
121
  type MarkdownEditorApi = Readonly<{
74
122
  /** The underlying Lexical editor, for dispatching your own commands */
@@ -76,14 +124,40 @@ type MarkdownEditorApi = Readonly<{
76
124
  /** Text formats active at the current selection */
77
125
  activeFormats: ReadonlySet<TextFormatType>;
78
126
  toggleFormat: (format: TextFormatType) => void;
127
+ /**
128
+ * Insert a table at the selection. `width` defaults to
129
+ * `defaultBlockWidth.table` (Root prop); when given it is written to the
130
+ * markdown explicitly, even when `full`.
131
+ */
79
132
  insertTable: (options?: {
80
133
  rows?: number;
81
134
  columns?: number;
135
+ width?: BlockWidth;
82
136
  }) => void;
83
- insertDrawing: () => void;
84
- /** Width of the table containing the selection; `null` outside tables */
137
+ /**
138
+ * Insert an empty drawing at the selection. `width` defaults to
139
+ * `defaultBlockWidth.drawing` (Root prop); when given it is written to
140
+ * the markdown explicitly, even when `full`.
141
+ */
142
+ insertDrawing: (options?: {
143
+ width?: BlockWidth;
144
+ }) => void;
145
+ /**
146
+ * Width of the block containing the selection — the focused drawing, or
147
+ * else the table the caret is in; `null` outside both.
148
+ */
149
+ blockWidth: BlockWidth | null;
150
+ /** Resize that block; no-op when `blockWidth` is `null` */
151
+ setBlockWidth: (width: BlockWidth) => void;
152
+ /**
153
+ * Width of the table containing the selection; `null` outside tables.
154
+ * @deprecated Use `blockWidth`, which also covers drawings.
155
+ */
85
156
  tableWidth: BlockWidth | null;
86
- /** Resize the table containing the selection; no-op outside tables */
157
+ /**
158
+ * Resize the table containing the selection; no-op outside tables.
159
+ * @deprecated Use `setBlockWidth`, which also covers drawings.
160
+ */
87
161
  setTableWidth: (width: BlockWidth) => void;
88
162
  /** Density of the table containing the selection; `null` outside tables */
89
163
  tableDensity: TableDensity | null;
@@ -283,8 +357,10 @@ type DrawingData = Readonly<{
283
357
  canvasWidth?: number;
284
358
  canvasHeight: number;
285
359
  /**
286
- * Block width: `full` (default, omitted when serialized) spans the
287
- * editor; `content` fits the shapes' horizontal extent.
360
+ * Block width: `full` (default) spans the editor; `text` aligns with the
361
+ * text column; `content` fits the shapes' horizontal extent. The editor
362
+ * omits `full` unless it was written explicitly (an insertion default or
363
+ * the source payload); unknown values are dropped.
288
364
  */
289
365
  width?: BlockWidth;
290
366
  shapes: readonly DrawingShape[];
@@ -350,7 +426,7 @@ declare const DIAGRAM: MultilineElementTransformer;
350
426
  * Markdown transformer for GFM tables. Adapted from the Lexical playground.
351
427
  * Rows are matched line-by-line on import and stitched into a single
352
428
  * TableNode; the divider row promotes the row above it to a header row.
353
- * A `<!-- width: content; density: compact -->` settings line directly
429
+ * A `<!-- width: text; density: compact -->` settings line directly
354
430
  * above the table (see tableSettings.ts) is consumed on import and
355
431
  * re-emitted on export.
356
432
  */
@@ -392,8 +468,8 @@ declare const DRAWING_DATA_JSON_SCHEMA: {
392
468
  readonly description: "Optional logical canvas width in pixels. When set, the drawing is scaled down to fit narrower layouts (SVG viewBox) so coordinates can assume this width. When omitted the canvas is fluid and coordinates are CSS pixels.";
393
469
  };
394
470
  readonly width: {
395
- readonly enum: readonly ["full", "content"];
396
- readonly description: "Block width: \"full\" (default, omit it) spans the editor; \"content\" fits the shapes' horizontal extent and left-aligns with the text";
471
+ readonly enum: readonly ["full", "text", "content"];
472
+ readonly description: "Block width: \"full\" (default, omit it) spans the editor; \"text\" aligns the block with the text column (same as \"full\" unless the app sets a text measure); \"content\" fits the shapes' horizontal extent and left-aligns with the text";
397
473
  };
398
474
  readonly shapes: {
399
475
  readonly type: "array";
@@ -595,6 +671,13 @@ type BoxDefinition = Readonly<{
595
671
  }>;
596
672
  declare const BOX_DEFINITIONS: Record<BoxType, BoxDefinition>;
597
673
 
674
+ /**
675
+ * Skeleton (intent) layer: a compact, LLM-friendly description of a diagram
676
+ * — boxes, connectors between them and loose texts — that expands into a
677
+ * full `DrawingData` document. Sizes, positions and connector geometry are
678
+ * derived, so a generator only needs to say *what* is on the canvas.
679
+ */
680
+
598
681
  type ColorName = 'gray' | 'red' | 'green' | 'blue' | 'orange' | 'purple';
599
682
  declare const COLOR_PRESETS: Record<ColorName, {
600
683
  stroke: string;
@@ -648,7 +731,8 @@ type DrawingSkeleton = Readonly<{
648
731
  direction?: 'right' | 'down';
649
732
  canvasWidth?: number;
650
733
  canvasHeight?: number;
651
- width?: 'content';
734
+ /** Block width of the expanded drawing; omit for `full` */
735
+ width?: BlockWidth;
652
736
  boxes: readonly SkeletonBox[];
653
737
  connectors?: readonly SkeletonConnector[];
654
738
  texts?: readonly SkeletonText[];
@@ -685,8 +769,8 @@ declare const DRAWING_SKELETON_JSON_SCHEMA: {
685
769
  readonly description: "Canvas height in px. Omit to fit the content";
686
770
  };
687
771
  readonly width: {
688
- readonly enum: readonly ["content"];
689
- readonly description: "Block width: \"content\" fits the drawing to its shapes and left-aligns it with the text; omit to span the editor";
772
+ readonly enum: readonly ["full", "text", "content"];
773
+ readonly description: "Block width: \"text\" aligns the drawing with the text column (same as \"full\" unless the app sets a text measure); \"content\" fits the drawing to its shapes and left-aligns it with the text; omit to span the editor";
690
774
  };
691
775
  readonly boxes: {
692
776
  readonly type: "array";
@@ -891,4 +975,4 @@ type MermaidOptions = Readonly<{
891
975
  */
892
976
  declare function drawingToMermaid(data: DrawingData, options?: MermaidOptions): string;
893
977
 
894
- export { $createDrawingNode, $createFrontmatterNode, $getSelectedTable, $getTableDensity, $getTableSettings, $getTableWidth, $isDrawingNode, $isFrontmatterNode, $setTableDensity, $setTableSettings, $setTableWidth, BOX_DEFINITIONS, BOX_TYPES, type Binding, type BindingSide, type BlockWidth, type BoxDefinition, type BoxType, COLOR_PRESETS, CONNECTOR_TYPES, type ColorName, type ConnectorType, DEFAULT_TABLE_SETTINGS, DIAGRAM, DRAWING, DRAWING_DATA_JSON_SCHEMA, DRAWING_SKELETON_JSON_SCHEMA, type DrawingData, DrawingNode, type DrawingShape, type DrawingShapeType, type DrawingSkeleton, EMPTY_DRAWING, type EditorContentProps, type EditorMode, type EditorRootProps, FILL_COLORS, FRONTMATTER, FormatButtons, FrontmatterNode, HistoryButtons, InsertButtons, MarkdownEditor, type MarkdownEditorApi, type Props as MarkdownEditorProps, type MermaidDirection, type MermaidOptions, type Point, SHAPE_TYPES, SIDE_FIXED_POINTS, STROKE_COLORS, type SerializedDrawingNode, type SerializedFrontmatterNode, type SkeletonBox, type SkeletonConnector, type SkeletonEnd, type SkeletonText, TABLE, TABLE_WIDTH_MARKER, type TableDensity, type TableSettings, type TextField, Toolbar, ToolbarButton, ToolbarDivider, type ToolbarItems, anchorPoint, bindEndpoints, boxOutline, connectorPoints, drawingToMermaid, expandSkeleton, findBoxAt, fixedPointFor, formatTableSettingsMarker, isDrawingSkeleton, makeBinding, normalizeDrawingData, parseDrawingData, parseDrawingSkeleton, parseTableSettingsMarker, resolveBindings, routeElbow, serializeDrawingData, useMarkdownEditor };
978
+ export { $createDrawingNode, $createFrontmatterNode, $getSelectedTable, $getTableDensity, $getTableSettings, $getTableWidth, $isDrawingNode, $isFrontmatterNode, $isTableWidthExplicit, $setTableDensity, $setTableSettings, $setTableWidth, BLOCK_WIDTHS, BOX_DEFINITIONS, BOX_TYPES, type Binding, type BindingSide, type BlockWidth, type BlockWidthDefaults, type BoxDefinition, type BoxType, COLOR_PRESETS, CONNECTOR_TYPES, type ColorName, type ConnectorType, DEFAULT_TABLE_SETTINGS, DIAGRAM, DRAWING, DRAWING_DATA_JSON_SCHEMA, DRAWING_SKELETON_JSON_SCHEMA, type DrawingData, DrawingNode, type DrawingShape, type DrawingShapeType, type DrawingSkeleton, EMPTY_DRAWING, type EditorContentProps, type EditorMode, type EditorRootProps, FILL_COLORS, FRONTMATTER, FormatButtons, FrontmatterNode, HistoryButtons, InsertButtons, MarkdownEditor, type MarkdownEditorApi, type Props as MarkdownEditorProps, type MermaidDirection, type MermaidOptions, type Point, SHAPE_TYPES, SIDE_FIXED_POINTS, STROKE_COLORS, type SerializedDrawingNode, type SerializedFrontmatterNode, type SkeletonBox, type SkeletonConnector, type SkeletonEnd, type SkeletonText, TABLE, TABLE_WIDTH_MARKER, type TableDensity, type TableSettings, type TableSettingsOptions, type TextField, Toolbar, ToolbarButton, ToolbarDivider, type ToolbarItems, anchorPoint, bindEndpoints, boxOutline, connectorPoints, drawingToMermaid, expandSkeleton, findBoxAt, fixedPointFor, formatTableSettingsMarker, isBlockWidth, isDrawingSkeleton, makeBinding, normalizeDrawingData, parseDrawingData, parseDrawingSkeleton, parseTableSettingsMarker, resolveBindings, routeElbow, serializeDrawingData, useMarkdownEditor };
package/dist/index.js CHANGED
@@ -1,8 +1,6 @@
1
1
  // src/EditorRoot.tsx
2
2
  import {
3
- createContext,
4
3
  useCallback as useCallback2,
5
- useContext,
6
4
  useEffect as useEffect8,
7
5
  useMemo as useMemo2,
8
6
  useRef as useRef5,
@@ -308,6 +306,18 @@ import { $getNodeByKey as $getNodeByKey2 } from "lexical";
308
306
  import { useLexicalComposerContext as useLexicalComposerContext5 } from "@lexical/react/LexicalComposerContext";
309
307
  import { useLexicalEditable } from "@lexical/react/useLexicalEditable";
310
308
 
309
+ // src/drawing/focusCommand.ts
310
+ import { createCommand } from "lexical";
311
+ var DRAWING_FOCUS_COMMAND = createCommand(
312
+ "DRAWING_FOCUS_COMMAND"
313
+ );
314
+
315
+ // src/blockWidth.ts
316
+ var BLOCK_WIDTHS = ["full", "text", "content"];
317
+ function isBlockWidth(value) {
318
+ return typeof value === "string" && BLOCK_WIDTHS.includes(value);
319
+ }
320
+
311
321
  // src/drawing/types.ts
312
322
  var BOX_TYPES = [
313
323
  "rect",
@@ -393,8 +403,9 @@ function normalizeDrawingData(parsed) {
393
403
  version: 2,
394
404
  canvasHeight: Number.isFinite(height) && height >= MIN_CANVAS_HEIGHT ? height : DEFAULT_CANVAS_HEIGHT,
395
405
  ...Number.isFinite(canvasWidth) && canvasWidth >= 120 ? { canvasWidth } : {},
396
- // `full` is the default and stays implicit so payloads stay compact
397
- ...parsed.width === "content" ? { width: "content" } : {},
406
+ // `full` stays implicit unless the payload spells it out; unknown
407
+ // values (newer versions) are dropped rather than rejected
408
+ ...isBlockWidth(parsed.width) ? { width: parsed.width } : {},
398
409
  // Bindings to ids that don't exist (or aren't boxes) are dropped
399
410
  shapes: shapes.map((shape) => {
400
411
  if (!isConnectorType(shape.type)) return shape;
@@ -2335,6 +2346,14 @@ var BLOCK_WIDTH_OPTIONS = [
2335
2346
  /* @__PURE__ */ jsx7("path", { d: "M6 10h8M8.5 7.5L6 10l2.5 2.5M11.5 7.5L14 10l-2.5 2.5" })
2336
2347
  ] })
2337
2348
  },
2349
+ {
2350
+ width: "text",
2351
+ label: "Text width",
2352
+ icon: /* @__PURE__ */ jsxs6(Fragment, { children: [
2353
+ /* @__PURE__ */ jsx7("path", { d: "M3 4v12M17 4v12" }),
2354
+ /* @__PURE__ */ jsx7("path", { d: "M6.5 7h7M6.5 10h7M6.5 13h4.5" })
2355
+ ] })
2356
+ },
2338
2357
  {
2339
2358
  width: "content",
2340
2359
  label: "Content width",
@@ -2482,6 +2501,11 @@ function DrawingToolbar({
2482
2501
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2483
2502
  var MIN_HEIGHT = 120;
2484
2503
  var MAX_HEIGHT = 1200;
2504
+ var WIDTH_CLASS = {
2505
+ full: "",
2506
+ text: "is-text-width",
2507
+ content: "is-content-width"
2508
+ };
2485
2509
  var MIN_CONTENT_WIDTH = 240;
2486
2510
  var CONTENT_WIDTH_PADDING = 40;
2487
2511
  var EMPTY_SET = /* @__PURE__ */ new Set();
@@ -2564,11 +2588,12 @@ function DrawingCanvas({ nodeKey, data }) {
2564
2588
  const commit = useCallback(
2565
2589
  (nextShapes, options) => {
2566
2590
  const nextWidth = options?.width ?? widthRef.current;
2591
+ const explicitFull = options?.width === void 0 && data.width !== void 0;
2567
2592
  const payload = {
2568
2593
  version: 2,
2569
2594
  ...canvasWidth ? { canvasWidth } : {},
2570
2595
  canvasHeight: options?.height ?? heightRef.current,
2571
- ...nextWidth === "content" ? { width: "content" } : {},
2596
+ ...nextWidth !== "full" || explicitFull ? { width: nextWidth } : {},
2572
2597
  shapes: nextShapes
2573
2598
  };
2574
2599
  const json = serializeDrawingData(payload);
@@ -2579,7 +2604,7 @@ function DrawingCanvas({ nodeKey, data }) {
2579
2604
  if ($isDrawingNode(node)) node.setData(payload);
2580
2605
  });
2581
2606
  },
2582
- [editor, nodeKey, canvasWidth]
2607
+ [editor, nodeKey, canvasWidth, data.width]
2583
2608
  );
2584
2609
  const updateShapes = useCallback(
2585
2610
  (updater, options) => {
@@ -2965,8 +2990,13 @@ function DrawingCanvas({ nodeKey, data }) {
2965
2990
  "div",
2966
2991
  {
2967
2992
  ref: rootRef,
2968
- className: `zui-drawing-canvas ${width === "content" ? "is-content-width" : ""} ${isEditable ? "is-editable" : ""}`,
2993
+ className: `zui-drawing-canvas ${WIDTH_CLASS[width]} ${isEditable ? "is-editable" : ""}`,
2969
2994
  tabIndex: isEditable ? 0 : void 0,
2995
+ onFocus: () => editor.dispatchCommand(DRAWING_FOCUS_COMMAND, nodeKey),
2996
+ onBlur: (e) => {
2997
+ if (rootRef.current?.contains(e.relatedTarget)) return;
2998
+ editor.dispatchCommand(DRAWING_FOCUS_COMMAND, null);
2999
+ },
2970
3000
  children: [
2971
3001
  isEditable && /* @__PURE__ */ jsxs8("div", { className: "zui-drawing-toolbar", onPointerDown: (e) => e.stopPropagation(), children: [
2972
3002
  /* @__PURE__ */ jsx9(
@@ -3214,7 +3244,7 @@ function expandSkeleton(skeleton) {
3214
3244
  version: 2,
3215
3245
  ...skeleton.canvasWidth !== void 0 ? { canvasWidth: skeleton.canvasWidth } : {},
3216
3246
  canvasHeight: skeleton.canvasHeight ?? Math.max(MIN_AUTO_CANVAS_HEIGHT, Math.ceil(bottom + CANVAS_MARGIN)),
3217
- ...skeleton.width === "content" ? { width: "content" } : {},
3247
+ ...skeleton.width !== void 0 ? { width: skeleton.width } : {},
3218
3248
  shapes
3219
3249
  });
3220
3250
  }
@@ -3418,7 +3448,7 @@ function normalizeSkeleton(raw) {
3418
3448
  direction: value.direction === "down" ? "down" : "right",
3419
3449
  ...canvasWidth !== void 0 ? { canvasWidth } : {},
3420
3450
  ...canvasHeight !== void 0 ? { canvasHeight } : {},
3421
- ...value.width === "content" ? { width: "content" } : {},
3451
+ ...isBlockWidth(value.width) ? { width: value.width } : {},
3422
3452
  boxes,
3423
3453
  connectors,
3424
3454
  texts
@@ -3551,8 +3581,8 @@ var DRAWING_SKELETON_JSON_SCHEMA = {
3551
3581
  description: "Canvas height in px. Omit to fit the content"
3552
3582
  },
3553
3583
  width: {
3554
- enum: ["content"],
3555
- description: 'Block width: "content" fits the drawing to its shapes and left-aligns it with the text; omit to span the editor'
3584
+ enum: ["full", "text", "content"],
3585
+ description: 'Block width: "text" aligns the drawing with the text column (same as "full" unless the app sets a text measure); "content" fits the drawing to its shapes and left-aligns it with the text; omit to span the editor'
3556
3586
  },
3557
3587
  boxes: {
3558
3588
  type: "array",
@@ -3788,10 +3818,12 @@ var DEFAULT_TABLE_SETTINGS = {
3788
3818
  width: "full",
3789
3819
  density: "comfortable"
3790
3820
  };
3821
+ var BLOCK_BLEED_PROPERTY = "--zui-text-editor-block-bleed";
3791
3822
  var OPTION_DECLARATIONS = {
3792
3823
  width: {
3793
3824
  full: {},
3794
- content: { width: "auto", "table-layout": "auto" }
3825
+ content: { width: "auto", "table-layout": "auto", [BLOCK_BLEED_PROPERTY]: "0px" },
3826
+ text: { [BLOCK_BLEED_PROPERTY]: "0px" }
3795
3827
  },
3796
3828
  density: {
3797
3829
  comfortable: {},
@@ -3806,6 +3838,7 @@ var OPTION_DECLARATIONS = {
3806
3838
  }
3807
3839
  };
3808
3840
  var SETTING_KEYS = Object.keys(OPTION_DECLARATIONS);
3841
+ var EXPLICIT_WIDTH_PROPERTY = "--zui-table-width";
3809
3842
  function parseStyle(style) {
3810
3843
  const declarations = /* @__PURE__ */ new Map();
3811
3844
  for (const part of style.split(";")) {
@@ -3848,14 +3881,22 @@ function $getTableSettings(table) {
3848
3881
  density: readSetting(declarations, "density")
3849
3882
  };
3850
3883
  }
3851
- function $setTableSettings(table, settings) {
3884
+ function $setTableSettings(table, settings, options = {}) {
3852
3885
  const declarations = parseStyle(table.getStyle());
3853
3886
  for (const key of SETTING_KEYS) {
3854
3887
  const option = settings[key];
3855
3888
  if (option !== void 0) writeSetting(declarations, key, option);
3856
3889
  }
3890
+ if (settings.width !== void 0) {
3891
+ if (options.explicitWidth) declarations.set(EXPLICIT_WIDTH_PROPERTY, settings.width);
3892
+ else declarations.delete(EXPLICIT_WIDTH_PROPERTY);
3893
+ }
3857
3894
  table.setStyle(serializeStyle(declarations));
3858
3895
  }
3896
+ function $isTableWidthExplicit(table) {
3897
+ const declarations = parseStyle(table.getStyle());
3898
+ return declarations.get(EXPLICIT_WIDTH_PROPERTY) === readSetting(declarations, "width");
3899
+ }
3859
3900
  function $getTableWidth(table) {
3860
3901
  return $getTableSettings(table).width;
3861
3902
  }
@@ -3890,16 +3931,19 @@ function parseTableSettingsMarker(text) {
3890
3931
  const match = MARKER_REG_EXP.exec(text);
3891
3932
  if (!match) return null;
3892
3933
  const settings = {};
3934
+ let recognised = false;
3893
3935
  for (const pair of match[1].split(";")) {
3894
3936
  const [key, value] = pair.split(":").map((s) => s.trim());
3937
+ if (key !== "width" && key !== "density") continue;
3938
+ recognised = true;
3895
3939
  if (key === "width" && isOption("width", value)) settings.width = value;
3896
3940
  if (key === "density" && isOption("density", value)) settings.density = value;
3897
3941
  }
3898
- return Object.keys(settings).length > 0 ? settings : null;
3942
+ return recognised ? settings : null;
3899
3943
  }
3900
- function formatTableSettingsMarker(settings) {
3944
+ function formatTableSettingsMarker(settings, options = {}) {
3901
3945
  const pairs = SETTING_KEYS.filter(
3902
- (key) => settings[key] !== DEFAULT_TABLE_SETTINGS[key]
3946
+ (key) => settings[key] !== DEFAULT_TABLE_SETTINGS[key] || key === "width" && options.explicitWidth
3903
3947
  ).map((key) => `${key}: ${settings[key]}`);
3904
3948
  return pairs.length > 0 ? `<!-- ${pairs.join("; ")} -->` : null;
3905
3949
  }
@@ -3932,7 +3976,9 @@ var TABLE = {
3932
3976
  export: (node) => {
3933
3977
  if (!$isTableNode2(node)) return null;
3934
3978
  const output = [];
3935
- const marker = formatTableSettingsMarker($getTableSettings(node));
3979
+ const marker = formatTableSettingsMarker($getTableSettings(node), {
3980
+ explicitWidth: $isTableWidthExplicit(node)
3981
+ });
3936
3982
  if (marker) output.push(marker);
3937
3983
  for (const row of node.getChildren()) {
3938
3984
  if (!$isTableRowNode(row)) continue;
@@ -4001,7 +4047,7 @@ var TABLE = {
4001
4047
  } else {
4002
4048
  const markerSettings = $getMarkerSettings(previousSibling);
4003
4049
  if (markerSettings) {
4004
- $setTableSettings(table, markerSettings);
4050
+ $setTableSettings(table, markerSettings, { explicitWidth: true });
4005
4051
  previousSibling?.remove();
4006
4052
  }
4007
4053
  parentNode.replace(table);
@@ -4016,14 +4062,14 @@ var editorTheme = {
4016
4062
  ltr: "text-left",
4017
4063
  rtl: "text-right",
4018
4064
  placeholder: "text-muted-foreground opacity-50",
4019
- paragraph: "mb-2",
4065
+ paragraph: "zui-paragraph mb-2",
4020
4066
  heading: {
4021
- h1: "text-4xl font-bold mb-4",
4022
- h2: "text-3xl font-bold mb-3",
4023
- h3: "text-2xl font-bold mb-2",
4024
- h4: "text-xl font-bold mb-2",
4025
- h5: "text-lg font-bold mb-1",
4026
- h6: "text-base font-bold mb-1"
4067
+ h1: "zui-heading zui-heading-1 text-4xl font-bold mb-4",
4068
+ h2: "zui-heading zui-heading-2 text-3xl font-bold mb-3",
4069
+ h3: "zui-heading zui-heading-3 text-2xl font-bold mb-2",
4070
+ h4: "zui-heading zui-heading-4 text-xl font-bold mb-2",
4071
+ h5: "zui-heading zui-heading-5 text-lg font-bold mb-1",
4072
+ h6: "zui-heading zui-heading-6 text-base font-bold mb-1"
4027
4073
  },
4028
4074
  list: {
4029
4075
  listitem: "ml-8",
@@ -4033,8 +4079,8 @@ var editorTheme = {
4033
4079
  listitem: "ml-8"
4034
4080
  },
4035
4081
  checklist: "zui-checklist",
4036
- ol: "list-decimal ml-4",
4037
- ul: "list-disc ml-4"
4082
+ ol: "zui-list zui-list-ordered list-decimal ml-4",
4083
+ ul: "zui-list zui-list-unordered list-disc ml-4"
4038
4084
  },
4039
4085
  link: "text-primary underline hover:opacity-80 cursor-pointer",
4040
4086
  text: {
@@ -4047,7 +4093,7 @@ var editorTheme = {
4047
4093
  underline: "underline",
4048
4094
  underlineStrikethrough: "underline line-through"
4049
4095
  },
4050
- code: "bg-muted rounded-md p-4 font-mono text-sm block my-2 overflow-x-auto",
4096
+ code: "zui-code bg-muted rounded-md p-4 font-mono text-sm block my-2 overflow-x-auto",
4051
4097
  codeHighlight: {
4052
4098
  atrule: "text-blue-500",
4053
4099
  attr: "text-yellow-500",
@@ -4080,7 +4126,7 @@ var editorTheme = {
4080
4126
  url: "text-blue-500",
4081
4127
  variable: "text-blue-500"
4082
4128
  },
4083
- quote: "border-l-4 border-border pl-4 italic",
4129
+ quote: "zui-quote border-l-4 border-border pl-4 italic",
4084
4130
  frontmatter: "zui-frontmatter",
4085
4131
  table: "zui-table",
4086
4132
  tableCell: "zui-table-cell",
@@ -4090,10 +4136,8 @@ var editorTheme = {
4090
4136
  drawing: "zui-drawing"
4091
4137
  };
4092
4138
 
4093
- // src/EditorRoot.tsx
4094
- import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
4095
- var SYNC_TRANSFORMERS = [FRONTMATTER, DRAWING, DIAGRAM, CHECK_LIST, TABLE, ...TRANSFORMERS2];
4096
- var SHORTCUT_TRANSFORMERS = [TABLE, ...TRANSFORMERS2.filter((t) => t !== CODE)];
4139
+ // src/editorContext.ts
4140
+ import { createContext, useContext } from "react";
4097
4141
  var EditorContext = createContext(null);
4098
4142
  function useEditorContext() {
4099
4143
  const ctx = useContext(EditorContext);
@@ -4104,6 +4148,12 @@ function useEditorContext() {
4104
4148
  }
4105
4149
  return ctx;
4106
4150
  }
4151
+
4152
+ // src/EditorRoot.tsx
4153
+ import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
4154
+ var SYNC_TRANSFORMERS = [FRONTMATTER, DRAWING, DIAGRAM, CHECK_LIST, TABLE, ...TRANSFORMERS2];
4155
+ var SHORTCUT_TRANSFORMERS = [TABLE, ...TRANSFORMERS2.filter((t) => t !== CODE)];
4156
+ var NO_DEFAULT_BLOCK_WIDTH = {};
4107
4157
  function onError(error) {
4108
4158
  console.error(error);
4109
4159
  }
@@ -4136,6 +4186,8 @@ function EditorRoot({
4136
4186
  className,
4137
4187
  mode = "edit-md",
4138
4188
  autoFocus = false,
4189
+ measure,
4190
+ defaultBlockWidth = NO_DEFAULT_BLOCK_WIDTH,
4139
4191
  children
4140
4192
  }) {
4141
4193
  const latestValueRef = useRef5(value ?? "");
@@ -4176,12 +4228,17 @@ function EditorRoot({
4176
4228
  readOnly,
4177
4229
  autoFocus,
4178
4230
  rawValue: value ?? "",
4179
- onRawChange: handleChange
4231
+ onRawChange: handleChange,
4232
+ defaultBlockWidth
4180
4233
  }),
4181
- [mode, readOnly, autoFocus, value, handleChange]
4234
+ [mode, readOnly, autoFocus, value, handleChange, defaultBlockWidth]
4182
4235
  );
4183
4236
  const isRaw = mode === "edit-raw";
4184
- return /* @__PURE__ */ jsx11(LexicalComposer, { initialConfig, children: /* @__PURE__ */ jsx11(EditorContext.Provider, { value: context, children: /* @__PURE__ */ jsxs9("div", { className: `zui-text-editor ${className ?? ""}`, children: [
4237
+ const rootStyle = useMemo2(
4238
+ () => measure !== void 0 ? { "--zui-text-editor-measure": measure } : void 0,
4239
+ [measure]
4240
+ );
4241
+ return /* @__PURE__ */ jsx11(LexicalComposer, { initialConfig, children: /* @__PURE__ */ jsx11(EditorContext.Provider, { value: context, children: /* @__PURE__ */ jsxs9("div", { className: `zui-text-editor ${className ?? ""}`, style: rootStyle, children: [
4185
4242
  children,
4186
4243
  !isRaw && /* @__PURE__ */ jsxs9(Fragment3, { children: [
4187
4244
  /* @__PURE__ */ jsx11(
@@ -4355,8 +4412,9 @@ import { useLexicalComposerContext as useLexicalComposerContext9 } from "@lexica
4355
4412
  import { useLexicalEditable as useLexicalEditable2 } from "@lexical/react/useLexicalEditable";
4356
4413
 
4357
4414
  // src/useMarkdownEditor.ts
4358
- import { useCallback as useCallback4, useEffect as useEffect10, useState as useState6 } from "react";
4415
+ import { useCallback as useCallback4, useEffect as useEffect10, useRef as useRef7, useState as useState6 } from "react";
4359
4416
  import {
4417
+ $getNodeByKey as $getNodeByKey4,
4360
4418
  $getSelection as $getSelection4,
4361
4419
  $isRangeSelection as $isRangeSelection4,
4362
4420
  CAN_REDO_COMMAND,
@@ -4376,20 +4434,29 @@ var TRACKED_FORMATS = [
4376
4434
  "strikethrough",
4377
4435
  "code"
4378
4436
  ];
4437
+ function $readDrawingWidth(key) {
4438
+ if (key === null) return null;
4439
+ const node = $getNodeByKey4(key);
4440
+ return $isDrawingNode(node) ? node.getData().width ?? "full" : null;
4441
+ }
4379
4442
  function useMarkdownEditor() {
4380
4443
  const [editor] = useLexicalComposerContext8();
4444
+ const { defaultBlockWidth } = useEditorContext();
4381
4445
  const [activeFormats, setActiveFormats] = useState6(
4382
4446
  /* @__PURE__ */ new Set()
4383
4447
  );
4384
4448
  const [canUndo, setCanUndo] = useState6(false);
4385
4449
  const [canRedo, setCanRedo] = useState6(false);
4386
4450
  const [tableSettings, setTableSettingsState] = useState6(null);
4451
+ const activeDrawingRef = useRef7(null);
4452
+ const [drawingWidth, setDrawingWidth] = useState6(null);
4387
4453
  useEffect10(
4388
4454
  () => mergeRegister(
4389
4455
  editor.registerUpdateListener(({ editorState }) => {
4390
4456
  editorState.read(() => {
4391
4457
  const table = $getSelectedTable();
4392
4458
  setTableSettingsState(table ? $getTableSettings(table) : null);
4459
+ setDrawingWidth($readDrawingWidth(activeDrawingRef.current));
4393
4460
  const selection = $getSelection4();
4394
4461
  if (!$isRangeSelection4(selection)) {
4395
4462
  setActiveFormats(/* @__PURE__ */ new Set());
@@ -4402,6 +4469,15 @@ function useMarkdownEditor() {
4402
4469
  setActiveFormats(next);
4403
4470
  });
4404
4471
  }),
4472
+ editor.registerCommand(
4473
+ DRAWING_FOCUS_COMMAND,
4474
+ (key) => {
4475
+ activeDrawingRef.current = key;
4476
+ setDrawingWidth(editor.getEditorState().read(() => $readDrawingWidth(key)));
4477
+ return false;
4478
+ },
4479
+ COMMAND_PRIORITY_LOW2
4480
+ ),
4405
4481
  editor.registerCommand(
4406
4482
  CAN_UNDO_COMMAND,
4407
4483
  (payload) => {
@@ -4428,20 +4504,36 @@ function useMarkdownEditor() {
4428
4504
  [editor]
4429
4505
  );
4430
4506
  const insertTable = useCallback4(
4431
- ({ rows = 3, columns = 3 } = {}) => {
4507
+ ({
4508
+ rows = 3,
4509
+ columns = 3,
4510
+ width = defaultBlockWidth.table
4511
+ } = {}) => {
4512
+ const before = editor.getEditorState().read(() => $getSelectedTable()?.getKey());
4432
4513
  editor.dispatchCommand(INSERT_TABLE_COMMAND, {
4433
4514
  columns: String(columns),
4434
4515
  rows: String(rows),
4435
4516
  includeHeaders: { rows: true, columns: false }
4436
4517
  });
4518
+ if (width === void 0) return;
4519
+ editor.update(() => {
4520
+ const table = $getSelectedTable();
4521
+ if (table && table.getKey() !== before) {
4522
+ $setTableSettings(table, { width }, { explicitWidth: true });
4523
+ }
4524
+ });
4437
4525
  },
4438
- [editor]
4526
+ [editor, defaultBlockWidth.table]
4527
+ );
4528
+ const insertDrawing = useCallback4(
4529
+ ({ width = defaultBlockWidth.drawing } = {}) => {
4530
+ editor.update(() => {
4531
+ const data = width === void 0 ? EMPTY_DRAWING : { version: 2, canvasHeight: EMPTY_DRAWING.canvasHeight, width, shapes: [] };
4532
+ $insertNodeToNearestRoot($createDrawingNode(serializeDrawingData(data)));
4533
+ });
4534
+ },
4535
+ [editor, defaultBlockWidth.drawing]
4439
4536
  );
4440
- const insertDrawing = useCallback4(() => {
4441
- editor.update(() => {
4442
- $insertNodeToNearestRoot($createDrawingNode());
4443
- });
4444
- }, [editor]);
4445
4537
  const updateTableSettings = useCallback4(
4446
4538
  (settings) => {
4447
4539
  editor.update(() => {
@@ -4459,6 +4551,22 @@ function useMarkdownEditor() {
4459
4551
  (density) => updateTableSettings({ density }),
4460
4552
  [updateTableSettings]
4461
4553
  );
4554
+ const setBlockWidth = useCallback4(
4555
+ (width) => {
4556
+ const key = activeDrawingRef.current;
4557
+ if (key === null) {
4558
+ updateTableSettings({ width });
4559
+ return;
4560
+ }
4561
+ editor.update(() => {
4562
+ const node = $getNodeByKey4(key);
4563
+ if (!$isDrawingNode(node)) return;
4564
+ const { width: _previous, ...data } = node.getData();
4565
+ node.setData(width === "full" ? data : { ...data, width });
4566
+ });
4567
+ },
4568
+ [editor, updateTableSettings]
4569
+ );
4462
4570
  const undo = useCallback4(() => {
4463
4571
  editor.dispatchCommand(UNDO_COMMAND, void 0);
4464
4572
  }, [editor]);
@@ -4471,6 +4579,8 @@ function useMarkdownEditor() {
4471
4579
  toggleFormat,
4472
4580
  insertTable,
4473
4581
  insertDrawing,
4582
+ blockWidth: drawingWidth ?? tableSettings?.width ?? null,
4583
+ setBlockWidth,
4474
4584
  tableWidth: tableSettings?.width ?? null,
4475
4585
  setTableWidth,
4476
4586
  tableDensity: tableSettings?.density ?? null,
@@ -4641,14 +4751,18 @@ function Icon3({ children }) {
4641
4751
  function TableSettingsPlugin() {
4642
4752
  const [editor] = useLexicalComposerContext9();
4643
4753
  const isEditable = useLexicalEditable2();
4644
- const { tableWidth, setTableWidth, tableDensity, setTableDensity } = useMarkdownEditor();
4645
4754
  const [anchor, setAnchor] = useState7(null);
4755
+ const [settings, setSettings] = useState7(null);
4646
4756
  const sync = useCallback5(() => {
4647
- const tableKey = editor.getEditorState().read(() => $getSelectedTable()?.getKey());
4648
- const element = tableKey ? editor.getElementByKey(tableKey) : null;
4757
+ const table = editor.getEditorState().read(() => {
4758
+ const node = $getSelectedTable();
4759
+ return node ? { key: node.getKey(), settings: $getTableSettings(node) } : null;
4760
+ });
4761
+ const element = table ? editor.getElementByKey(table.key) : null;
4649
4762
  const main = editor.getRootElement()?.parentElement;
4650
- if (!element || !main) {
4763
+ if (!table || !element || !main) {
4651
4764
  setAnchor(null);
4765
+ setSettings(null);
4652
4766
  return;
4653
4767
  }
4654
4768
  const rect = element.getBoundingClientRect();
@@ -4657,6 +4771,7 @@ function TableSettingsPlugin() {
4657
4771
  top: rect.top - mainRect.top,
4658
4772
  right: mainRect.right - rect.right + 8
4659
4773
  });
4774
+ setSettings(table.settings);
4660
4775
  }, [editor]);
4661
4776
  useEffect11(() => {
4662
4777
  sync();
@@ -4669,7 +4784,16 @@ function TableSettingsPlugin() {
4669
4784
  observer?.disconnect();
4670
4785
  };
4671
4786
  }, [editor, sync]);
4672
- if (!isEditable || !anchor || !tableWidth || !tableDensity) return null;
4787
+ const update = useCallback5(
4788
+ (next) => {
4789
+ editor.update(() => {
4790
+ const table = $getSelectedTable();
4791
+ if (table) $setTableSettings(table, next);
4792
+ });
4793
+ },
4794
+ [editor]
4795
+ );
4796
+ if (!isEditable || !anchor || !settings) return null;
4673
4797
  return /* @__PURE__ */ jsxs11(
4674
4798
  "div",
4675
4799
  {
@@ -4682,8 +4806,8 @@ function TableSettingsPlugin() {
4682
4806
  ToolbarButton,
4683
4807
  {
4684
4808
  label,
4685
- active: tableWidth === width,
4686
- onClick: () => setTableWidth(width),
4809
+ active: settings.width === width,
4810
+ onClick: () => update({ width }),
4687
4811
  children: /* @__PURE__ */ jsx14(Icon3, { children: icon })
4688
4812
  },
4689
4813
  width
@@ -4693,8 +4817,8 @@ function TableSettingsPlugin() {
4693
4817
  ToolbarButton,
4694
4818
  {
4695
4819
  label,
4696
- active: tableDensity === density,
4697
- onClick: () => setTableDensity(density),
4820
+ active: settings.density === density,
4821
+ onClick: () => update({ density }),
4698
4822
  children: /* @__PURE__ */ jsx14(Icon3, { children: icon })
4699
4823
  },
4700
4824
  density
@@ -4923,8 +5047,8 @@ var DRAWING_DATA_JSON_SCHEMA = {
4923
5047
  description: "Optional logical canvas width in pixels. When set, the drawing is scaled down to fit narrower layouts (SVG viewBox) so coordinates can assume this width. When omitted the canvas is fluid and coordinates are CSS pixels."
4924
5048
  },
4925
5049
  width: {
4926
- enum: ["full", "content"],
4927
- description: `Block width: "full" (default, omit it) spans the editor; "content" fits the shapes' horizontal extent and left-aligns with the text`
5050
+ enum: ["full", "text", "content"],
5051
+ description: `Block width: "full" (default, omit it) spans the editor; "text" aligns the block with the text column (same as "full" unless the app sets a text measure); "content" fits the shapes' horizontal extent and left-aligns with the text`
4928
5052
  },
4929
5053
  shapes: {
4930
5054
  type: "array",
@@ -5067,9 +5191,11 @@ export {
5067
5191
  $getTableWidth,
5068
5192
  $isDrawingNode,
5069
5193
  $isFrontmatterNode,
5194
+ $isTableWidthExplicit,
5070
5195
  $setTableDensity,
5071
5196
  $setTableSettings,
5072
5197
  $setTableWidth,
5198
+ BLOCK_WIDTHS,
5073
5199
  BOX_DEFINITIONS,
5074
5200
  BOX_TYPES,
5075
5201
  COLOR_PRESETS,
@@ -5105,6 +5231,7 @@ export {
5105
5231
  findBoxAt,
5106
5232
  fixedPointFor,
5107
5233
  formatTableSettingsMarker,
5234
+ isBlockWidth,
5108
5235
  isDrawingSkeleton,
5109
5236
  makeBinding,
5110
5237
  normalizeDrawingData,
package/dist/styles.css CHANGED
@@ -11,21 +11,62 @@
11
11
  min-height: 100%;
12
12
  }
13
13
 
14
+ /*
15
+ * Text measure (opt in). `--zui-text-editor-measure` — a CSS length, unset
16
+ * by default — caps the text column. The pane's gutter grows from the base
17
+ * 2rem to centre that column, and `--zui-text-editor-bleed` is how far a
18
+ * full-width block extends past the column on each side. With the measure
19
+ * unset both resolve to the previous layout (2rem gutter, no bleed).
20
+ *
21
+ * The pane is a container so `cqw` measures *its* width in every
22
+ * descendant, whatever the descendant's own width; the outline sidebar is
23
+ * a sibling, so full-width blocks stop short of it.
24
+ */
14
25
  .zui-text-editor-main {
15
26
  position: relative;
16
27
  display: flex;
17
28
  flex-direction: column;
18
29
  flex: 1;
19
30
  min-width: 0;
31
+ container-type: inline-size;
32
+ --zui-text-editor-gutter: max(
33
+ 2rem,
34
+ calc((100cqw - var(--zui-text-editor-measure, 100cqw)) / 2)
35
+ );
36
+ --zui-text-editor-bleed: calc(var(--zui-text-editor-gutter) - 2rem);
20
37
  }
21
38
 
22
39
  .zui-text-editor-content {
23
40
  flex: 1;
24
41
  min-height: 100%;
25
- padding: 1.5rem 2rem;
42
+ padding: 1.5rem var(--zui-text-editor-gutter, 2rem);
26
43
  outline: none;
27
44
  }
28
45
 
46
+ /*
47
+ * Block width. Every top-level block sits in the text column and may bleed
48
+ * past it by `--zui-text-editor-block-bleed` (0 unless set). Tables and
49
+ * drawings default it to the pane's bleed (`full`); their `text` and
50
+ * `content` settings pin it to 0 — as an inline style on the table (see
51
+ * tableSettings.ts) and a class on the drawing canvas. Zero-specificity
52
+ * selectors, so one class rule overrides any block kind, e.g.
53
+ * `.zui-code { --zui-text-editor-block-bleed: var(--zui-text-editor-bleed) }`.
54
+ */
55
+ :where(.zui-text-editor-content > *) {
56
+ margin-inline: calc(-1 * var(--zui-text-editor-block-bleed, 0px));
57
+ }
58
+
59
+ :where(.zui-table, .zui-drawing) {
60
+ --zui-text-editor-block-bleed: var(--zui-text-editor-bleed, 0px);
61
+ }
62
+
63
+ :where(
64
+ .zui-drawing:has(> .is-text-width),
65
+ .zui-drawing:has(> .is-content-width)
66
+ ) {
67
+ --zui-text-editor-block-bleed: 0px;
68
+ }
69
+
29
70
  /* Outline (table of contents) sidebar */
30
71
  .zui-text-editor-outline {
31
72
  position: sticky;
@@ -115,7 +156,8 @@
115
156
 
116
157
  .zui-text-editor-fold {
117
158
  position: absolute;
118
- left: 0.45rem;
159
+ /* Just left of the text column; 0.45rem with the default gutter */
160
+ left: calc(var(--zui-text-editor-gutter, 2rem) - 1.55rem);
119
161
  display: inline-flex;
120
162
  align-items: center;
121
163
  justify-content: center;
@@ -155,7 +197,7 @@
155
197
  .zui-text-editor-placeholder {
156
198
  position: absolute;
157
199
  top: 1.5rem;
158
- left: 2rem;
200
+ left: var(--zui-text-editor-gutter, 2rem);
159
201
  pointer-events: none;
160
202
  user-select: none;
161
203
  }
@@ -209,7 +251,7 @@
209
251
  .zui-frontmatter {
210
252
  display: block;
211
253
  white-space: pre-wrap;
212
- margin: 0 0 1rem;
254
+ margin-block: 0 1rem;
213
255
  padding: 0.75rem 1rem;
214
256
  border: 1px solid currentColor;
215
257
  border-radius: 0.375rem;
@@ -272,11 +314,15 @@
272
314
  background: color-mix(in srgb, currentColor 15%, transparent);
273
315
  }
274
316
 
275
- /* Tables — artifact-style: rounded outer border, subtle header, tight rules */
317
+ /*
318
+ * Tables — artifact-style: rounded outer border, subtle header, tight rules.
319
+ * Width follows the block bleed above (inline margins come from the shared
320
+ * block rule); a `content` table sets `width: auto` inline instead.
321
+ */
276
322
  .zui-table {
277
- width: 100%;
278
- max-width: 100%;
279
- margin: 1rem 0;
323
+ width: calc(100% + 2 * var(--zui-text-editor-block-bleed, 0px));
324
+ max-width: calc(100% + 2 * var(--zui-text-editor-block-bleed, 0px));
325
+ margin-block: 1rem;
280
326
  border-collapse: separate;
281
327
  border-spacing: 0;
282
328
  border: 1px solid color-mix(in srgb, currentColor 15%, transparent);
@@ -347,7 +393,7 @@
347
393
 
348
394
  /* Drawing canvas */
349
395
  .zui-drawing {
350
- margin: 1rem 0;
396
+ margin-block: 1rem;
351
397
  }
352
398
 
353
399
  .zui-drawing-canvas {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuilib/text-editor",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "ZUI — A markdown editor component wrapping Lexical",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",