dbml-erd-viewer 0.1.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/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/dbml-erd-viewer.css +2 -0
- package/dist/dbml-erd-viewer.js +902 -0
- package/dist/dbml-erd-viewer.umd.cjs +2 -0
- package/dist/index.d.ts +367 -0
- package/package.json +98 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jinil Lee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
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
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
.react-flow{--xy-edge-stroke-default:#b1b1b7;--xy-edge-stroke-width-default:1;--xy-edge-stroke-selected-default:#555;--xy-connectionline-stroke-default:#b1b1b7;--xy-connectionline-stroke-width-default:1;--xy-attribution-background-color-default:#ffffff80;--xy-minimap-background-color-default:#fff;--xy-minimap-mask-background-color-default:#f0f0f099;--xy-minimap-mask-stroke-color-default:transparent;--xy-minimap-mask-stroke-width-default:1;--xy-minimap-node-background-color-default:#e2e2e2;--xy-minimap-node-stroke-color-default:transparent;--xy-minimap-node-stroke-width-default:2;--xy-background-color-default:transparent;--xy-background-pattern-dots-color-default:#91919a;--xy-background-pattern-lines-color-default:#eee;--xy-background-pattern-cross-color-default:#e2e2e2;background-color:var(--xy-background-color,var(--xy-background-color-default));--xy-node-color-default:inherit;--xy-node-border-default:1px solid #1a192b;--xy-node-background-color-default:#fff;--xy-node-group-background-color-default:#f0f0f040;--xy-node-boxshadow-hover-default:0 1px 4px 1px #00000014;--xy-node-boxshadow-selected-default:0 0 0 .5px #1a192b;--xy-node-border-radius-default:3px;--xy-handle-background-color-default:#1a192b;--xy-handle-border-color-default:#fff;--xy-selection-background-color-default:#0059dc14;--xy-selection-border-default:1px dotted #0059dccc;--xy-controls-button-background-color-default:#fefefe;--xy-controls-button-background-color-hover-default:#f4f4f4;--xy-controls-button-color-default:inherit;--xy-controls-button-color-hover-default:inherit;--xy-controls-button-border-color-default:#eee;--xy-controls-box-shadow-default:0 0 2px 1px #00000014;--xy-edge-label-background-color-default:#fff;--xy-edge-label-color-default:inherit;--xy-resize-background-color-default:#3367d9;direction:ltr}.react-flow.dark{--xy-edge-stroke-default:#3e3e3e;--xy-edge-stroke-width-default:1;--xy-edge-stroke-selected-default:#727272;--xy-connectionline-stroke-default:#b1b1b7;--xy-connectionline-stroke-width-default:1;--xy-attribution-background-color-default:#96969640;--xy-minimap-background-color-default:#141414;--xy-minimap-mask-background-color-default:#3c3c3c99;--xy-minimap-mask-stroke-color-default:transparent;--xy-minimap-mask-stroke-width-default:1;--xy-minimap-node-background-color-default:#2b2b2b;--xy-minimap-node-stroke-color-default:transparent;--xy-minimap-node-stroke-width-default:2;--xy-background-color-default:#141414;--xy-background-pattern-dots-color-default:#777;--xy-background-pattern-lines-color-default:#777;--xy-background-pattern-cross-color-default:#777;--xy-node-color-default:#f8f8f8;--xy-node-border-default:1px solid #3c3c3c;--xy-node-background-color-default:#1e1e1e;--xy-node-group-background-color-default:#f0f0f040;--xy-node-boxshadow-hover-default:0 1px 4px 1px #ffffff14;--xy-node-boxshadow-selected-default:0 0 0 .5px #999;--xy-handle-background-color-default:#bebebe;--xy-handle-border-color-default:#1e1e1e;--xy-selection-background-color-default:#c8c8dc14;--xy-selection-border-default:1px dotted #c8c8dccc;--xy-controls-button-background-color-default:#2b2b2b;--xy-controls-button-background-color-hover-default:#3e3e3e;--xy-controls-button-color-default:#f8f8f8;--xy-controls-button-color-hover-default:#fff;--xy-controls-button-border-color-default:#5b5b5b;--xy-controls-box-shadow-default:0 0 2px 1px #00000014;--xy-edge-label-background-color-default:#141414;--xy-edge-label-color-default:#f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props,var(--xy-background-color,var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__pane{z-index:1;touch-action:none}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke,var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width,var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke,var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width,var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{pointer-events:none;position:absolute;overflow:visible}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:.5s linear infinite dashdraw}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected,var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke,var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke,var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:.5s linear infinite dashdraw}svg.react-flow__connectionline{z-index:1001;position:absolute;overflow:visible}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{-webkit-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default;position:absolute}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:0 0;pointer-events:none}.react-flow__nodesselection-rect{pointer-events:all;cursor:grab;position:absolute}.react-flow__handle{pointer-events:none;background-color:var(--xy-handle-background-color,var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color,var(--xy-handle-border-color-default));border-radius:100%;width:6px;min-width:5px;height:6px;min-height:5px;position:absolute}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;bottom:0;left:50%;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{z-index:5;margin:15px;position:absolute}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px)translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px)translateY(-50%)}.react-flow__attribution{background:var(--xy-attribution-background-color,var(--xy-attribution-background-color-default));margin:0;padding:2px 3px;font-size:10px}.react-flow__attribution a{color:#999;text-decoration:none}@keyframes dashdraw{0%{stroke-dashoffset:10px}}.react-flow__edgelabel-renderer{pointer-events:none;-webkit-user-select:none;user-select:none;width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__viewport-portal{-webkit-user-select:none;user-select:none;width:100%;height:100%;position:absolute;top:0;left:0}.react-flow__minimap{background:var(--xy-minimap-background-color-props,var(--xy-minimap-background-color,var(--xy-minimap-background-color-default)))}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var(--xy-minimap-mask-background-color-props,var(--xy-minimap-mask-background-color,var(--xy-minimap-mask-background-color-default)));stroke:var(--xy-minimap-mask-stroke-color-props,var(--xy-minimap-mask-stroke-color,var(--xy-minimap-mask-stroke-color-default)));stroke-width:var(--xy-minimap-mask-stroke-width-props,var(--xy-minimap-mask-stroke-width,var(--xy-minimap-mask-stroke-width-default)))}.react-flow__minimap-node{fill:var(--xy-minimap-node-background-color-props,var(--xy-minimap-node-background-color,var(--xy-minimap-node-background-color-default)));stroke:var(--xy-minimap-node-stroke-color-props,var(--xy-minimap-node-stroke-color,var(--xy-minimap-node-stroke-color-default)));stroke-width:var(--xy-minimap-node-stroke-width-props,var(--xy-minimap-node-stroke-width,var(--xy-minimap-node-stroke-width-default)))}.react-flow__background-pattern.dots{fill:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-dots-color-default)))}.react-flow__background-pattern.lines{stroke:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-lines-color-default)))}.react-flow__background-pattern.cross{stroke:var(--xy-background-pattern-color-props,var(--xy-background-pattern-color,var(--xy-background-pattern-cross-color-default)))}.react-flow__controls{box-shadow:var(--xy-controls-box-shadow,var(--xy-controls-box-shadow-default));flex-direction:column;display:flex}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{background:var(--xy-controls-button-background-color,var(--xy-controls-button-background-color-default));border:none;border-bottom:1px solid var(--xy-controls-button-border-color-props,var(--xy-controls-button-border-color,var(--xy-controls-button-border-color-default)));width:26px;height:26px;color:var(--xy-controls-button-color-props,var(--xy-controls-button-color,var(--xy-controls-button-color-default)));cursor:pointer;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;padding:4px;display:flex}.react-flow__controls-button svg{fill:currentColor;width:100%;max-width:12px;max-height:12px}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{border-radius:var(--xy-node-border-radius,var(--xy-node-border-radius-default));width:150px;color:var(--xy-node-color,var(--xy-node-color-default));text-align:center;border:var(--xy-node-border,var(--xy-node-border-default));background-color:var(--xy-node-background-color,var(--xy-node-background-color-default));padding:10px;font-size:12px}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover,var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected,var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color,var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color,var(--xy-selection-background-color-default));border:var(--xy-selection-border,var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var(--xy-controls-button-background-color-hover-props,var(--xy-controls-button-background-color-hover,var(--xy-controls-button-background-color-hover-default)));color:var(--xy-controls-button-color-hover-props,var(--xy-controls-button-color-hover,var(--xy-controls-button-color-hover-default)))}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var(--xy-controls-button-border-color-props,var(--xy-controls-button-border-color,var(--xy-controls-button-border-color-default)))}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{background-color:var(--xy-resize-background-color,var(--xy-resize-background-color-default));border:1px solid #fff;border-radius:1px;width:5px;height:5px;translate:-50% -50%}.react-flow__resize-control.handle.left{top:50%;left:0}.react-flow__resize-control.handle.right{top:50%;left:100%}.react-flow__resize-control.handle.top{top:0;left:50%}.react-flow__resize-control.handle.bottom{top:100%;left:50%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color,var(--xy-resize-background-color-default));border-style:solid;border-width:0}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;height:100%;top:0;transform:translate(-50%)}.react-flow__resize-control.line.left{border-left-width:1px;left:0}.react-flow__resize-control.line.right{border-right-width:1px;left:100%}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{width:100%;height:1px;left:0;transform:translateY(-50%)}.react-flow__resize-control.line.top{border-top-width:1px;top:0}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color,var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color,var(--xy-edge-label-color-default))}.dv-viewer{background:var(--dv-canvas,#fafbfc);--dv-bg:#fff;--dv-border:#d9dee5;--dv-header-bg:#2d3748;--dv-header-fg:#fff;--dv-row-fg:#1a202c;--dv-type-fg:#718096;--dv-row-hover:#f1f5f9;--dv-row-highlight:#dbeafe;--dv-pk:#b7791f;--dv-fk:#2b6cb0;--dv-null:#b06a6a;--dv-edge:#94a3b8;--dv-edge-active:#2b6cb0;--dv-font:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif}.dv-table{background:var(--dv-bg);border:1px solid var(--dv-border);font-family:var(--dv-font);border-radius:8px;font-size:12px;overflow:hidden;box-shadow:0 1px 3px #00000014}.dv-table__header{background:var(--dv-header-bg);color:var(--dv-header-fg);align-items:center;gap:2px;padding:0 12px;font-size:13px;font-weight:600;display:flex}.dv-table__schema{opacity:.7;font-weight:400}.dv-table__body{flex-direction:column;display:flex}.dv-row{border-top:1px solid var(--dv-border);color:var(--dv-row-fg);justify-content:space-between;align-items:center;gap:8px;padding:0 12px;display:flex}.dv-row:hover{background:var(--dv-row-hover)}.dv-row--highlighted{background:var(--dv-row-highlight);box-shadow:inset 2px 0 0 var(--dv-edge-active,#2b6cb0)}.dv-row__name{white-space:nowrap;text-overflow:ellipsis;align-items:center;gap:6px;display:inline-flex;overflow:hidden}.dv-row__name--pk{font-weight:600}.dv-row__type{color:var(--dv-type-fg);white-space:nowrap;align-items:center;gap:6px;display:inline-flex}.dv-row__null{border:1.25px solid var(--dv-null);box-sizing:border-box;border-radius:50%;flex:none;width:6px;height:6px}.dv-row__null--notnull{background:var(--dv-null)}.dv-badge{color:#fff;border-radius:3px;padding:2px 4px;font-size:9px;font-weight:700;line-height:1;display:inline-block}.dv-badge--pk{background:var(--dv-pk)}.dv-badge--fk{background:var(--dv-fk)}.dv-erd-edge__interaction{stroke:#0000;stroke-width:18px;fill:none}.dv-error{box-sizing:border-box;justify-content:center;align-items:center;padding:24px;display:flex}.dv-error__message{color:#c53030;white-space:pre-wrap;word-break:break-word;background:#fff5f5;border:1px solid #feb2b2;border-radius:8px;max-width:640px;margin:0;padding:16px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px}
|
|
2
|
+
/*$vite$:1*/
|