@erd-studio/renderer 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@erd-studio/renderer`. While `0.x`, a breaking change bumps the minor version.
4
+
5
+ ## 0.3.0
6
+
7
+ ### Changed
8
+ - Pins `@erd-studio/core` 0.3.0, which reads schema v6, the table naming ([#181](https://github.com/liam-machine/erd-studio/issues/181)). No change to the renderer's own API: a v6 subject area reaches `ErdCanvas` as the same `DisplaySubjectArea` its v5 twin does.
9
+
10
+ ## 0.2.0
11
+
12
+ ### Changed (breaking)
13
+ - **Exports use the new words: table and subject area** ([#180](https://github.com/liam-machine/erd-studio/issues/180), part 2 of [#162](https://github.com/liam-machine/erd-studio/issues/162)). What was called a *model* is a **table**, and a *domain* is a **subject area**. Pins `@erd-studio/core` 0.2.0. The types re-exported from core carry its new names. Only names changed: every function behaves as before, and object shapes, property names (`DisplaySubjectArea.models`, `domainPath`, `ErdCanvas`'s `domain` prop, …), message `type` strings and the files read on disk are the same, so a `DisplaySubjectArea` from 0.2.0 is the `DisplayDomain` of 0.1.x. There are no aliases for the old names. Rename the imports:
14
+
15
+ | 0.1.x | 0.2.0 |
16
+ |---|---|
17
+ | `computeModelLabels` | `computeTableLabels` |
18
+ | `DisplayDomain` | `DisplaySubjectArea` |
19
+ | `DisplayModel` | `DisplayTable` |
20
+ | `LabelledModel` | `LabelledTable` |
21
+ | `matchesModelSearch` | `matchesTableSearch` |
22
+ | `ModelFlowNode` | `TableFlowNode` |
23
+ | `ModelNode` | `TableNode` |
24
+ | `ModelNodeData` | `TableNodeData` |
25
+ | `ModelRole` | `TableRole` |
26
+ | `transformDomain` | `transformSubjectArea` |
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @erd-studio/renderer
2
2
 
3
- The entity-relationship diagram canvas from the [ERD Studio](https://github.com/liam-machine/erd-studio) VS Code extension, packaged for reuse. It is built on React Flow and renders a `DisplayDomain` from `@erd-studio/core`: model nodes with their columns and key badges, FK edges with cardinality, annotations, the detail panel and the legend.
3
+ The entity-relationship diagram canvas from the [ERD Studio](https://github.com/liam-machine/erd-studio) VS Code extension, packaged for reuse. It is built on React Flow and renders a `DisplaySubjectArea` from `@erd-studio/core`: model nodes with their columns and key badges, FK edges with cardinality, annotations, the detail panel and the legend.
4
4
 
5
5
  Peer dependencies: `react` and `react-dom` 18.3+, `@xyflow/react` 12.4+.
6
6
 
@@ -8,8 +8,8 @@ Peer dependencies: `react` and `react-dom` 18.3+, `@xyflow/react` 12.4+.
8
8
 
9
9
  | Import | What it is for |
10
10
  |---|---|
11
- | `@erd-studio/renderer` | Showing a domain: the `ErdCanvas` viewer component (below), plus `transformDomain` (a `DisplayDomain` to React Flow nodes and edges), `pickHandleSides`, `repickHandleSides` / `nodeRect`, the diagram exporters re-exported from `@erd-studio/core` (below), and the display, domain and graph types (`DisplayDomain`, `ModelFlowNode`, `FkFlowEdge`, …). |
12
- | `@erd-studio/renderer/editor` | Everything above plus what a host needs to build a full editor around the canvas, as the extension's webview does: the store (below), the host adapter, `useCanvasGraph`, `CanvasBackdrop`, `canvasNodeTypes` / `canvasEdgeTypes`, the components (`ModelNode`, `FkEdge`, `AnnotationNode`, `AnnotationEdge`, `DetailPanel`, `Legend`, `KeyBadge`, `KeyBadgeGroup`, `DataTypeSelect`, `ColumnRowEditor`), the canvas hooks and the pure helpers. |
11
+ | `@erd-studio/renderer` | Showing a domain: the `ErdCanvas` viewer component (below), plus `transformSubjectArea` (a `DisplaySubjectArea` to React Flow nodes and edges), `pickHandleSides`, `repickHandleSides` / `nodeRect`, the diagram exporters re-exported from `@erd-studio/core` (below), and the display, domain and graph types (`DisplaySubjectArea`, `TableFlowNode`, `FkFlowEdge`, …). |
12
+ | `@erd-studio/renderer/editor` | Everything above plus what a host needs to build a full editor around the canvas, as the extension's webview does: the store (below), the host adapter, `useCanvasGraph`, `CanvasBackdrop`, `canvasNodeTypes` / `canvasEdgeTypes`, the components (`TableNode`, `FkEdge`, `AnnotationNode`, `AnnotationEdge`, `DetailPanel`, `Legend`, `KeyBadge`, `KeyBadgeGroup`, `DataTypeSelect`, `ColumnRowEditor`), the canvas hooks and the pure helpers. |
13
13
  | `@erd-studio/renderer/store` | The canvas store on its own, with no React Flow runtime: `createCanvasSlice`, `createCanvasStore`, `CanvasStoreProvider`, `useEditorStore`, `useEditorStoreApi` and their types. |
14
14
  | `@erd-studio/renderer/sizing` | Model node size estimation (`NODE_WIDTH`, `estimateNodeWidth`, `estimateNodeHeight`, `countVisibleColumnRows`, `resolveNodeDimensions`), for layout code that needs node sizes before React Flow has measured them. No components. |
15
15
  | `@erd-studio/renderer/styles.css` | The stylesheet: every component's CSS in cascade order, then the theme tokens. It `@import`s `@xyflow/react/dist/style.css`, which your bundler resolves. |
@@ -18,14 +18,14 @@ All four JavaScript entry points share their modules, so a store or context crea
18
18
 
19
19
  ## Viewer: `ErdCanvas`
20
20
 
21
- `ErdCanvas` renders a `DisplayDomain` read-only, the way the extension shows a read-only domain: model nodes with their columns and key badges, FK edges, annotations, the minimap, the legend, and the detail panel a click on a model opens. It presents no editing affordances and posts nothing anywhere.
21
+ `ErdCanvas` renders a `DisplaySubjectArea` read-only, the way the extension shows a read-only domain: model nodes with their columns and key badges, FK edges, annotations, the minimap, the legend, and the detail panel a click on a model opens. It presents no editing affordances and posts nothing anywhere.
22
22
 
23
23
  ```tsx
