dbdocs 1.6.2__tar.gz → 1.7.0__tar.gz

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.
Files changed (56) hide show
  1. {dbdocs-1.6.2 → dbdocs-1.7.0}/PKG-INFO +1 -1
  2. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/core/artifacts.py +2 -2
  3. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/erd.py +25 -23
  4. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/builder.py +6 -1
  5. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/bundle/assets/css/style.css +10 -4
  6. dbdocs-1.7.0/dbdocs/site/bundle/assets/graph/index.css +1 -0
  7. dbdocs-1.7.0/dbdocs/site/bundle/assets/graph/index.js +62 -0
  8. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/bundle/assets/js/data/data.js +1 -1
  9. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/bundle/assets/js/service/service.js +1 -0
  10. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/bundle/assets/js/ui/overlays.js +4 -1
  11. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/bundle/assets/js/ui/ui.js +17 -3
  12. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/deploy.py +1 -1
  13. dbdocs-1.6.2/dbdocs/site/bundle/assets/graph/index.css +0 -1
  14. dbdocs-1.6.2/dbdocs/site/bundle/assets/graph/index.js +0 -62
  15. {dbdocs-1.6.2 → dbdocs-1.7.0}/.gitignore +0 -0
  16. {dbdocs-1.6.2 → dbdocs-1.7.0}/LICENSE +0 -0
  17. {dbdocs-1.6.2 → dbdocs-1.7.0}/README.md +0 -0
  18. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/__init__.py +0 -0
  19. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/__main__.py +0 -0
  20. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/cli/__init__.py +0 -0
  21. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/cli/main.py +0 -0
  22. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/core/__init__.py +0 -0
  23. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/core/config.py +0 -0
  24. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/core/exceptions.py +0 -0
  25. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/core/log.py +0 -0
  26. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/__init__.py +0 -0
  27. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/_sqlglot_lineage.py +0 -0
  28. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/column_lineage.py +0 -0
  29. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/graph.py +0 -0
  30. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/health/__init__.py +0 -0
  31. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/health/dimensions.py +0 -0
  32. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/health/extractor.py +0 -0
  33. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/health/rules/__init__.py +0 -0
  34. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/health/rules/base.py +0 -0
  35. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/health/rules/dimensions/__init__.py +0 -0
  36. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/health/rules/dimensions/documentation.py +0 -0
  37. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/health/rules/dimensions/governance.py +0 -0
  38. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/health/rules/dimensions/modeling.py +0 -0
  39. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/health/rules/dimensions/performance.py +0 -0
  40. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/health/rules/dimensions/structure.py +0 -0
  41. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/health/rules/dimensions/testing.py +0 -0
  42. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/health/rules/registry.py +0 -0
  43. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/nodes.py +0 -0
  44. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/extract/tests_index.py +0 -0
  45. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/main.py +0 -0
  46. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/__init__.py +0 -0
  47. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/api_schema.py +0 -0
  48. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/bundle/assets/css/icons.css +0 -0
  49. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/bundle/assets/favicon.svg +0 -0
  50. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/bundle/assets/js/app.js +0 -0
  51. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/bundle/assets/js/ui/dom.js +0 -0
  52. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/bundle/assets/vendor/marked.min.js +0 -0
  53. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/bundle/assets/vendor/minisearch.min.js +0 -0
  54. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/bundle/index.html +0 -0
  55. {dbdocs-1.6.2 → dbdocs-1.7.0}/dbdocs/site/inject.py +0 -0
  56. {dbdocs-1.6.2 → dbdocs-1.7.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbdocs
3
- Version: 1.6.2
3
+ Version: 1.7.0
4
4
  Summary: Alternative dbt docs site: Catalog + ERD + column-level lineage
5
5
  Project-URL: Homepage, https://github.com/datnguye/dbt-docs
6
6
  Project-URL: Repository, https://github.com/datnguye/dbt-docs
@@ -1,10 +1,10 @@
1
1
  """Loading dbt artifacts (manifest/catalog) via the dbterd parser.
2
2
 
3
- dbterd parses ``manifest.json`` / ``catalog.json`` into ``dbt_artifacts_parser``
3
+ dbterd parses ``manifest.json`` / ``catalog.json`` into ``artifact_parser``
4
4
  Pydantic models. Two cross-cutting gotchas live here so the rest of dbdocs never
5
5
  has to think about them:
6
6
 
7
- * **Schema field aliasing.** ``dbt_artifacts_parser`` aliases the ``schema``
7
+ * **Schema field aliasing.** ``artifact_parser`` aliases the ``schema``
8
8
  field to ``schema_`` to avoid clobbering Pydantic's ``BaseModel.schema()`` —
9
9
  so ``node.schema`` is a *bound method*, not the value. Always read
10
10
  ``node.schema_``; :func:`db_schema` centralizes that.
@@ -1,15 +1,19 @@
1
1
  """Structured ERD data via dbterd's official ``json`` target.
2
2
 
3
- dbterd 1.28.0 ships a built-in, schema-validated ``json`` target that emits
3
+ dbterd ships a built-in, schema-validated ``json`` target that emits
4
4
  ``{nodes, edges, metadata}``. Each node carries ``id`` (the dbt unique_id),
5
5
  ``name``, ``schema_name``, ``database``, ``resource_type``, and ``columns``
6
6
  (with ``data_type``, ``is_primary_key``, ``is_foreign_key``). Each edge carries
7
7
  ``id``, ``from_id`` (FK/child side), ``to_id`` (referenced/parent side),
8
- ``from_columns``, ``to_columns``, ``label``, and ``cardinality``.
9
-
10
- ``build_erd_data`` maps that shape into the SPA's ``{nodes, edges}`` — the
11
- React Flow bundle reads ``nodes`` (entities + column flags) and ``edges``
12
- (FK relationships, ``source``/``target`` keyed by dbt unique_id).
8
+ ``from_columns``, ``to_columns``, ``label``, ``relationship_type``, and
9
+ ``cardinality``.
10
+
11
+ ``build_erd_data`` maps that shape into the SPA's ``{nodes, edges}`` using
12
+ dbterd's own field names throughout — no renaming. The React Flow bundle reads
13
+ ``nodes`` (entities + column flags) and ``edges`` (FK relationships, keyed by
14
+ dbt unique_id in ``from_id``/``to_id``) and maps ``from_id``/``to_id`` to React
15
+ Flow's ``source``/``target`` on its own side, with the parent/referenced node
16
+ as ``to_id``.
13
17
  """
14
18
 
15
19
  import json
@@ -54,17 +58,14 @@ def build_erd_data(erd: DbtErd) -> dict:
54
58
  When ``entity_name_format`` is configured, dbterd emits edge ``from_id`` /
55
59
  ``to_id`` as the formatted entity name (e.g. ``orders``) rather than the
56
60
  full unique_id (e.g. ``model.jaffle_shop.orders``). A ``name_to_id`` map
57
- resolves those short names back to the node id before building edges, so the
58
- SPA's ``source``/``target`` always reference a valid node ``id``.
61
+ resolves those short names back to the node id before building edges, so
62
+ the emitted ``from_id``/``to_id`` always reference a valid node ``id``.
59
63
 
60
64
  Some dbterd algos (e.g. ``model_contract``) do not set ``is_foreign_key``
61
65
  on node columns even when those columns appear in FK edges. After building
62
66
  edges, any column named in an edge's ``from_columns`` (the FK/child side)
63
- has its ``is_foreign_key`` flag back-filled to ``True`` on the target node.
64
-
65
- SPA edge direction: ``source`` is the referenced (parent) side, ``target``
66
- is the FK (child) side — matching dbterd's ``to_id`` and ``from_id``
67
- respectively.
67
+ has its ``is_foreign_key`` flag back-filled to ``True`` on the ``from_id``
68
+ node.
68
69
  """
69
70
  payload = json.loads(erd.get_erd())
70
71
  raw_nodes = payload.get("nodes", [])
@@ -95,13 +96,13 @@ def _backfill_fk_flags(nodes: "list[dict]", edges: "list[dict]") -> None:
95
96
  """
96
97
  nodes_by_id = {n["id"]: n for n in nodes}
97
98
  for edge in edges:
98
- target_node = nodes_by_id.get(edge["target"])
99
- if target_node is None:
99
+ from_node = nodes_by_id.get(edge["from_id"])
100
+ if from_node is None:
100
101
  continue
101
102
  fk_cols = {c.lower() for c in edge.get("from_columns", [])}
102
103
  if not fk_cols:
103
104
  continue
104
- for col in target_node["columns"]:
105
+ for col in from_node["columns"]:
105
106
  if col["name"].lower() in fk_cols:
106
107
  col["is_foreign_key"] = True
107
108
 
@@ -111,12 +112,12 @@ def _build_node(node: dict) -> dict:
111
112
  "id": node["id"],
112
113
  "label": node.get("name") or "",
113
114
  "database": node.get("database") or "",
114
- "schema": node.get("schema_name") or "",
115
+ "schema_name": node.get("schema_name") or "",
115
116
  "resource_type": node.get("resource_type") or "model",
116
117
  "columns": [
117
118
  {
118
119
  "name": c["name"],
119
- "type": c.get("data_type") or "",
120
+ "data_type": c.get("data_type") or "",
120
121
  "description": c.get("description") or "",
121
122
  "is_primary_key": bool(c.get("is_primary_key")),
122
123
  "is_foreign_key": bool(c.get("is_foreign_key")),
@@ -135,16 +136,17 @@ def _resolve_edge_id(raw: str, node_ids: "set[str]", name_to_id: "dict[str, str]
135
136
 
136
137
 
137
138
  def _build_edge(edge: dict, index: int, node_ids: "set[str]", name_to_id: "dict[str, str]") -> dict:
138
- # from_id is the FK/child side; to_id is the referenced/parent side.
139
- # SPA convention: source = parent (to_id), target = child (from_id).
139
+ # from_id is the FK/child side; to_id is the referenced/parent side —
140
+ # dbterd's own convention, carried through unchanged.
140
141
  raw_from = edge.get("from_id") or ""
141
142
  raw_to = edge.get("to_id") or ""
142
143
  return {
143
144
  "id": edge.get("id") or f"e{index}",
144
- "source": _resolve_edge_id(raw_to, node_ids, name_to_id),
145
- "target": _resolve_edge_id(raw_from, node_ids, name_to_id),
145
+ "from_id": _resolve_edge_id(raw_from, node_ids, name_to_id),
146
+ "to_id": _resolve_edge_id(raw_to, node_ids, name_to_id),
146
147
  "from_columns": edge.get("from_columns") or [],
147
148
  "to_columns": edge.get("to_columns") or [],
148
149
  "label": edge.get("label") or "",
149
- "type": edge.get("cardinality") or "",
150
+ "relationship_type": edge.get("relationship_type") or "",
151
+ "cardinality": edge.get("cardinality") or "",
150
152
  }
@@ -211,7 +211,7 @@ class ReportBuilder:
211
211
  counts[label] = counts.get(label, 0) + len(collection)
212
212
  return counts
213
213
 
214
- def generate(self, output_dir: "str | None" = None) -> str:
214
+ def generate(self, output_dir: "str | None" = None, versioned: bool = False) -> str:
215
215
  """Render the site into ``output_dir`` (or config's). Returns its path.
216
216
 
217
217
  The data dict is written as an external ``dbdocs-data.json.gz`` that the
@@ -225,6 +225,10 @@ class ReportBuilder:
225
225
  see ``_write_api`` for the layout. The api/ directory is created after the
226
226
  bundle copytree (so the rmtree + copytree cycle doesn't touch it) and is
227
227
  not shipped in the wheel.
228
+
229
+ ``versioned=True`` is set by ``deploy()`` to signal that a
230
+ ``versions.json`` sibling exists; the SPA gates its version-switcher fetch
231
+ on this flag so a plain ``generate`` build never makes a 404 request.
228
232
  """
229
233
  out = Path(output_dir) if output_dir else Path(self.config.output_path)
230
234
  if out.exists():
@@ -233,6 +237,7 @@ class ReportBuilder:
233
237
  copytree(src=BUNDLE_DIR, dst=out, dirs_exist_ok=True)
234
238
 
235
239
  data = self.build_data()
240
+ data["metadata"]["versioned"] = versioned
236
241
  data["metadata"].update(self._stage_branding(out))
237
242
  index = out / "index.html"
238
243
  index.write_text(strip_marker(index.read_text(encoding="utf-8")), encoding="utf-8")
@@ -190,7 +190,11 @@ body.nav-collapsed .nav-collapse { display: none; }
190
190
  }
191
191
  .nav-reopen:hover { color: var(--accent); border-color: var(--accent); }
192
192
  body.nav-collapsed .nav-reopen { display: inline-flex; }
193
- .sidebar { flex: 1; overflow-y: auto; padding: 12px; }
193
+ .sidebar {
194
+ flex: 1; min-height: 0; overflow: hidden; padding: 12px;
195
+ display: flex; flex-direction: column;
196
+ }
197
+ .sidebar > .nav-cta, .sidebar > .nav-tabs, .sidebar > .nav-filter { flex-shrink: 0; }
194
198
  .nav-filter {
195
199
  width: 100%; box-sizing: border-box; margin: 6px 0 10px;
196
200
  padding: 7px 10px; font-size: 13px; border-radius: var(--radius);
@@ -289,7 +293,7 @@ body.nav-collapsed .nav-reopen { display: inline-flex; }
289
293
  .nav-tab:hover { color: var(--text); background: var(--bg-elev); }
290
294
  .nav-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
291
295
  .nav-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
292
- .nav-tab-panel { min-height: 0; }
296
+ .nav-tab-panel { flex: 1; min-height: 0; overflow-y: auto; }
293
297
 
294
298
  /* Resource-type dot */
295
299
  .dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; display: inline-block; }
@@ -460,8 +464,10 @@ details.node-section:last-of-type { border-bottom: none; }
460
464
  /* Graph host (React Flow bundle mounts here) */
461
465
  .graph-host { width: 100%; }
462
466
  /* Fullscreen: the host fills the screen; its inner .dbd-graph grows to match. */
463
- .graph-host:fullscreen { background: var(--bg); padding: 0; }
464
- .graph-host:fullscreen .dbd-graph { height: 100vh; border: none; border-radius: 0; }
467
+ .graph-host:fullscreen,
468
+ .erd-section-host:fullscreen { background: var(--bg); padding: 0; }
469
+ .graph-host:fullscreen .dbd-graph,
470
+ .erd-section-host:fullscreen .dbd-graph { height: 100vh; border: none; border-radius: 0; }
465
471
 
466
472
  .page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
467
473
  .page-head h2 { border-bottom: none; padding-bottom: 0; }
@@ -0,0 +1 @@
1
+ .erd-table{background:var(--erd-node-bg, #1e1e1e);color:var(--erd-node-fg, #d4d4d4);border:1px solid var(--erd-border, #007acc);border-radius:6px;font-family:var(--erd-font-mono, monospace);font-size:12px;min-width:220px;box-shadow:0 1px 3px var(--erd-shadow, rgba(0, 0, 0, .25))}.erd-table[data-resource=source]{border-style:dashed}.erd-table[data-filter=match]{border-color:var(--erd-accent, #e5c07b);box-shadow:0 0 0 2px var(--erd-accent, #e5c07b),0 1px 4px var(--erd-shadow, rgba(0, 0, 0, .35))}.erd-table[data-filter=dim]{opacity:.32;filter:saturate(.6);transition:opacity .12s ease,filter .12s ease}.erd-table[data-filter=dim]:hover{opacity:.85;filter:none}.erd-table[data-active=true]{border-color:var(--erd-border, #007acc);box-shadow:0 0 0 2px var(--erd-border, #007acc),0 2px 8px var(--erd-shadow, rgba(0, 0, 0, .4))}.erd-table[data-active=true][data-filter=match]{box-shadow:0 0 0 2px var(--erd-border, #007acc),inset 0 0 0 1px var(--erd-accent, #e5c07b)}.erd-table-header{display:flex;align-items:center;gap:6px;box-sizing:border-box;height:32px;padding:6px 10px;background:var(--erd-header-bg, #252526);border-bottom:1px solid var(--erd-border, #007acc);cursor:pointer;-webkit-user-select:none;user-select:none}.erd-table-header:hover{background:var(--erd-hover-bg, #2a2d2e)}.erd-table-header:focus-visible{outline:2px solid var(--erd-border, #007acc);outline-offset:-2px}.erd-table-name{font-weight:600}.erd-table-schema{margin-left:auto;color:var(--erd-muted-fg, #808080);font-size:11px}.erd-table-columns{list-style:none;margin:0;padding:4px 0}.erd-column{display:grid;grid-template-columns:28px 1fr auto;align-items:center;gap:6px;box-sizing:border-box;height:22px;padding:0 10px;position:relative}.erd-column[data-highlighted=true]{background:color-mix(in srgb,var(--erd-accent, #e5c07b) 16%,transparent);box-shadow:inset 2px 0 0 var(--erd-accent, #e5c07b)}.erd-column[data-highlighted=true] .erd-column-name{color:var(--erd-accent, #e5c07b);font-weight:600}.erd-column-badge{font-size:10px;font-weight:700;text-align:center}.badge-pk{color:var(--erd-accent, #e5c07b)}.badge-fk{color:var(--erd-fk, #61afef)}.badge-none{color:transparent}.erd-column-type{color:var(--erd-muted-fg, #808080)}.erd-table-icon{display:inline-flex;align-items:center;color:var(--erd-icon, #FF694A)}.erd-table-more{box-sizing:border-box;height:22px;padding:0 10px;border-top:1px solid var(--erd-divider, #3c3c3c);display:flex;align-items:center;color:var(--erd-muted-fg, #808080);font-size:11px;font-style:italic}.erd-table[data-dimmed=true]{opacity:.25;transition:opacity .12s ease}.erd-table-expand{display:block;width:100%;box-sizing:border-box;height:24px;padding:0 10px;background:transparent;color:var(--erd-link-fg, #3794ff);border:none;border-top:1px dashed var(--erd-divider, #3c3c3c);text-align:center;cursor:pointer;font-size:11px;font-family:inherit}.erd-table-expand:hover{background:var(--erd-hover-bg, #2a2d2e)}.erd-table .react-flow__handle.erd-hidden-handle{width:1px;height:1px;min-width:0;min-height:0;border:none;background:transparent;opacity:0;pointer-events:none}.erd-flow{width:100%;height:100%;min-height:0;position:relative}.erd-flow .react-flow{width:100%;height:100%}.react-flow{direction:ltr;--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: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--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: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--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: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--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 rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.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: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--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: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--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: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--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 rgba(0, 0, 0, .08);--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{position:absolute;width:100%;height:100%;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{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.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;-moz-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:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.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:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;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%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;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{position:absolute;z-index:5;margin:15px}.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{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.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{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.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{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;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))}.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{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.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-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.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))}.dbd-graph{display:flex;flex-direction:column;height:72vh;border:1px solid var(--border, #e3e6ea);border-radius:8px;overflow:hidden;background:var(--bg-soft, #f5f6f8)}.dbd-canvas{flex:1;min-height:0}.dbd-graph-empty{display:flex;align-items:center;justify-content:center;height:100%;padding:24px}.dbd-graph-empty p{max-width:32rem;text-align:center;color:var(--text-soft, #5a6473)}.dbd-erd-bar{display:flex;justify-content:flex-end;padding:6px 8px;border-bottom:1px solid var(--border, #e3e6ea);background:var(--bg-elev, #fff)}.dbd-lock-btn{padding:4px 12px;border:1px solid var(--border, #e3e6ea);border-radius:6px;background:var(--bg-soft, #f5f6f8);color:var(--text-soft, #5b636c);font-size:12px;cursor:pointer}.dbd-lock-btn:hover{border-color:var(--accent, #2f6feb)}.dbd-lock-btn.on{color:var(--accent, #2f6feb);border-color:var(--accent, #2f6feb)}.react-flow__edge{cursor:pointer}.react-flow__edge:hover .react-flow__edge-path{stroke-width:2.5}.react-flow__attribution{display:none}.react-flow__controls{box-shadow:0 1px 4px #00000040;border-radius:6px;overflow:hidden}.react-flow__controls-button{background:var(--bg-elev, #fff);border-bottom:1px solid var(--border, #e3e6ea);color:var(--text, #1c2024);fill:var(--text, #1c2024);width:26px;height:26px}.react-flow__controls-button:hover{background:var(--bg-soft, #f5f6f8)}.react-flow__controls-button svg{fill:currentColor;max-width:14px;max-height:14px}.react-flow__minimap{background:var(--bg-elev, #fff);border:1px solid var(--border, #e3e6ea);border-radius:6px}.react-flow__minimap-mask{fill:#7f7f7f2e}.react-flow__background{color:var(--border, #e3e6ea)}.dbd-toolbar{display:flex;flex-wrap:wrap;gap:10px;padding:8px 10px;border-bottom:1px solid var(--border, #e3e6ea);background:var(--bg-elev, #fff)}.dbd-toolbar input,.dbd-toolbar select{padding:5px 9px;border:1px solid var(--border, #e3e6ea);border-radius:6px;background:var(--bg-soft, #f5f6f8);color:var(--text, #1c2024);font-size:13px}.dbd-layer-seg{display:flex;border:1px solid var(--border, #e3e6ea);border-radius:6px;overflow:hidden;background:var(--bg-soft, #f5f6f8)}.dbd-layer-seg button{padding:5px 10px;border:none;border-right:1px solid var(--border, #e3e6ea);background:transparent;color:var(--text-soft, #5b636c);font-size:13px;cursor:pointer;white-space:nowrap}.dbd-layer-seg button:last-child{border-right:none}.dbd-layer-seg button:hover{background:var(--bg-soft, #f0f1f3);color:var(--text, #1c2024)}.dbd-layer-seg button.active{background:var(--accent, #2f6feb);color:#fff}.dbd-multi{position:relative}.dbd-multi-trigger{display:flex;align-items:center;gap:4px;padding:5px 9px;border:1px solid var(--border, #e3e6ea);border-radius:6px;background:var(--bg-soft, #f5f6f8);color:var(--text, #1c2024);font-size:13px;cursor:pointer;white-space:nowrap}.dbd-multi-trigger:hover,.dbd-multi-trigger[aria-expanded=true]{border-color:var(--accent, #2f6feb)}.dbd-multi-arrow{font-size:10px;color:var(--text-soft, #5b636c)}.dbd-multi-panel{position:absolute;top:calc(100% + 4px);left:0;z-index:100;min-width:180px;padding:4px 0;border:1px solid var(--border, #e3e6ea);border-radius:6px;background:var(--bg-elev, #fff);box-shadow:0 4px 12px #0000001f}.dbd-multi-option{display:flex;align-items:center;gap:7px;padding:5px 12px;cursor:pointer;font-size:13px;color:var(--text, #1c2024);-webkit-user-select:none;user-select:none}.dbd-multi-option:hover{background:var(--bg-soft, #f5f6f8)}.dbd-multi-all{border-bottom:1px solid var(--border, #e3e6ea);margin-bottom:2px;font-weight:600}.dbd-multi-label{flex:1}.dbd-multi-count{color:var(--text-soft, #5b636c);font-size:11px}.dbd-multi-group{padding:4px 0 2px}.dbd-multi-group:not(:first-child){border-top:1px solid var(--border, #e3e6ea)}.dbd-multi-group-legend{padding:4px 12px 2px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-soft, #5b636c)}.dbd-node{display:flex;align-items:center;gap:7px;min-width:150px;padding:7px 12px;border:1px solid var(--border, #e3e6ea);border-radius:6px;background:var(--bg-elev, #fff);color:var(--text, #1c2024);font-size:12px}.dbd-node-bar{width:4px;height:18px;border-radius:2px}.dbd-node-label{font-weight:600;flex:1}.dbd-node-badge{padding:1px 5px;border-radius:3px;font-size:9px;font-weight:700;color:#fff;text-transform:uppercase;letter-spacing:.04em;flex-shrink:0}.dbd-dim{opacity:.25}.dbd-graph .erd-table{min-width:210px;border-radius:8px;overflow:hidden;font-family:inherit;box-shadow:none}.dbd-graph .erd-table[data-resource=source]{border-style:solid}.dbd-graph .erd-table[data-focus=true]{box-shadow:0 0 0 2px var(--accent, #2f6feb)}.dbd-graph .erd-table-header{gap:8px;border-bottom:none;border-top:3px solid var(--border, #888)}.dbd-graph .erd-table[data-resource=model] .erd-table-header{border-top-color:var(--model, #2f6feb)}.dbd-graph .erd-table[data-resource=source] .erd-table-header{border-top-color:var(--source, #16a34a)}.dbd-graph .erd-table[data-resource=seed] .erd-table-header{border-top-color:var(--seed, #b45309)}.dbd-graph .erd-table[data-resource=snapshot] .erd-table-header{border-top-color:var(--snapshot, #7c3aed)}.dbd-graph .erd-table-icon{display:none}.dbd-graph .erd-table-name{font-weight:700}.dbd-graph .erd-column{grid-template-columns:auto 1fr auto;gap:8px;border-top:1px solid var(--border, #e3e6ea)}.dbd-graph .erd-column-name{font-family:monospace}.dbd-graph .erd-column-type{font-size:11px}.dbd-graph .erd-column-badge{display:inline-flex;justify-content:center;min-width:22px;padding:0 5px;border-radius:4px;color:#fff}.dbd-graph .erd-column-badge.badge-pk{background:var(--seed, #b45309);color:#fff}.dbd-graph .erd-column-badge.badge-fk{background:var(--accent, #2f6feb);color:#fff}.dbd-graph .erd-column-badge.badge-none{background:transparent}.dbd-graph .react-flow__edge:hover{--erd-edge-width: 2.5}