dbml-erd-viewer 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,329 +1,377 @@
1
- <h1 align="center">dbml-erd-viewer</h1>
2
-
3
- <p align="center">
4
- Render a <a href="https://dbml.dbdiagram.io/">DBML</a> database schema as an interactive ERD —
5
- a React component built on <a href="https://reactflow.dev">@xyflow/react</a> (React Flow).
6
- </p>
7
-
8
- <p align="center">
9
- <a href="https://www.npmjs.com/package/dbml-erd-viewer"><img src="https://img.shields.io/npm/v/dbml-erd-viewer.svg" alt="npm version" /></a>
10
- <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT license" />
11
- <img src="https://img.shields.io/badge/types-included-blue.svg" alt="TypeScript types included" />
12
- <a href="https://usingsky.github.io/dbml-erd-viewer/"><img src="https://img.shields.io/badge/playground-live-success.svg" alt="Live playground" /></a>
13
- </p>
14
-
15
- <p align="center">
16
- <img src="docs/hero.png" alt="dbml-erd-viewer rendering a schema with crow's-foot relationships" width="900" />
17
- </p>
18
-
19
- <p align="center"><strong><a href="https://usingsky.github.io/dbml-erd-viewer/">▶ Try the live playground</a></strong> — tweak every prop interactively.</p>
20
-
21
- ## Features
22
-
23
- - **DBML in, diagram out** — parses schemas with [`@dbml/core`](https://github.com/holistics/dbml); tables become draggable nodes with PK/FK badges, types, notes, and a nullability dot (filled = `not null`, hollow = nullable).
24
- - **Crow's-foot notation** — MySQL-Workbench-style edges: identifying (solid) vs non-identifying (dashed), cardinality, and optionality, all inferred from the DBML.
25
- - **Pluggable layout** — zero-dependency built-in layout, or opt into [`dagre`](https://github.com/dagrejs/dagre) / [`elkjs`](https://github.com/kieler/elkjs) for crossing minimization.
26
- - **Themeable** — light/dark presets plus per-token CSS variables and custom fonts.
27
- - **Interactive** — drag tables, persist positions, hover to highlight related columns, one-click auto-layout.
28
- - **Export** — save the diagram as PNG or SVG.
29
- - **Typed & dual-format** — ships TypeScript types, ESM + UMD.
30
-
31
- ## Install
32
-
33
- ```bash
34
- npm install dbml-erd-viewer @xyflow/react @dbml/core react react-dom
35
- ```
36
-
37
- `react`, `react-dom`, `@xyflow/react`, and `@dbml/core` are treated as external — install them
38
- alongside the library (they are listed as dependencies, so a default install pulls them in).
39
-
40
- ## Usage
41
-
42
- ```tsx
43
- import { DbmlViewer } from 'dbml-erd-viewer';
44
- import 'dbml-erd-viewer/styles.css'; // bundles the required React Flow base styles
45
-
46
- const dbml = `
47
- Table users {
48
- id int [pk, increment]
49
- username varchar [not null, unique]
50
- }
51
-
52
- Table posts {
53
- id int [pk]
54
- user_id int [ref: > users.id]
55
- title varchar
56
- }
57
- `;
58
-
59
- export default function App() {
60
- return (
61
- <div style={{ width: '100%', height: 600 }}>
62
- <DbmlViewer dbml={dbml} showMiniMap />
63
- </div>
64
- );
65
- }
66
- ```
67
-
68
- > The viewer fills its parent, so give the wrapping element an explicit height.
69
-
70
- ## Relationship notation
71
-
72
- Edges follow MySQL Workbench's EER diagram (crow's-foot) notation:
73
-
74
- | Visual | Meaning |
75
- | ------------------------------ | ----------------------------------------------------------------- |
76
- | **Solid line** | Identifying relationship — the FK is part of the child's primary key |
77
- | **Dashed line** | Non-identifying relationship — the FK is not part of the primary key |
78
- | Crow's foot (`<`) | "Many" side (the child table that holds the foreign key) |
79
- | Double bar (`‖`) | "One and only one" a `not null` foreign key |
80
- | Bar + ring (`⊣O`) | "Zero or one" a nullable foreign key |
81
-
82
- These are derived automatically from the DBML: cardinality from the `>`/`-`/`<` ref operator,
83
- identifying vs non-identifying from primary-key membership (including composite `[pk]` indexes),
84
- and the "one" side's optionality from the foreign-key column's nullability. The "many" (crow's-foot)
85
- side carries a cardinality bar (Workbench style) by default — whether a parent may have zero children
86
- isn't expressible in standard DBML.
87
-
88
- **Optional collection (opt-in):** if the foreign-key column's `note` contains the word `optional`,
89
- the crow's-foot side is drawn with a ring instead of the bar ("zero or many"):
90
-
91
- ```dbml
92
- Table line_items {
93
- order_id int [not null, ref: > orders.id] // crow's foot + bar
94
- promo_id int [ref: > promos.id, note: 'optional'] // crow's foot + ring (zero or many)
95
- }
96
- ```
97
-
98
- ## `<DbmlViewer>` props
99
-
100
- | Prop | Type | Default | Description |
101
- | ---------------- | ----------------------------- | ------- | ---------------------------------------------- |
102
- | `dbml` | `string` | | DBML source text to render (required). |
103
- | `fitView` | `boolean` | `true` | Fit the diagram into view on load. |
104
- | `showControls` | `boolean` | `true` | Show the zoom/pan controls. |
105
- | `showMiniMap` | `boolean` | `false` | Show the minimap. |
106
- | `showBackground` | `boolean` | `true` | Show the dotted background grid. |
107
- | `layoutOptions` | `LayoutOptions` | | Algorithm, direction, and node spacing (see below). |
108
- | `onParseError` | `(error: DbmlParseError) => void` | | Called when the DBML fails to parse. |
109
- | `onLayoutError` | `(error: LayoutError) => void` | — | Called when a `dagre`/`elk` layout fails (e.g. missing optional dep). |
110
- | `nodePositions` | `NodePositions` | | Saved table positions (`id → {x,y}`) to restore. See [Persisting positions](#persisting-node-positions). |
111
- | `onNodePositionsChange` | `(positions: NodePositions) => void` | — | Called after a table drag with the full positions map to persist. |
112
- | `className` | `string` | — | Class applied to the wrapper element. |
113
- | `style` | `CSSProperties` | — | Inline style for the wrapper element. |
114
-
115
- If the DBML is invalid, the viewer renders the parser errors inline (one line per problem, with
116
- `Line:column — message`) and, if provided, calls `onParseError`. The `DbmlParseError` also exposes a
117
- structured `diagnostics: DbmlDiagnostic[]` array (`{ message, line?, column?, code? }`) if you want to
118
- render errors your own way:
119
-
120
- ```tsx
121
- <DbmlViewer
122
- dbml={dbml}
123
- onParseError={(err) => err.diagnostics.forEach((d) => console.log(d.line, d.message))}
124
- />
125
- ```
126
-
127
- ## Layout
128
-
129
- `layoutOptions.algorithm` selects how tables are positioned:
130
-
131
- | Algorithm | Dependency | Notes |
132
- | ---------- | ----------------- | --------------------------------------------------------------------- |
133
- | `'simple'` | none (default) | Built-in left-to-right layering. Zero dependencies, synchronous. |
134
- | `'dagre'` | `@dagrejs/dagre` | Sugiyama layered layout with crossing minimization. |
135
- | `'elk'` | `elkjs` | ELK `layered` algorithm; strongest crossing reduction for dense schemas. |
136
-
137
- `dagre` and `elk` are **optional peer dependencies**, loaded on demand only when selected — they are
138
- not in the default bundle. Install whichever you use:
139
-
140
- ```bash
141
- npm install @dagrejs/dagre # for algorithm: 'dagre'
142
- npm install elkjs # for algorithm: 'elk'
143
- ```
144
-
145
- ```tsx
146
- <DbmlViewer
147
- dbml={dbml}
148
- layoutOptions={{ algorithm: 'elk', direction: 'LR', horizontalGap: 120, verticalGap: 40 }}
149
- onLayoutError={(e) => console.error(e.message)}
150
- />
151
- ```
152
-
153
- If a `dagre`/`elk` layout throws (e.g. the dependency isn't installed), the viewer falls back to the
154
- built-in `simple` layout and calls `onLayoutError`. `direction` is `'LR'` (left→right) or `'TB'`
155
- (top→bottom).
156
-
157
- ## Persisting node positions
158
-
159
- Tables are draggable. To keep where the user moved them, store the positions yourself and pass
160
- them back via `nodePositions`. The library is storage-agnostic `onNodePositionsChange` fires after
161
- each drag with the full `id {x, y}` map:
162
-
163
- ```tsx
164
- const KEY = 'erd-positions';
165
- const [positions, setPositions] = useState<NodePositions>(
166
- () => JSON.parse(localStorage.getItem(KEY) ?? '{}'),
167
- );
168
-
169
- const save = (next: NodePositions) => {
170
- setPositions(next);
171
- localStorage.setItem(KEY, JSON.stringify(next));
172
- };
173
-
174
- <DbmlViewer dbml={dbml} nodePositions={positions} onNodePositionsChange={save} />;
175
- ```
176
-
177
- `nodePositions` is applied when the schema/layout (re)builds: a saved position wins, and tables
178
- without one fall back to auto-layout (so newly added tables still get placed). To reset to a fresh
179
- auto-layout, pass `{}` / `undefined`. The same precedence applies when switching layout algorithm —
180
- clear the saved map if you want the new algorithm to reposition everything.
181
-
182
- ### Stable positions while editing
183
-
184
- The viewer only re-runs auto-layout when the **set of tables** or the **layout options** change.
185
- Editing column-level details (columns, types, notes) or even relations between the existing
186
- tables keeps every node where it is and just refreshes the contents and edges. Adding a table keeps
187
- the existing tables in place and only positions the new one; changing the layout algorithm/direction
188
- reshuffles everything. This holds whether or not you use `nodePositions`, so manual drags survive
189
- ordinary edits.
190
-
191
- To deliberately re-arrange everything, the control panel (when `showControls` is on) has an
192
- **Auto layout** button (hierarchy icon). Clicking it re-runs the current layout algorithm on all
193
- tables, fits the view, and — if you use `nodePositions` — emits the new arrangement via
194
- `onNodePositionsChange` so it persists.
195
-
196
- ## Exporting to PNG / SVG
197
-
198
- Pass a `ref` to get a `DbmlViewerHandle` and export the full diagram (all tables, framed at
199
- 100%) as an image. This uses the optional `html-to-image` dependency, loaded on demand:
200
-
201
- ```bash
202
- npm install html-to-image
203
- ```
204
-
205
- ```tsx
206
- import { useRef } from 'react';
207
- import { DbmlViewer, type DbmlViewerHandle } from 'dbml-erd-viewer';
208
-
209
- function App() {
210
- const viewer = useRef<DbmlViewerHandle>(null);
211
- return (
212
- <>
213
- <button onClick={() => viewer.current?.download('schema.png', { type: 'png' })}>
214
- Export PNG
215
- </button>
216
- <DbmlViewer ref={viewer} dbml={dbml} />
217
- </>
218
- );
219
- }
220
- ```
221
-
222
- `DbmlViewerHandle`:
223
-
224
- | Method | Description |
225
- | ------------------------------------------------------- | -------------------------------------------------- |
226
- | `download(filename, options?)` | Render the diagram and trigger a browser download. |
227
- | `toDataUrl(options?)` `Promise<string>` | Return the image as a data URL. |
228
-
229
- `DiagramExportOptions`: `type` (`'png'` \| `'svg'`, default `'png'`), `padding` (default `24`),
230
- `backgroundColor` (defaults to the viewer's canvas color), `pixelRatio` (PNG sharpness, default `2`).
231
- The export reflects the current theme. If `html-to-image` isn't installed, the methods reject with an
232
- `ExportError`.
233
-
234
- ## Lower-level API
235
-
236
- The parsing and layout helpers are exported for building custom renderers:
237
-
238
- ```ts
239
- import { parseDbml, layoutSchema } from 'dbml-erd-viewer';
240
-
241
- const schema = parseDbml(dbml); // { tables, relations }
242
- const boxes = layoutSchema(schema); // Map<tableId, { x, y, width, height }>
243
- ```
244
-
245
- See `src/types.ts` for the full `ParsedSchema`, `TableInfo`, `ColumnInfo`, and `RelationInfo` shapes.
246
-
247
- ## Theming
248
-
249
- There are three ways to customize the look, from most to least convenient:
250
-
251
- **1. The `theme` prop** — pass a (partial) `DbmlViewerTheme`; each token maps to a CSS variable and
252
- only the keys you set are overridden. The `darkTheme` and `lightTheme` presets are exported:
253
-
254
- ```tsx
255
- import { DbmlViewer, darkTheme, type DbmlViewerTheme } from 'dbml-erd-viewer';
256
-
257
- // Use a preset directly…
258
- <DbmlViewer dbml={dbml} theme={darkTheme} />;
259
-
260
- // …or spread a preset and tweak, or pass your own partial theme:
261
- const custom: DbmlViewerTheme = { ...darkTheme, headerBackground: '#4f46e5', edge: '#818cf8' };
262
- <DbmlViewer dbml={dbml} theme={custom} />;
263
- ```
264
-
265
- Available tokens: `canvas`, `background`, `border`, `headerBackground`, `headerForeground`,
266
- `rowForeground`, `typeForeground`, `rowHover`, `rowHighlight`, `primaryKey`, `foreignKey`, `edge`,
267
- `edgeActive`, `fontFamily`.
268
-
269
- **2. CSS custom properties** — the same variables, scoped to `.dv-viewer`, set in your own
270
- stylesheet for an app-wide default:
271
-
272
- ```css
273
- .dv-viewer {
274
- --dv-header-bg: #1e3a8a;
275
- --dv-edge: #1e3a8a;
276
- --dv-pk: #b7791f;
277
- --dv-fk: #2b6cb0;
278
- }
279
- ```
280
-
281
- **3. Class hooks** — for anything beyond colors/fonts, target the structural classes directly:
282
- `.dv-table`, `.dv-table__header`, `.dv-row`, `.dv-row--highlighted`, `.dv-badge--pk`,
283
- `.dv-badge--fk`, `.dv-erd-edge__path`, `.dv-erd-marker`. You can also pass `className`/`style` to
284
- the wrapper.
285
-
286
- ## Fonts
287
-
288
- The table text uses the `fontFamily` theme token (CSS variable `--dv-font`), which defaults to the
289
- system UI font stack. To use your own font, set it via the `theme` prop or the CSS variable:
290
-
291
- ```tsx
292
- <DbmlViewer dbml={dbml} theme={{ fontFamily: '"Inter", sans-serif' }} />
293
- ```
294
-
295
- ```css
296
- /* app-wide, in your own stylesheet */
297
- .dv-viewer { --dv-font: "Inter", sans-serif; }
298
- ```
299
-
300
- The library does **not** bundle any fonts — load the font yourself (the same way you load fonts for
301
- the rest of your app), then reference it by name:
302
-
303
- ```html
304
- <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&display=swap" />
305
- ```
306
-
307
- ```css
308
- /* or self-hosted */
309
- @font-face { font-family: "Inter"; src: url("/fonts/Inter.woff2") format("woff2"); }
310
- ```
311
-
312
- **Image export:** PNG/SVG export inlines the rendered text, so a custom font must be **loaded before
313
- export** or the image falls back to a default font. The viewer waits for `document.fonts.ready`
314
- before capturing, which covers fonts already requested by the page; if you load a font only for the
315
- diagram, trigger it first (e.g. `await document.fonts.load('16px "Inter"')`) before calling
316
- `download()` / `toDataUrl()`.
317
-
318
- ## Acknowledgements
319
-
320
- This library is built on top of two excellent projects:
321
-
322
- - [**@dbml/core**](https://github.com/holistics/dbml) ([npm](https://www.npmjs.com/package/@dbml/core), [docs](https://dbml.dbdiagram.io/)) — the official DBML parser, used to parse the schema into tables and relations.
323
- - [**@xyflow/react**](https://github.com/xyflow/xyflow) ([npm](https://www.npmjs.com/package/@xyflow/react), [docs](https://reactflow.dev)) — React Flow, used to render the interactive node-and-edge diagram.
324
-
325
- Many thanks to their maintainers and contributors.
326
-
327
- ## License
328
-
329
- MIT
1
+ <h1 align="center">dbml-erd-viewer</h1>
2
+
3
+ <p align="center">
4
+ Render a <a href="https://dbml.dbdiagram.io/">DBML</a> database schema as an interactive ERD —
5
+ a React component built on <a href="https://reactflow.dev">@xyflow/react</a> (React Flow).
6
+ </p>
7
+
8
+ <p align="center">
9
+ <a href="https://www.npmjs.com/package/dbml-erd-viewer"><img src="https://img.shields.io/npm/v/dbml-erd-viewer.svg" alt="npm version" /></a>
10
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT license" />
11
+ <img src="https://img.shields.io/badge/types-included-blue.svg" alt="TypeScript types included" />
12
+ <a href="https://usingsky.github.io/dbml-erd-viewer/"><img src="https://img.shields.io/badge/playground-live-success.svg" alt="Live playground" /></a>
13
+ </p>
14
+
15
+ <p align="center">
16
+ <img src="docs/hero.png" alt="dbml-erd-viewer rendering a schema with crow's-foot relationships" width="900" />
17
+ </p>
18
+
19
+ <p align="center"><strong><a href="https://usingsky.github.io/dbml-erd-viewer/">▶ Try the live playground</a></strong> — tweak every prop interactively.</p>
20
+
21
+ ## Features
22
+
23
+ - **DBML in, diagram out** — parses schemas with [`@dbml/core`](https://github.com/holistics/dbml); tables become draggable nodes with PK/FK badges, types, notes, and a nullability dot (filled = `not null`, hollow = nullable).
24
+ - **Crow's-foot notation** — MySQL-Workbench-style edges: identifying (solid) vs non-identifying (dashed), cardinality, and optionality, all inferred from the DBML.
25
+ - **Pluggable layout** — zero-dependency built-in layout, or opt into [`dagre`](https://github.com/dagrejs/dagre) / [`elkjs`](https://github.com/kieler/elkjs) for crossing minimization. Content-sized nodes with configurable min/max width.
26
+ - **Flexible edges** — column-anchored (default) or floating table-to-table connections that follow tables as they move.
27
+ - **Themeable** — light/dark presets plus per-token CSS variables (in a cascade layer) and custom fonts.
28
+ - **Interactive** — drag tables, persist positions, hover an edge **or** column to highlight the relationship, one-click auto-layout.
29
+ - **Imperative API** — control the viewport (`fitView`, zoom, center) and export via a `ref`.
30
+ - **Export** — save the diagram as PNG or SVG.
31
+ - **Typed & dual-format** — ships TypeScript types, ESM + UMD.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ npm install dbml-erd-viewer @xyflow/react @dbml/core react react-dom
37
+ ```
38
+
39
+ `react`, `react-dom`, `@xyflow/react`, and `@dbml/core` are treated as external — install them
40
+ alongside the library (they are listed as dependencies, so a default install pulls them in).
41
+
42
+ ## Usage
43
+
44
+ ```tsx
45
+ import { DbmlViewer } from 'dbml-erd-viewer';
46
+ import 'dbml-erd-viewer/styles.css'; // bundles the required React Flow base styles
47
+
48
+ const dbml = `
49
+ Table users {
50
+ id int [pk, increment]
51
+ username varchar [not null, unique]
52
+ }
53
+
54
+ Table posts {
55
+ id int [pk]
56
+ user_id int [ref: > users.id]
57
+ title varchar
58
+ }
59
+ `;
60
+
61
+ export default function App() {
62
+ return (
63
+ <div style={{ width: '100%', height: 600 }}>
64
+ <DbmlViewer dbml={dbml} showMiniMap />
65
+ </div>
66
+ );
67
+ }
68
+ ```
69
+
70
+ > The viewer fills its parent, so give the wrapping element an explicit height.
71
+
72
+ ## Relationship notation
73
+
74
+ Edges follow MySQL Workbench's EER diagram (crow's-foot) notation:
75
+
76
+ | Visual | Meaning |
77
+ | ------------------------------ | ----------------------------------------------------------------- |
78
+ | **Solid line** | Identifying relationship the FK is part of the child's primary key |
79
+ | **Dashed line** | Non-identifying relationship the FK is not part of the primary key |
80
+ | Crow's foot (`<`) | "Many" side (the child table that holds the foreign key) |
81
+ | Double bar (`‖`) | "One and only one" — a `not null` foreign key |
82
+ | Bar + ring (`⊣O`) | "Zero or one" a nullable foreign key |
83
+
84
+ These are derived automatically from the DBML: cardinality from the `>`/`-`/`<` ref operator,
85
+ identifying vs non-identifying from primary-key membership (including composite `[pk]` indexes),
86
+ and the "one" side's optionality from the foreign-key column's nullability. The "many" (crow's-foot)
87
+ side carries a cardinality bar (Workbench style) by default — whether a parent may have zero children
88
+ isn't expressible in standard DBML.
89
+
90
+ **Optional collection (opt-in):** if the foreign-key column's `note` contains the word `optional`,
91
+ the crow's-foot side is drawn with a ring instead of the bar ("zero or many"):
92
+
93
+ ```dbml
94
+ Table line_items {
95
+ order_id int [not null, ref: > orders.id] // crow's foot + bar
96
+ promo_id int [ref: > promos.id, note: 'optional'] // crow's foot + ring (zero or many)
97
+ }
98
+ ```
99
+
100
+ ## `<DbmlViewer>` props
101
+
102
+ | Prop | Type | Default | Description |
103
+ | ---------------- | ----------------------------- | ------- | ---------------------------------------------- |
104
+ | `dbml` | `string` | | DBML source text to render (required). |
105
+ | `fitView` | `boolean` | `true` | Fit the diagram into view on load. |
106
+ | `showControls` | `boolean` | `true` | Show the zoom/pan controls. |
107
+ | `showMiniMap` | `boolean` | `false` | Show the minimap. |
108
+ | `showBackground` | `boolean` | `true` | Show the dotted background grid. |
109
+ | `layoutOptions` | `LayoutOptions` | — | Algorithm, direction, and node spacing (see below). |
110
+ | `edgeConnection` | `'column' \| 'floating'` | `'column'` | How edges attach to tables (see [Edge connection mode](#edge-connection-mode)). |
111
+ | `onParseError` | `(error: DbmlParseError) => void` | — | Called when the DBML fails to parse. |
112
+ | `onLayoutError` | `(error: LayoutError) => void` | — | Called when a `dagre`/`elk` layout fails (e.g. missing optional dep). |
113
+ | `nodePositions` | `NodePositions` | — | Saved table positions (`id {x,y}`) to restore. See [Persisting positions](#persisting-node-positions). |
114
+ | `onNodePositionsChange` | `(positions: NodePositions) => void` | — | Called after a table drag with the full positions map to persist. |
115
+ | `className` | `string` | — | Class applied to the wrapper element. |
116
+ | `style` | `CSSProperties` | — | Inline style for the wrapper element. |
117
+
118
+ If the DBML is invalid, the viewer renders the parser errors inline (one line per problem, with
119
+ `Line:column — message`) and, if provided, calls `onParseError`. The `DbmlParseError` also exposes a
120
+ structured `diagnostics: DbmlDiagnostic[]` array (`{ message, line?, column?, code? }`) if you want to
121
+ render errors your own way:
122
+
123
+ ```tsx
124
+ <DbmlViewer
125
+ dbml={dbml}
126
+ onParseError={(err) => err.diagnostics.forEach((d) => console.log(d.line, d.message))}
127
+ />
128
+ ```
129
+
130
+ ## Layout
131
+
132
+ `layoutOptions.algorithm` selects how tables are positioned:
133
+
134
+ | Algorithm | Dependency | Notes |
135
+ | ---------- | ----------------- | --------------------------------------------------------------------- |
136
+ | `'simple'` | none (default) | Built-in left-to-right layering. Zero dependencies, synchronous. |
137
+ | `'dagre'` | `@dagrejs/dagre` | Sugiyama layered layout with crossing minimization. |
138
+ | `'elk'` | `elkjs` | ELK `layered` algorithm; strongest crossing reduction for dense schemas. |
139
+
140
+ `dagre` and `elk` are **optional peer dependencies**, loaded on demand only when selected — they are
141
+ not in the default bundle. Install whichever you use:
142
+
143
+ ```bash
144
+ npm install @dagrejs/dagre # for algorithm: 'dagre'
145
+ npm install elkjs # for algorithm: 'elk'
146
+ ```
147
+
148
+ ```tsx
149
+ <DbmlViewer
150
+ dbml={dbml}
151
+ layoutOptions={{ algorithm: 'elk', direction: 'LR', horizontalGap: 120, verticalGap: 40 }}
152
+ onLayoutError={(e) => console.error(e.message)}
153
+ />
154
+ ```
155
+
156
+ If a `dagre`/`elk` layout throws (e.g. the dependency isn't installed), the viewer falls back to the
157
+ built-in `simple` layout and calls `onLayoutError`. `direction` is `'LR'` (left→right) or `'TB'`
158
+ (top→bottom).
159
+
160
+ The `simple` layout places FK-connected tables as the layered graph, then packs tables with **no
161
+ relations** into a grid below it (filling left-to-right, then wrapping) instead of stacking them in a
162
+ tall left-hand column.
163
+
164
+ ### Node width
165
+
166
+ Table nodes are sized to their content (the widest column row and the header), clamped between
167
+ `minNodeWidth` and `maxNodeWidth`. Content wider than `maxNodeWidth` is truncated with an ellipsis
168
+ (the full text stays in the element's `title`). Both are part of `layoutOptions`:
169
+
170
+ | Option | Default | Notes |
171
+ | -------------- | ------- | ------------------------------------------------------------------ |
172
+ | `minNodeWidth` | `160` | Lower bound for a node's width, in px. |
173
+ | `maxNodeWidth` | `320` | Upper bound; longer column names/types ellipsize at this width. |
174
+
175
+ ```tsx
176
+ <DbmlViewer dbml={dbml} layoutOptions={{ minNodeWidth: 200, maxNodeWidth: 480 }} />
177
+ ```
178
+
179
+ ## Edge connection mode
180
+
181
+ `edgeConnection` controls how relations attach to tables:
182
+
183
+ | Value | Behaviour |
184
+ | ------------ | ------------------------------------------------------------------------------------ |
185
+ | `'column'` | Default. Each edge anchors to its specific FK/PK column rows. |
186
+ | `'floating'` | Edges connect table-to-table, attaching wherever the two tables face each other and following them as they move ([floating-edge style](https://reactflow.dev/examples/edges/simple-floating-edges)). |
187
+
188
+ ```tsx
189
+ <DbmlViewer dbml={dbml} edgeConnection="floating" />
190
+ ```
191
+
192
+ ## Persisting node positions
193
+
194
+ Tables are draggable. To keep where the user moved them, store the positions yourself and pass
195
+ them back via `nodePositions`. The library is storage-agnostic — `onNodePositionsChange` fires after
196
+ each drag with the full `id → {x, y}` map:
197
+
198
+ ```tsx
199
+ const KEY = 'erd-positions';
200
+ const [positions, setPositions] = useState<NodePositions>(
201
+ () => JSON.parse(localStorage.getItem(KEY) ?? '{}'),
202
+ );
203
+
204
+ const save = (next: NodePositions) => {
205
+ setPositions(next);
206
+ localStorage.setItem(KEY, JSON.stringify(next));
207
+ };
208
+
209
+ <DbmlViewer dbml={dbml} nodePositions={positions} onNodePositionsChange={save} />;
210
+ ```
211
+
212
+ `nodePositions` is applied when the schema/layout (re)builds: a saved position wins, and tables
213
+ without one fall back to auto-layout (so newly added tables still get placed). To reset to a fresh
214
+ auto-layout, pass `{}` / `undefined`. The same precedence applies when switching layout algorithm —
215
+ clear the saved map if you want the new algorithm to reposition everything.
216
+
217
+ ### Stable positions while editing
218
+
219
+ The viewer only re-runs auto-layout when the **set of tables** or the **layout options** change.
220
+ Editing column-level details (columns, types, notes) — or even relations — between the existing
221
+ tables keeps every node where it is and just refreshes the contents and edges. Adding a table keeps
222
+ the existing tables in place and only positions the new one; changing the layout algorithm/direction
223
+ reshuffles everything. This holds whether or not you use `nodePositions`, so manual drags survive
224
+ ordinary edits.
225
+
226
+ To deliberately re-arrange everything, the control panel (when `showControls` is on) has an
227
+ **Auto layout** button (hierarchy icon). Clicking it re-runs the current layout algorithm on all
228
+ tables, fits the view, and — if you use `nodePositions` — emits the new arrangement via
229
+ `onNodePositionsChange` so it persists.
230
+
231
+ ## Exporting to PNG / SVG
232
+
233
+ Pass a `ref` to get a `DbmlViewerHandle` and export the full diagram (all tables, framed at
234
+ 100%) as an image. This uses the optional `html-to-image` dependency, loaded on demand:
235
+
236
+ ```bash
237
+ npm install html-to-image
238
+ ```
239
+
240
+ ```tsx
241
+ import { useRef } from 'react';
242
+ import { DbmlViewer, type DbmlViewerHandle } from 'dbml-erd-viewer';
243
+
244
+ function App() {
245
+ const viewer = useRef<DbmlViewerHandle>(null);
246
+ return (
247
+ <>
248
+ <button onClick={() => viewer.current?.download('schema.png', { type: 'png' })}>
249
+ Export PNG
250
+ </button>
251
+ <DbmlViewer ref={viewer} dbml={dbml} />
252
+ </>
253
+ );
254
+ }
255
+ ```
256
+
257
+ `DbmlViewerHandle`:
258
+
259
+ | Method | Description |
260
+ | ------------------------------------------ | -------------------------------------------------- |
261
+ | `download(filename, options?)` | Render the diagram and trigger a browser download. |
262
+ | `toDataUrl(options?)` → `Promise<string>` | Return the image as a data URL. |
263
+ | `fitView(options?)` | Fit the whole diagram into the viewport. |
264
+ | `zoomIn(options?)` / `zoomOut(options?)` | Zoom by one step. |
265
+ | `zoomTo(level, options?)` | Zoom to a specific level (`1` = 100%). |
266
+ | `setCenter(x, y, options?)` | Center the viewport on a flow-coordinate point. |
267
+ | `fitBounds(bounds, options?)` | Fit a flow-coordinate rectangle into the viewport. |
268
+ | `setViewport(viewport, options?)` / `getViewport()` | Set / read the viewport (`{ x, y, zoom }`). |
269
+
270
+ `DiagramExportOptions`: `type` (`'png'` \| `'svg'`, default `'png'`), `padding` (default `24`),
271
+ `backgroundColor` (defaults to the viewer's canvas color), `pixelRatio` (PNG sharpness, default `2`).
272
+ The export reflects the current theme. If `html-to-image` isn't installed, the methods reject with an
273
+ `ExportError`.
274
+
275
+ The viewport methods delegate to the underlying React Flow instance (and are no-ops before the
276
+ diagram mounts); their option types (`FitViewOptions`, `Viewport`, …) are re-exported from the package.
277
+
278
+ ## Lower-level API
279
+
280
+ The parsing and layout helpers are exported for building custom renderers:
281
+
282
+ ```ts
283
+ import { parseDbml, layoutSchema } from 'dbml-erd-viewer';
284
+
285
+ const schema = parseDbml(dbml); // { tables, relations }
286
+ const boxes = layoutSchema(schema); // Map<tableId, { x, y, width, height }>
287
+ ```
288
+
289
+ See `src/types.ts` for the full `ParsedSchema`, `TableInfo`, `ColumnInfo`, and `RelationInfo` shapes.
290
+
291
+ ## Theming
292
+
293
+ There are three ways to customize the look, from most to least convenient:
294
+
295
+ **1. The `theme` prop** — pass a (partial) `DbmlViewerTheme`; each token maps to a CSS variable and
296
+ only the keys you set are overridden. The `darkTheme` and `lightTheme` presets are exported:
297
+
298
+ ```tsx
299
+ import { DbmlViewer, darkTheme, type DbmlViewerTheme } from 'dbml-erd-viewer';
300
+
301
+ // Use a preset directly…
302
+ <DbmlViewer dbml={dbml} theme={darkTheme} />;
303
+
304
+ // …or spread a preset and tweak, or pass your own partial theme:
305
+ const custom: DbmlViewerTheme = { ...darkTheme, headerBackground: '#4f46e5', edge: '#818cf8' };
306
+ <DbmlViewer dbml={dbml} theme={custom} />;
307
+ ```
308
+
309
+ Available tokens: `canvas`, `background`, `border`, `headerBackground`, `headerForeground`,
310
+ `rowForeground`, `typeForeground`, `rowHover`, `rowHighlight`, `primaryKey`, `foreignKey`, `edge`,
311
+ `edgeActive`, `fontFamily`.
312
+
313
+ **2. CSS custom properties** the same variables, scoped to `.dv-viewer`, set in your own
314
+ stylesheet for an app-wide default:
315
+
316
+ ```css
317
+ .dv-viewer {
318
+ --dv-header-bg: #1e3a8a;
319
+ --dv-edge: #1e3a8a;
320
+ --dv-pk: #b7791f;
321
+ --dv-fk: #2b6cb0;
322
+ }
323
+ ```
324
+
325
+ **3. Class hooks** for anything beyond colors/fonts, target the structural classes directly:
326
+ `.dv-table`, `.dv-table__header`, `.dv-row`, `.dv-row--highlighted`, `.dv-badge--pk`,
327
+ `.dv-badge--fk`, `.dv-erd-edge__path`, `.dv-erd-marker`. You can also pass `className`/`style` to
328
+ the wrapper.
329
+
330
+ > All bundled styles live in the `dbml-erd-viewer` [cascade layer](https://developer.mozilla.org/en-US/docs/Web/CSS/@layer).
331
+ > Because unlayered styles always beat layered ones, your own CSS (options 2 and 3) overrides the
332
+ > defaults regardless of import order or selector specificity — no `!important` required.
333
+
334
+ ## Fonts
335
+
336
+ The table text uses the `fontFamily` theme token (CSS variable `--dv-font`), which defaults to the
337
+ system UI font stack. To use your own font, set it via the `theme` prop or the CSS variable:
338
+
339
+ ```tsx
340
+ <DbmlViewer dbml={dbml} theme={{ fontFamily: '"Inter", sans-serif' }} />
341
+ ```
342
+
343
+ ```css
344
+ /* app-wide, in your own stylesheet */
345
+ .dv-viewer { --dv-font: "Inter", sans-serif; }
346
+ ```
347
+
348
+ The library does **not** bundle any fonts — load the font yourself (the same way you load fonts for
349
+ the rest of your app), then reference it by name:
350
+
351
+ ```html
352
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&display=swap" />
353
+ ```
354
+
355
+ ```css
356
+ /* or self-hosted */
357
+ @font-face { font-family: "Inter"; src: url("/fonts/Inter.woff2") format("woff2"); }
358
+ ```
359
+
360
+ **Image export:** PNG/SVG export inlines the rendered text, so a custom font must be **loaded before
361
+ export** or the image falls back to a default font. The viewer waits for `document.fonts.ready`
362
+ before capturing, which covers fonts already requested by the page; if you load a font only for the
363
+ diagram, trigger it first (e.g. `await document.fonts.load('16px "Inter"')`) before calling
364
+ `download()` / `toDataUrl()`.
365
+
366
+ ## Acknowledgements
367
+
368
+ This library is built on top of two excellent projects:
369
+
370
+ - [**@dbml/core**](https://github.com/holistics/dbml) ([npm](https://www.npmjs.com/package/@dbml/core), [docs](https://dbml.dbdiagram.io/)) — the official DBML parser, used to parse the schema into tables and relations.
371
+ - [**@xyflow/react**](https://github.com/xyflow/xyflow) ([npm](https://www.npmjs.com/package/@xyflow/react), [docs](https://reactflow.dev)) — React Flow, used to render the interactive node-and-edge diagram.
372
+
373
+ Many thanks to their maintainers and contributors.
374
+
375
+ ## License
376
+
377
+ MIT