24
24
  import { useRef, useState } from 'react';
25
- import { ErdCanvas, type ErdCanvasHandle, type DisplayDomain } from '@erd-studio/renderer';
25
+ import { ErdCanvas, type ErdCanvasHandle, type DisplaySubjectArea } from '@erd-studio/renderer';
26
26
  import '@erd-studio/renderer/styles.css';
27
27
 
28
- function Diagram({ domain }: { domain: DisplayDomain }) {
28
+ function Diagram({ domain }: { domain: DisplaySubjectArea }) {
29
29
  const canvas = useRef<ErdCanvasHandle>(null);
30
30
  const [moved, setMoved] = useState(false);
31
31
  return (
@@ -51,12 +51,12 @@ Each `ErdCanvas` has its own store, so several can share a page.
51
51
 
52
52
  ## Exporting a diagram
53
53
 
54
- The `.` entry re-exports `@erd-studio/core`'s one-way exporters, so a page that shows a diagram with `ErdCanvas` can offer it as text without depending on core itself: `toMermaid`, `toDbml`, `exportDiagram`, `diagramExportFileName`, `DIAGRAM_EXPORT_FORMATS`, `DIAGRAM_EXPORT_FILE_EXTENSIONS` and the `DiagramExportFormat` type. They are pure functions with no React or DOM use; the same `DisplayDomain` gives the same text everywhere ERD Studio exports it.
54
+ The `.` entry re-exports `@erd-studio/core`'s one-way exporters, so a page that shows a diagram with `ErdCanvas` can offer it as text without depending on core itself: `toMermaid`, `toDbml`, `exportDiagram`, `diagramExportFileName`, `DIAGRAM_EXPORT_FORMATS`, `DIAGRAM_EXPORT_FILE_EXTENSIONS` and the `DiagramExportFormat` type. They are pure functions with no React or DOM use; the same `DisplaySubjectArea` gives the same text everywhere ERD Studio exports it.
55
55
 
56
56
  ```ts
57
- import { exportDiagram, diagramExportFileName, type DisplayDomain } from '@erd-studio/renderer';
57
+ import { exportDiagram, diagramExportFileName, type DisplaySubjectArea } from '@erd-studio/renderer';
58
58
 
59
- function downloadDbml(domain: DisplayDomain) {
59
+ function downloadDbml(domain: DisplaySubjectArea) {
60
60
  const text = exportDiagram(domain, 'dbml'); // or 'mermaid'
61
61
  const url = URL.createObjectURL(new Blob([text], { type: 'text/plain' }));
62
62
  const link = Object.assign(document.createElement('a'), { href: url, download: diagramExportFileName(domain, 'dbml') });
@@ -102,7 +102,7 @@ function foldComposites(rels) {
102
102
  }
103
103
 
104
104
  // src/lib/modelLabels.ts
105
- function computeModelLabels(models) {
105
+ function computeTableLabels(models) {
106
106
  const baseOf = (m) => m.alias?.trim() || m.name;
107
107
  const counts = /* @__PURE__ */ new Map();
108
108
  for (const m of models) {
@@ -120,7 +120,7 @@ function computeModelLabels(models) {
120
120
  }
121
121
  return labels;
122
122
  }
123
- function matchesModelSearch(data, query) {
123
+ function matchesTableSearch(data, query) {
124
124
  return data.modelName.toLowerCase().includes(query) || data.label !== void 0 && data.label.toLowerCase().includes(query);
125
125
  }
126
126
 
@@ -153,7 +153,7 @@ function pickHandleSides(source, target) {
153
153
  return dy >= 0 ? { sourceSide: "bottom", targetSide: "top" } : { sourceSide: "top", targetSide: "bottom" };
154
154
  }
155
155
  }
156
- function transformDomain(domain, options) {
156
+ function transformSubjectArea(domain, options) {
157
157
  const { models, relationships, viewConfig, layer, stage, layerConfig, readOnly } = domain;
158
158
  const positions = viewConfig.positions ?? {};
159
159
  const discrepancyMap = /* @__PURE__ */ new Map();
@@ -176,7 +176,7 @@ function transformDomain(domain, options) {
176
176
  data: { ...data, isExpanded: options?.isExpanded?.(id) ?? false },
177
177
  measured: void 0
178
178
  });
179
- const labels = computeModelLabels(models);
179
+ const labels = computeTableLabels(models);
180
180
  const nodes = models.map((model) => {
181
181
  const columns = mapColumns(model);
182
182
  const position = positionMap.get(model.name) ?? DEFAULT_POSITION;
@@ -207,6 +207,7 @@ function transformDomain(domain, options) {
207
207
  // untouched. Never derive it inside an overlay pass.
208
208
  ...model.provenance ? { provenance: model.provenance } : {},
209
209
  ...model.loadError ? { loadError: model.loadError } : {},
210
+ ...model.loadWarning ? { loadWarning: model.loadWarning } : {},
210
211
  ...disc ? { discrepancy: disc } : {},
211
212
  ...disc && options?.discrepancyReport ? {
212
213
  discrepancySourceStage: options.discrepancyReport.sourceStage,
@@ -277,6 +278,7 @@ function transformDomain(domain, options) {
277
278
  cardinality: rel.cardinality,
278
279
  ...rel.role ? { role: rel.role } : {},
279
280
  ...rel.pairs ? { pairs: rel.pairs, compositeKey: rel.compositeKey } : {},
281
+ ...rel.keysContradict && !readOnly ? { keysContradict: true } : {},
280
282
  stage,
281
283
  ...readOnly ? { readOnly: true } : {},
282
284
  ...discStatus ? { discrepancyStatus: discStatus } : {},
@@ -402,7 +404,7 @@ function applyNodeOverlays(nodes, edges, state) {
402
404
  return node;
403
405
  }
404
406
  const data = node.data;
405
- const searchDimmed = query ? !matchesModelSearch(data, query) : false;
407
+ const searchDimmed = query ? !matchesTableSearch(data, query) : false;
406
408
  const selectionDimmed = hasSelection && !connected.has(node.id);
407
409
  const dimmed = searchDimmed || selectionDimmed;
408
410
  const expanded = isExpanded(node.id);
@@ -478,7 +480,7 @@ function useCanvasGraph({
478
480
  nodeDimensions,
479
481
  isExpanded: isExpandedNow
480
482
  };
481
- let { nodes: newNodes, edges: newEdges } = transformDomain(domain, transformOptions);
483
+ let { nodes: newNodes, edges: newEdges } = transformSubjectArea(domain, transformOptions);
482
484
  newNodes = newNodes.map((n) => {
483
485
  const prev = prevById.get(n.id);
484
486
  return prev?.measured?.width != null && prev.measured.height != null ? { ...n, measured: prev.measured } : n;
@@ -1743,8 +1745,12 @@ function describeLoadError(error) {
1743
1745
  }
1744
1746
  return error.line !== void 0 ? `YAML error on line ${error.line} \u2014 ERD Studio can't read this file.` : "YAML error \u2014 ERD Studio can't read this file.";
1745
1747
  }
1746
- function ModelNodeComponent({ data, selected }) {
1747
- const { modelName, label, stage, layer, layerConfig, schema, columns, grain, meta, dimmed, readOnly, isGhost, ghostReason, provenance, loadError, isStub, isExpanded = false, onToggleExpansion, discrepancy, discrepancySourceStage, discrepancyTargetStage } = data;
1748
+ function describeLoadWarning(warning) {
1749
+ const key = warning.key ? `"${warning.key}"` : "A key";
1750
+ return `${key} is written twice (line ${warning.line}) \u2014 the last one is shown. Remove the other copy to edit this table.`;
1751
+ }
1752
+ function TableNodeComponent({ data, selected }) {
1753
+ const { modelName, label, stage, layer, layerConfig, schema, columns, grain, meta, dimmed, readOnly, isGhost, ghostReason, provenance, loadError, loadWarning, isStub, isExpanded = false, onToggleExpansion, discrepancy, discrepancySourceStage, discrepancyTargetStage } = data;
1748
1754
  const openNodeContextMenu = useEditorStore((s) => s.openNodeContextMenu);
1749
1755
  const send = useSend();
1750
1756
  const viewer = useIsViewer();
@@ -1796,7 +1802,7 @@ function ModelNodeComponent({ data, selected }) {
1796
1802
  }, [isExpanded, columnDiscrepancyMap, discrepancy]);
1797
1803
  const isDiscExtra = discrepancy?.status === "extra";
1798
1804
  const stageClass = isGhost ? "ghost" : stage;
1799
- const title = label ? `${label} (model ${modelName})` : modelName;
1805
+ const title = label ? `${label} (table ${modelName})` : modelName;
1800
1806
  const modelMeta = useMemo2(() => metaRows(meta), [meta]);
1801
1807
  const nameTip = useHoverTip(
1802
1808
  ghostReason ? `${title} \u2014 ${GHOST_REASON_TITLE[ghostReason]}` : isDiscExtra ? `${title} \u2014 only in ${discrepancySourceStage ?? "this stage"}, not in ${discrepancyTargetStage ?? "the stage being compared"}` : title,
@@ -1853,6 +1859,21 @@ function ModelNodeComponent({ data, selected }) {
1853
1859
  badgeTip.tip
1854
1860
  ] }),
1855
1861
  grain && /* @__PURE__ */ jsx7("div", { className: "model-node__grain", title: grain, children: grain }),
1862
+ loadWarning && /* @__PURE__ */ jsxs5("div", { className: "model-node__load-warning", role: "status", children: [
1863
+ /* @__PURE__ */ jsx7("span", { className: "model-node__load-warning-text", children: describeLoadWarning(loadWarning) }),
1864
+ viewer ? null : /* @__PURE__ */ jsx7(
1865
+ "button",
1866
+ {
1867
+ type: "button",
1868
+ className: "model-node__load-error-open nodrag",
1869
+ onClick: (e) => {
1870
+ e.stopPropagation();
1871
+ send({ type: "openModelFile", payload: { modelName } });
1872
+ },
1873
+ children: "Open file"
1874
+ }
1875
+ )
1876
+ ] }),
1856
1877
  /* @__PURE__ */ jsxs5("div", { className: "model-node__columns", children: [
1857
1878
  displayColumns.map((col, idx) => /* @__PURE__ */ jsx7(
1858
1879
  ColumnRow,
@@ -1954,7 +1975,7 @@ function ModelNodeComponent({ data, selected }) {
1954
1975
  }
1955
1976
  );
1956
1977
  }
1957
- var ModelNode = memo(ModelNodeComponent);
1978
+ var TableNode = memo(TableNodeComponent);
1958
1979
 
1959
1980
  // src/components/Graph/FkEdge.tsx
1960
1981
  import { memo as memo2, useCallback as useCallback9, useMemo as useMemo3, useState as useState6 } from "react";
@@ -1966,6 +1987,7 @@ import {
1966
1987
  useStoreApi,
1967
1988
  useInternalNode
1968
1989
  } from "@xyflow/react";
1990
+ import { backwardsRelationshipNote } from "@erd-studio/core";
1969
1991
 
1970
1992
  // src/lib/cardinalityUtils.ts
1971
1993
  function swapCardinality(c) {
@@ -2140,7 +2162,9 @@ function FkEdgeComponent({
2140
2162
  return positions;
2141
2163
  }, [positionsKey, neighbourIds, storeApi]);
2142
2164
  const host = useCanvasHost();
2165
+ const viewer = useIsViewer();
2143
2166
  const [hovered, setHovered] = useState6(false);
2167
+ const [backwardsHovered, setBackwardsHovered] = useState6(false);
2144
2168
  const handleSwap = useCallback9(
2145
2169
  (e) => {
2146
2170
  if (!data) return;
@@ -2320,7 +2344,39 @@ function FkEdgeComponent({
2320
2344
  children: "!"
2321
2345
  }
2322
2346
  ),
2323
- !readOnly && discrepancyStatus !== "missing" && /* @__PURE__ */ jsx8(
2347
+ viewer ? null : data.keysContradict && !readOnly && discrepancyStatus !== "missing" && /* @__PURE__ */ jsxs6(
2348
+ "span",
2349
+ {
2350
+ className: `fk-edge__backwards${dimmed ? " fk-edge__backwards--dimmed" : ""}`,
2351
+ style: {
2352
+ transform: `translate(-9px, -50%) translate(${midX}px, ${midY}px)`
2353
+ },
2354
+ onMouseEnter: (e) => {
2355
+ e.stopPropagation();
2356
+ setBackwardsHovered(true);
2357
+ },
2358
+ onMouseLeave: (e) => {
2359
+ e.stopPropagation();
2360
+ setBackwardsHovered(false);
2361
+ },
2362
+ children: [
2363
+ /* @__PURE__ */ jsx8(
2364
+ "span",
2365
+ {
2366
+ className: "fk-edge__backwards-icon",
2367
+ role: "img",
2368
+ "aria-label": "Probably drawn backwards",
2369
+ title: backwardsRelationshipNote(data, data.pairs ?? [{ fromColumn, toColumn }]),
2370
+ children: "\u26A0"
2371
+ }
2372
+ ),
2373
+ backwardsHovered && // Turn round is ⇄: a backwards many-to-one becomes the one-to-many it
2374
+ // reads as, which the host stores the right way round (#163).
2375
+ /* @__PURE__ */ jsx8("button", { className: "fk-edge__backwards-btn", onClick: handleSwap, title: "Turn this relationship round", children: "Turn round" })
2376
+ ]
2377
+ }
2378
+ ),
2379
+ !readOnly && discrepancyStatus !== "missing" && !(data.keysContradict && !viewer) && /* @__PURE__ */ jsx8(
2324
2380
  "span",
2325
2381
  {
2326
2382
  className: `fk-edge__swap-zone${dimmed ? " fk-edge__swap-zone--dimmed" : ""}`,
@@ -2552,7 +2608,7 @@ function AnnotationNodeInner({ data }) {
2552
2608
  {
2553
2609
  className: "annotation-node__link-handle nodrag",
2554
2610
  onMouseDown: handleLinkDragStart,
2555
- title: "Drag to a model to link",
2611
+ title: "Drag to a table to link",
2556
2612
  children: "\u{1F517}"
2557
2613
  }
2558
2614
  ),
@@ -2667,7 +2723,7 @@ function AnnotationEdgeInner({
2667
2723
  var AnnotationEdge = memo4(AnnotationEdgeInner);
2668
2724
 
2669
2725
  // src/canvas/flowTypes.ts
2670
- var canvasNodeTypes = { model: ModelNode, annotation: AnnotationNode };
2726
+ var canvasNodeTypes = { model: TableNode, annotation: AnnotationNode };
2671
2727
  var canvasEdgeTypes = { fk: FkEdge, annotationLink: AnnotationEdge };
2672
2728
 
2673
2729
  // src/lib/dragHandleSides.ts
@@ -2848,7 +2904,7 @@ function hasRationaleContent(rationale) {
2848
2904
  rationale.measures
2849
2905
  ].some(Boolean);
2850
2906
  }
2851
- function ModelRationale({ modelName, rationale, modelRole, grain }) {
2907
+ function TableRationale({ modelName, rationale, modelRole, grain }) {
2852
2908
  const viewer = useIsViewer();
2853
2909
  const [open, setOpen] = useState9(false);
2854
2910
  const [forceShow, setForceShow] = useState9(false);
@@ -2911,7 +2967,7 @@ function ModelRationale({ modelName, rationale, modelRole, grain }) {
2911
2967
  rationale,
2912
2968
  fieldKey: "purpose",
2913
2969
  label: "Purpose",
2914
- placeholder: "What requirements does this model meet?",
2970
+ placeholder: "What requirements does this table meet?",
2915
2971
  autoEdit: !viewer && forceShow && !rationale?.purpose
2916
2972
  }
2917
2973
  ),
@@ -2922,7 +2978,7 @@ function ModelRationale({ modelName, rationale, modelRole, grain }) {
2922
2978
  rationale,
2923
2979
  fieldKey: "design",
2924
2980
  label: "Design",
2925
- placeholder: "Why was this model designed this way?"
2981
+ placeholder: "Why was this table designed this way?"
2926
2982
  }
2927
2983
  ),
2928
2984
  modelRole && /* @__PURE__ */ jsx12(
@@ -2932,7 +2988,7 @@ function ModelRationale({ modelName, rationale, modelRole, grain }) {
2932
2988
  rationale,
2933
2989
  fieldKey: "roleChoice",
2934
2990
  label: "Role Choice",
2935
- placeholder: "Why was this model role selected?"
2991
+ placeholder: "Why was this table role selected?"
2936
2992
  }
2937
2993
  ),
2938
2994
  grain && /* @__PURE__ */ jsx12(
@@ -3013,7 +3069,7 @@ function DescriptionEditor({ modelName, description }) {
3013
3069
  {
3014
3070
  className: "detail-panel__rationale-add-btn",
3015
3071
  onClick: () => setEditing(true),
3016
- title: "Add model description",
3072
+ title: "Add table description",
3017
3073
  children: "+ Add Description"
3018
3074
  }
3019
3075
  ) });
@@ -3028,7 +3084,7 @@ function DescriptionEditor({ modelName, description }) {
3028
3084
  value,
3029
3085
  onChange: (e) => setValue(e.target.value),
3030
3086
  onKeyDown: handleKeyDown,
3031
- placeholder: "Describe this model...",
3087
+ placeholder: "Describe this table...",
3032
3088
  rows: 3,
3033
3089
  autoFocus: true
3034
3090
  }
@@ -3161,7 +3217,7 @@ function GrainEditor({ modelName, grain }) {
3161
3217
 
3162
3218
  // src/components/DetailPanel/AliasEditor.tsx
3163
3219
  import { useCallback as useCallback14, useEffect as useEffect10, useRef as useRef8, useState as useState12 } from "react";
3164
- import { MODEL_ALIAS_MAX_LENGTH, MODEL_ALIAS_RULE, isValidModelAlias } from "@erd-studio/core";
3220
+ import { TABLE_ALIAS_MAX_LENGTH, TABLE_ALIAS_RULE, isValidTableAlias } from "@erd-studio/core";
3165
3221
  import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
3166
3222
  function AliasEditor({ modelName, alias, readOnly }) {
3167
3223
  const host = useCanvasHost();
@@ -3174,7 +3230,7 @@ function AliasEditor({ modelName, alias, readOnly }) {
3174
3230
  }
3175
3231
  }, [alias, editing]);
3176
3232
  const trimmed = value.trim();
3177
- const error = trimmed && !isValidModelAlias(trimmed) ? MODEL_ALIAS_RULE : null;
3233
+ const error = trimmed && !isValidTableAlias(trimmed) ? TABLE_ALIAS_RULE : null;
3178
3234
  const handleSave = useCallback14(() => {
3179
3235
  if (error || settled.current) return;
3180
3236
  settled.current = true;
@@ -3207,7 +3263,7 @@ function AliasEditor({ modelName, alias, readOnly }) {
3207
3263
  }, []);
3208
3264
  if (editing) {
3209
3265
  return /* @__PURE__ */ jsxs12("div", { className: "detail-panel__metadata-row detail-panel__metadata-row--alias", children: [
3210
- /* @__PURE__ */ jsx15("span", { className: "detail-panel__label detail-panel__label--wide", children: "Table name" }),
3266
+ /* @__PURE__ */ jsx15("span", { className: "detail-panel__label detail-panel__label--wide", children: "Warehouse name" }),
3211
3267
  /* @__PURE__ */ jsxs12("span", { className: "detail-panel__alias-edit", children: [
3212
3268
  /* @__PURE__ */ jsx15(
3213
3269
  "input",
@@ -3215,26 +3271,26 @@ function AliasEditor({ modelName, alias, readOnly }) {
3215
3271
  className: `detail-panel__alias-input${error ? " detail-panel__alias-input--error" : ""}`,
3216
3272
  value,
3217
3273
  placeholder: modelName,
3218
- maxLength: MODEL_ALIAS_MAX_LENGTH,
3274
+ maxLength: TABLE_ALIAS_MAX_LENGTH,
3219
3275
  onChange: (e) => setValue(e.target.value),
3220
3276
  onKeyDown: handleKeyDown,
3221
3277
  onBlur: () => error ? handleCancel() : handleSave(),
3222
- "aria-label": "Table name (alias)",
3278
+ "aria-label": "Warehouse name (alias)",
3223
3279
  "aria-invalid": error ? true : void 0,
3224
3280
  autoFocus: true
3225
3281
  }
3226
3282
  ),
3227
- /* @__PURE__ */ jsx15("span", { className: `detail-panel__alias-hint${error ? " detail-panel__alias-hint--error" : ""}`, children: error ?? "The table dbt builds (its alias). Leave blank to use the model name." })
3283
+ /* @__PURE__ */ jsx15("span", { className: `detail-panel__alias-hint${error ? " detail-panel__alias-hint--error" : ""}`, children: error ?? "The table dbt builds (its alias). Leave blank to use the table's name." })
3228
3284
  ] })
3229
3285
  ] });
3230
3286
  }
3231
3287
  return /* @__PURE__ */ jsxs12("div", { className: "detail-panel__metadata-row detail-panel__metadata-row--alias", children: [
3232
- /* @__PURE__ */ jsx15("span", { className: "detail-panel__label detail-panel__label--wide", children: "Table name" }),
3288
+ /* @__PURE__ */ jsx15("span", { className: "detail-panel__label detail-panel__label--wide", children: "Warehouse name" }),
3233
3289
  /* @__PURE__ */ jsx15(
3234
3290
  "span",
3235
3291
  {
3236
3292
  className: `detail-panel__value${alias ? "" : " detail-panel__value--muted"}`,
3237
- title: alias ? `Built as ${alias} (dbt alias); the model is ${modelName}` : "Same as the model name",
3293
+ title: alias ? `Built as ${alias} (dbt alias); the table's name is ${modelName}` : "Same as the table's name",
3238
3294
  onDoubleClick: readOnly ? void 0 : startEditing,
3239
3295
  children: alias || modelName
3240
3296
  }
@@ -3244,8 +3300,8 @@ function AliasEditor({ modelName, alias, readOnly }) {
3244
3300
  {
3245
3301
  className: "detail-panel__rationale-edit-btn detail-panel__alias-edit-btn",
3246
3302
  onClick: startEditing,
3247
- title: "Set the table name (alias)",
3248
- "aria-label": "Edit table name",
3303
+ title: "Set the warehouse name (alias)",
3304
+ "aria-label": "Edit warehouse name",
3249
3305
  children: "\u270E"
3250
3306
  }
3251
3307
  )
@@ -3395,7 +3451,7 @@ function KeyBadgeGroup({
3395
3451
  "span",
3396
3452
  {
3397
3453
  className: "key-badge-group__warning",
3398
- title: "Multiple primary keys detected in this model",
3454
+ title: "Multiple primary keys detected in this table",
3399
3455
  children: "!"
3400
3456
  }
3401
3457
  ) : null;
@@ -4288,7 +4344,7 @@ function MetaEditor({ modelName, columnName, meta, readOnly, compact }) {
4288
4344
  "span",
4289
4345
  {
4290
4346
  className: `meta-editor__text${text ? "" : " meta-editor__text--structured"}`,
4291
- title: editable && !text ? "Edit nested values in the model file" : formatMetaValue(value),
4347
+ title: editable && !text ? "Edit nested values in the table's file" : formatMetaValue(value),
4292
4348
  onDoubleClick: editable && text ? () => startEdit(key, value) : void 0,
4293
4349
  children: formatMetaValue(value)
4294
4350
  }
@@ -4635,6 +4691,7 @@ function ColumnEditor({ modelName, columns, readOnly, modelRole }) {
4635
4691
  }
4636
4692
 
4637
4693
  // src/components/DetailPanel/DetailPanel.tsx
4694
+ import { backwardsRelationshipNote as backwardsRelationshipNote2 } from "@erd-studio/core";
4638
4695
  import { Fragment as Fragment4, jsx as jsx22, jsxs as jsxs19 } from "react/jsx-runtime";
4639
4696
  function columnsText(rel, end) {
4640
4697
  const cols = columnPairs(rel).map((p) => end === "from" ? p.fromColumn : p.toColumn);
@@ -4703,10 +4760,10 @@ function DetailPanel() {
4703
4760
  return "Only the upper/lower case differs \u2014 rename to another name first";
4704
4761
  }
4705
4762
  if (domain && trimmed !== selectedNode && domain.models.some((m) => same(m.name))) {
4706
- return "A model with this name already exists";
4763
+ return "A table with this name already exists";
4707
4764
  }
4708
4765
  if (trimmed !== selectedNode && existingModels.some((m) => m.source === "logical" && same(m.name))) {
4709
- return "A model with this name already exists in the model library";
4766
+ return "A table with this name already exists in the Table Library";
4710
4767
  }
4711
4768
  return "";
4712
4769
  },
@@ -4775,6 +4832,21 @@ function DetailPanel() {
4775
4832
  },
4776
4833
  [host]
4777
4834
  );
4835
+ const handleTurnRound = useCallback22(
4836
+ (rel) => {
4837
+ host.postMessage({
4838
+ type: "updateRelationship",
4839
+ payload: {
4840
+ fromModel: rel.fromModel,
4841
+ fromColumn: rel.fromColumn,
4842
+ toModel: rel.toModel,
4843
+ toColumn: rel.toColumn,
4844
+ cardinality: swapCardinality(rel.cardinality)
4845
+ }
4846
+ });
4847
+ },
4848
+ [host]
4849
+ );
4778
4850
  const handleRelationshipClick = useCallback22(
4779
4851
  (x, y, rel) => {
4780
4852
  const edgeData = {
@@ -4857,8 +4929,8 @@ function DetailPanel() {
4857
4929
  {
4858
4930
  className: "detail-panel__title-edit-btn",
4859
4931
  onClick: handleStartRename,
4860
- title: "Rename model",
4861
- "aria-label": "Rename model",
4932
+ title: "Rename table",
4933
+ "aria-label": "Rename table",
4862
4934
  children: "\u270E"
4863
4935
  }
4864
4936
  ),
@@ -4894,7 +4966,7 @@ function DetailPanel() {
4894
4966
  !isReadOnly && /* @__PURE__ */ jsx22("div", { className: "detail-panel__section", children: /* @__PURE__ */ jsx22(GrainEditor, { modelName: model.name, grain: model.grain }) }),
4895
4967
  (!isReadOnly && !viewer || model.meta && Object.keys(model.meta).length > 0) && /* @__PURE__ */ jsx22("div", { className: "detail-panel__section", children: /* @__PURE__ */ jsx22(MetaEditor, { modelName: model.name, meta: model.meta, readOnly: isReadOnly }) }),
4896
4968
  (!viewer || hasRationaleContent(model.rationale)) && /* @__PURE__ */ jsx22("div", { className: "detail-panel__section", children: /* @__PURE__ */ jsx22(
4897
- ModelRationale,
4969
+ TableRationale,
4898
4970
  {
4899
4971
  modelName: model.name,
4900
4972
  rationale: model.rationale,
@@ -4904,7 +4976,7 @@ function DetailPanel() {
4904
4976
  ) }),
4905
4977
  !isReadOnly && /* @__PURE__ */ jsx22("div", { className: "detail-panel__section detail-panel__section--actions", children: confirmingDelete ? /* @__PURE__ */ jsxs19(Fragment4, { children: [
4906
4978
  /* @__PURE__ */ jsxs19("span", { className: "detail-panel__confirm-label", children: [
4907
- "Remove model from domain?",
4979
+ "Remove table from subject area?",
4908
4980
  totalRelationships > 0 ? ` (${totalRelationships} relationship(s) will also be removed)` : ""
4909
4981
  ] }),
4910
4982
  /* @__PURE__ */ jsxs19("div", { className: "detail-panel__confirm-actions", children: [
@@ -4913,7 +4985,7 @@ function DetailPanel() {
4913
4985
  {
4914
4986
  className: "detail-panel__button detail-panel__button--danger",
4915
4987
  onClick: handleDeleteModel,
4916
- "aria-label": "Confirm remove model",
4988
+ "aria-label": "Confirm remove table",
4917
4989
  children: "Yes, Remove"
4918
4990
  }
4919
4991
  ),
@@ -4932,9 +5004,9 @@ function DetailPanel() {
4932
5004
  {
4933
5005
  className: "detail-panel__button detail-panel__button--danger",
4934
5006
  onClick: () => setConfirmingDelete(true),
4935
- title: "Remove this model from the domain",
4936
- "aria-label": "Remove this model from the domain",
4937
- children: "Remove from Domain"
5007
+ title: "Remove this table from the subject area",
5008
+ "aria-label": "Remove this table from the subject area",
5009
+ children: "Remove from Subject Area"
4938
5010
  }
4939
5011
  ) }),
4940
5012
  /* @__PURE__ */ jsx22("div", { className: "detail-panel__section", children: /* @__PURE__ */ jsx22(
@@ -4980,6 +5052,18 @@ function DetailPanel() {
4980
5052
  ] })
4981
5053
  ] }),
4982
5054
  /* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-cardinality", children: rel.cardinality }),
5055
+ viewer ? null : !isReadOnly && rel.keysContradict && /* @__PURE__ */ jsx22(
5056
+ "button",
5057
+ {
5058
+ className: "detail-panel__rel-backwards",
5059
+ onClick: (e) => {
5060
+ e.stopPropagation();
5061
+ handleTurnRound(rel);
5062
+ },
5063
+ title: `${backwardsRelationshipNote2(rel, rel.pairs ?? [rel])} Click to turn it round.`,
5064
+ children: "\u26A0 Turn round"
5065
+ }
5066
+ ),
4983
5067
  !isReadOnly && /* @__PURE__ */ jsx22(
4984
5068
  "button",
4985
5069
  {
@@ -5024,6 +5108,18 @@ function DetailPanel() {
5024
5108
  /* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-local", title: columnsText(rel, "to"), children: columnsText(rel, "to") })
5025
5109
  ] }),
5026
5110
  /* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-cardinality", children: rel.cardinality }),
5111
+ viewer ? null : !isReadOnly && rel.keysContradict && /* @__PURE__ */ jsx22(
5112
+ "button",
5113
+ {
5114
+ className: "detail-panel__rel-backwards",
5115
+ onClick: (e) => {
5116
+ e.stopPropagation();
5117
+ handleTurnRound(rel);
5118
+ },
5119
+ title: `${backwardsRelationshipNote2(rel, rel.pairs ?? [rel])} Click to turn it round.`,
5120
+ children: "\u26A0 Turn round"
5121
+ }
5122
+ ),
5027
5123
  !isReadOnly && /* @__PURE__ */ jsx22(
5028
5124
  "button",
5029
5125
  {
@@ -5135,7 +5231,7 @@ function Legend() {
5135
5231
  ] }),
5136
5232
  /* @__PURE__ */ jsxs20("div", { className: "legend__item", children: [
5137
5233
  /* @__PURE__ */ jsx23("svg", { className: "legend__edge-sample", viewBox: "0 0 48 8", "aria-hidden": "true", children: /* @__PURE__ */ jsx23("line", { x1: "0", y1: "4", x2: "48", y2: "4", className: "legend__edge legend__edge--annotation" }) }),
5138
- /* @__PURE__ */ jsx23("span", { className: "legend__item-desc", children: "Note linked to model" })
5234
+ /* @__PURE__ */ jsx23("span", { className: "legend__item-desc", children: "Note linked to table" })
5139
5235
  ] })
5140
5236
  ] })
5141
5237
  ] }),
@@ -5273,10 +5369,10 @@ function Legend() {
5273
5369
 
5274
5370
  // src/ErdCanvas.tsx
5275
5371
  import { jsx as jsx24, jsxs as jsxs21 } from "react/jsx-runtime";
5276
- function toRenderableDomain(domain) {
5372
+ function toRenderableSubjectArea(domain) {
5277
5373
  let modelsChanged = false;
5278
5374
  const models = (domain.models ?? []).map((model) => {
5279
- const renderable = toRenderableModel(model);
5375
+ const renderable = toRenderableTable(model);
5280
5376
  if (renderable !== model) modelsChanged = true;
5281
5377
  return renderable;
5282
5378
  });
@@ -5292,7 +5388,7 @@ function toRenderableDomain(domain) {
5292
5388
  readOnly: true
5293
5389
  };
5294
5390
  }
5295
- function toRenderableModel(model) {
5391
+ function toRenderableTable(model) {
5296
5392
  if (model.columns == null) return { ...model, columns: [] };
5297
5393
  if (model.columns.every((col) => col.dataType != null)) return model;
5298
5394
  return { ...model, columns: model.columns.map(toRenderableColumn) };
@@ -5481,7 +5577,7 @@ function ErdCanvasInner({
5481
5577
  );
5482
5578
  }
5483
5579
  var ErdCanvas = forwardRef(function ErdCanvas2(props, ref) {
5484
- const renderable = useMemo7(() => toRenderableDomain(props.domain), [props.domain]);
5580
+ const renderable = useMemo7(() => toRenderableSubjectArea(props.domain), [props.domain]);
5485
5581
  const [store] = useState20(() => createCanvasStore({ domain: renderable }));
5486
5582
  return /* @__PURE__ */ jsx24(CanvasStoreProvider, { store, children: /* @__PURE__ */ jsx24(CanvasEnvironmentProvider, { host: NOOP_CANVAS_HOST, viewer: true, children: /* @__PURE__ */ jsx24(ReactFlowProvider, { children: /* @__PURE__ */ jsx24(ErdCanvasInner, { ...props, domain: renderable, handleRef: ref }) }) }) });
5487
5583
  });
@@ -5506,10 +5602,10 @@ export {
5506
5602
  relationshipDisplayKey,
5507
5603
  relationshipEdgeId,
5508
5604
  foldComposites,
5509
- computeModelLabels,
5510
- matchesModelSearch,
5605
+ computeTableLabels,
5606
+ matchesTableSearch,
5511
5607
  pickHandleSides,
5512
- transformDomain,
5608
+ transformSubjectArea,
5513
5609
  applyNodeOverlays,
5514
5610
  useCanvasGraph,
5515
5611
  STAGE_HEX,
@@ -5531,7 +5627,7 @@ export {
5531
5627
  ADDITIVE_TITLE,
5532
5628
  DISCREPANCY_PHRASE,
5533
5629
  edgeHoverText,
5534
- ModelNode,
5630
+ TableNode,
5535
5631
  swapCardinality,
5536
5632
  FkEdge,
5537
5633
  ANNOTATION_COLORS,