@erd-studio/renderer 0.1.4 → 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;
@@ -278,6 +278,7 @@ function transformDomain(domain, options) {
278
278
  cardinality: rel.cardinality,
279
279
  ...rel.role ? { role: rel.role } : {},
280
280
  ...rel.pairs ? { pairs: rel.pairs, compositeKey: rel.compositeKey } : {},
281
+ ...rel.keysContradict && !readOnly ? { keysContradict: true } : {},
281
282
  stage,
282
283
  ...readOnly ? { readOnly: true } : {},
283
284
  ...discStatus ? { discrepancyStatus: discStatus } : {},
@@ -403,7 +404,7 @@ function applyNodeOverlays(nodes, edges, state) {
403
404
  return node;
404
405
  }
405
406
  const data = node.data;
406
- const searchDimmed = query ? !matchesModelSearch(data, query) : false;
407
+ const searchDimmed = query ? !matchesTableSearch(data, query) : false;
407
408
  const selectionDimmed = hasSelection && !connected.has(node.id);
408
409
  const dimmed = searchDimmed || selectionDimmed;
409
410
  const expanded = isExpanded(node.id);
@@ -479,7 +480,7 @@ function useCanvasGraph({
479
480
  nodeDimensions,
480
481
  isExpanded: isExpandedNow
481
482
  };
482
- let { nodes: newNodes, edges: newEdges } = transformDomain(domain, transformOptions);
483
+ let { nodes: newNodes, edges: newEdges } = transformSubjectArea(domain, transformOptions);
483
484
  newNodes = newNodes.map((n) => {
484
485
  const prev = prevById.get(n.id);
485
486
  return prev?.measured?.width != null && prev.measured.height != null ? { ...n, measured: prev.measured } : n;
@@ -1746,9 +1747,9 @@ function describeLoadError(error) {
1746
1747
  }
1747
1748
  function describeLoadWarning(warning) {
1748
1749
  const key = warning.key ? `"${warning.key}"` : "A key";
1749
- return `${key} is written twice (line ${warning.line}) \u2014 the last one is shown. Remove the other copy to edit this model.`;
1750
+ return `${key} is written twice (line ${warning.line}) \u2014 the last one is shown. Remove the other copy to edit this table.`;
1750
1751
  }
1751
- function ModelNodeComponent({ data, selected }) {
1752
+ function TableNodeComponent({ data, selected }) {
1752
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;
1753
1754
  const openNodeContextMenu = useEditorStore((s) => s.openNodeContextMenu);
1754
1755
  const send = useSend();
@@ -1801,7 +1802,7 @@ function ModelNodeComponent({ data, selected }) {
1801
1802
  }, [isExpanded, columnDiscrepancyMap, discrepancy]);
1802
1803
  const isDiscExtra = discrepancy?.status === "extra";
1803
1804
  const stageClass = isGhost ? "ghost" : stage;
1804
- const title = label ? `${label} (model ${modelName})` : modelName;
1805
+ const title = label ? `${label} (table ${modelName})` : modelName;
1805
1806
  const modelMeta = useMemo2(() => metaRows(meta), [meta]);
1806
1807
  const nameTip = useHoverTip(
1807
1808
  ghostReason ? `${title} \u2014 ${GHOST_REASON_TITLE[ghostReason]}` : isDiscExtra ? `${title} \u2014 only in ${discrepancySourceStage ?? "this stage"}, not in ${discrepancyTargetStage ?? "the stage being compared"}` : title,
@@ -1974,7 +1975,7 @@ function ModelNodeComponent({ data, selected }) {
1974
1975
  }
1975
1976
  );
1976
1977
  }
1977
- var ModelNode = memo(ModelNodeComponent);
1978
+ var TableNode = memo(TableNodeComponent);
1978
1979
 
1979
1980
  // src/components/Graph/FkEdge.tsx
1980
1981
  import { memo as memo2, useCallback as useCallback9, useMemo as useMemo3, useState as useState6 } from "react";
@@ -1986,6 +1987,7 @@ import {
1986
1987
  useStoreApi,
1987
1988
  useInternalNode
1988
1989
  } from "@xyflow/react";
1990
+ import { backwardsRelationshipNote } from "@erd-studio/core";
1989
1991
 
1990
1992
  // src/lib/cardinalityUtils.ts
1991
1993
  function swapCardinality(c) {
@@ -2160,7 +2162,9 @@ function FkEdgeComponent({
2160
2162
  return positions;
2161
2163
  }, [positionsKey, neighbourIds, storeApi]);
2162
2164
  const host = useCanvasHost();
2165
+ const viewer = useIsViewer();
2163
2166
  const [hovered, setHovered] = useState6(false);
2167
+ const [backwardsHovered, setBackwardsHovered] = useState6(false);
2164
2168
  const handleSwap = useCallback9(
2165
2169
  (e) => {
2166
2170
  if (!data) return;
@@ -2340,7 +2344,39 @@ function FkEdgeComponent({
2340
2344
  children: "!"
2341
2345
  }
2342
2346
  ),
2343
- !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(
2344
2380
  "span",
2345
2381
  {
2346
2382
  className: `fk-edge__swap-zone${dimmed ? " fk-edge__swap-zone--dimmed" : ""}`,
@@ -2572,7 +2608,7 @@ function AnnotationNodeInner({ data }) {
2572
2608
  {
2573
2609
  className: "annotation-node__link-handle nodrag",
2574
2610
  onMouseDown: handleLinkDragStart,
2575
- title: "Drag to a model to link",
2611
+ title: "Drag to a table to link",
2576
2612
  children: "\u{1F517}"
2577
2613
  }
2578
2614
  ),
@@ -2687,7 +2723,7 @@ function AnnotationEdgeInner({
2687
2723
  var AnnotationEdge = memo4(AnnotationEdgeInner);
2688
2724
 
2689
2725
  // src/canvas/flowTypes.ts
2690
- var canvasNodeTypes = { model: ModelNode, annotation: AnnotationNode };
2726
+ var canvasNodeTypes = { model: TableNode, annotation: AnnotationNode };
2691
2727
  var canvasEdgeTypes = { fk: FkEdge, annotationLink: AnnotationEdge };
2692
2728
 
2693
2729
  // src/lib/dragHandleSides.ts
@@ -2868,7 +2904,7 @@ function hasRationaleContent(rationale) {
2868
2904
  rationale.measures
2869
2905
  ].some(Boolean);
2870
2906
  }
2871
- function ModelRationale({ modelName, rationale, modelRole, grain }) {
2907
+ function TableRationale({ modelName, rationale, modelRole, grain }) {
2872
2908
  const viewer = useIsViewer();
2873
2909
  const [open, setOpen] = useState9(false);
2874
2910
  const [forceShow, setForceShow] = useState9(false);
@@ -2931,7 +2967,7 @@ function ModelRationale({ modelName, rationale, modelRole, grain }) {
2931
2967
  rationale,
2932
2968
  fieldKey: "purpose",
2933
2969
  label: "Purpose",
2934
- placeholder: "What requirements does this model meet?",
2970
+ placeholder: "What requirements does this table meet?",
2935
2971
  autoEdit: !viewer && forceShow && !rationale?.purpose
2936
2972
  }
2937
2973
  ),
@@ -2942,7 +2978,7 @@ function ModelRationale({ modelName, rationale, modelRole, grain }) {
2942
2978
  rationale,
2943
2979
  fieldKey: "design",
2944
2980
  label: "Design",
2945
- placeholder: "Why was this model designed this way?"
2981
+ placeholder: "Why was this table designed this way?"
2946
2982
  }
2947
2983
  ),
2948
2984
  modelRole && /* @__PURE__ */ jsx12(
@@ -2952,7 +2988,7 @@ function ModelRationale({ modelName, rationale, modelRole, grain }) {
2952
2988
  rationale,
2953
2989
  fieldKey: "roleChoice",
2954
2990
  label: "Role Choice",
2955
- placeholder: "Why was this model role selected?"
2991
+ placeholder: "Why was this table role selected?"
2956
2992
  }
2957
2993
  ),
2958
2994
  grain && /* @__PURE__ */ jsx12(
@@ -3033,7 +3069,7 @@ function DescriptionEditor({ modelName, description }) {
3033
3069
  {
3034
3070
  className: "detail-panel__rationale-add-btn",
3035
3071
  onClick: () => setEditing(true),
3036
- title: "Add model description",
3072
+ title: "Add table description",
3037
3073
  children: "+ Add Description"
3038
3074
  }
3039
3075
  ) });
@@ -3048,7 +3084,7 @@ function DescriptionEditor({ modelName, description }) {
3048
3084
  value,
3049
3085
  onChange: (e) => setValue(e.target.value),
3050
3086
  onKeyDown: handleKeyDown,
3051
- placeholder: "Describe this model...",
3087
+ placeholder: "Describe this table...",
3052
3088
  rows: 3,
3053
3089
  autoFocus: true
3054
3090
  }
@@ -3181,7 +3217,7 @@ function GrainEditor({ modelName, grain }) {
3181
3217
 
3182
3218
  // src/components/DetailPanel/AliasEditor.tsx
3183
3219
  import { useCallback as useCallback14, useEffect as useEffect10, useRef as useRef8, useState as useState12 } from "react";
3184
- 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";
3185
3221
  import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
3186
3222
  function AliasEditor({ modelName, alias, readOnly }) {
3187
3223
  const host = useCanvasHost();
@@ -3194,7 +3230,7 @@ function AliasEditor({ modelName, alias, readOnly }) {
3194
3230
  }
3195
3231
  }, [alias, editing]);
3196
3232
  const trimmed = value.trim();
3197
- const error = trimmed && !isValidModelAlias(trimmed) ? MODEL_ALIAS_RULE : null;
3233
+ const error = trimmed && !isValidTableAlias(trimmed) ? TABLE_ALIAS_RULE : null;
3198
3234
  const handleSave = useCallback14(() => {
3199
3235
  if (error || settled.current) return;
3200
3236
  settled.current = true;
@@ -3227,7 +3263,7 @@ function AliasEditor({ modelName, alias, readOnly }) {
3227
3263
  }, []);
3228
3264
  if (editing) {
3229
3265
  return /* @__PURE__ */ jsxs12("div", { className: "detail-panel__metadata-row detail-panel__metadata-row--alias", children: [
3230
- /* @__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" }),
3231
3267
  /* @__PURE__ */ jsxs12("span", { className: "detail-panel__alias-edit", children: [
3232
3268
  /* @__PURE__ */ jsx15(
3233
3269
  "input",
@@ -3235,26 +3271,26 @@ function AliasEditor({ modelName, alias, readOnly }) {
3235
3271
  className: `detail-panel__alias-input${error ? " detail-panel__alias-input--error" : ""}`,
3236
3272
  value,
3237
3273
  placeholder: modelName,
3238
- maxLength: MODEL_ALIAS_MAX_LENGTH,
3274
+ maxLength: TABLE_ALIAS_MAX_LENGTH,
3239
3275
  onChange: (e) => setValue(e.target.value),
3240
3276
  onKeyDown: handleKeyDown,
3241
3277
  onBlur: () => error ? handleCancel() : handleSave(),
3242
- "aria-label": "Table name (alias)",
3278
+ "aria-label": "Warehouse name (alias)",
3243
3279
  "aria-invalid": error ? true : void 0,
3244
3280
  autoFocus: true
3245
3281
  }
3246
3282
  ),
3247
- /* @__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." })
3248
3284
  ] })
3249
3285
  ] });
3250
3286
  }
3251
3287
  return /* @__PURE__ */ jsxs12("div", { className: "detail-panel__metadata-row detail-panel__metadata-row--alias", children: [
3252
- /* @__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" }),
3253
3289
  /* @__PURE__ */ jsx15(
3254
3290
  "span",
3255
3291
  {
3256
3292
  className: `detail-panel__value${alias ? "" : " detail-panel__value--muted"}`,
3257
- 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",
3258
3294
  onDoubleClick: readOnly ? void 0 : startEditing,
3259
3295
  children: alias || modelName
3260
3296
  }
@@ -3264,8 +3300,8 @@ function AliasEditor({ modelName, alias, readOnly }) {
3264
3300
  {
3265
3301
  className: "detail-panel__rationale-edit-btn detail-panel__alias-edit-btn",
3266
3302
  onClick: startEditing,
3267
- title: "Set the table name (alias)",
3268
- "aria-label": "Edit table name",
3303
+ title: "Set the warehouse name (alias)",
3304
+ "aria-label": "Edit warehouse name",
3269
3305
  children: "\u270E"
3270
3306
  }
3271
3307
  )
@@ -3415,7 +3451,7 @@ function KeyBadgeGroup({
3415
3451
  "span",
3416
3452
  {
3417
3453
  className: "key-badge-group__warning",
3418
- title: "Multiple primary keys detected in this model",
3454
+ title: "Multiple primary keys detected in this table",
3419
3455
  children: "!"
3420
3456
  }
3421
3457
  ) : null;
@@ -4308,7 +4344,7 @@ function MetaEditor({ modelName, columnName, meta, readOnly, compact }) {
4308
4344
  "span",
4309
4345
  {
4310
4346
  className: `meta-editor__text${text ? "" : " meta-editor__text--structured"}`,
4311
- 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),
4312
4348
  onDoubleClick: editable && text ? () => startEdit(key, value) : void 0,
4313
4349
  children: formatMetaValue(value)
4314
4350
  }
@@ -4655,6 +4691,7 @@ function ColumnEditor({ modelName, columns, readOnly, modelRole }) {
4655
4691
  }
4656
4692
 
4657
4693
  // src/components/DetailPanel/DetailPanel.tsx
4694
+ import { backwardsRelationshipNote as backwardsRelationshipNote2 } from "@erd-studio/core";
4658
4695
  import { Fragment as Fragment4, jsx as jsx22, jsxs as jsxs19 } from "react/jsx-runtime";
4659
4696
  function columnsText(rel, end) {
4660
4697
  const cols = columnPairs(rel).map((p) => end === "from" ? p.fromColumn : p.toColumn);
@@ -4723,10 +4760,10 @@ function DetailPanel() {
4723
4760
  return "Only the upper/lower case differs \u2014 rename to another name first";
4724
4761
  }
4725
4762
  if (domain && trimmed !== selectedNode && domain.models.some((m) => same(m.name))) {
4726
- return "A model with this name already exists";
4763
+ return "A table with this name already exists";
4727
4764
  }
4728
4765
  if (trimmed !== selectedNode && existingModels.some((m) => m.source === "logical" && same(m.name))) {
4729
- 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";
4730
4767
  }
4731
4768
  return "";
4732
4769
  },
@@ -4795,6 +4832,21 @@ function DetailPanel() {
4795
4832
  },
4796
4833
  [host]
4797
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
+ );
4798
4850
  const handleRelationshipClick = useCallback22(
4799
4851
  (x, y, rel) => {
4800
4852
  const edgeData = {
@@ -4877,8 +4929,8 @@ function DetailPanel() {
4877
4929
  {
4878
4930
  className: "detail-panel__title-edit-btn",
4879
4931
  onClick: handleStartRename,
4880
- title: "Rename model",
4881
- "aria-label": "Rename model",
4932
+ title: "Rename table",
4933
+ "aria-label": "Rename table",
4882
4934
  children: "\u270E"
4883
4935
  }
4884
4936
  ),
@@ -4914,7 +4966,7 @@ function DetailPanel() {
4914
4966
  !isReadOnly && /* @__PURE__ */ jsx22("div", { className: "detail-panel__section", children: /* @__PURE__ */ jsx22(GrainEditor, { modelName: model.name, grain: model.grain }) }),
4915
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 }) }),
4916
4968
  (!viewer || hasRationaleContent(model.rationale)) && /* @__PURE__ */ jsx22("div", { className: "detail-panel__section", children: /* @__PURE__ */ jsx22(
4917
- ModelRationale,
4969
+ TableRationale,
4918
4970
  {
4919
4971
  modelName: model.name,
4920
4972
  rationale: model.rationale,
@@ -4924,7 +4976,7 @@ function DetailPanel() {
4924
4976
  ) }),
4925
4977
  !isReadOnly && /* @__PURE__ */ jsx22("div", { className: "detail-panel__section detail-panel__section--actions", children: confirmingDelete ? /* @__PURE__ */ jsxs19(Fragment4, { children: [
4926
4978
  /* @__PURE__ */ jsxs19("span", { className: "detail-panel__confirm-label", children: [
4927
- "Remove model from domain?",
4979
+ "Remove table from subject area?",
4928
4980
  totalRelationships > 0 ? ` (${totalRelationships} relationship(s) will also be removed)` : ""
4929
4981
  ] }),
4930
4982
  /* @__PURE__ */ jsxs19("div", { className: "detail-panel__confirm-actions", children: [
@@ -4933,7 +4985,7 @@ function DetailPanel() {
4933
4985
  {
4934
4986
  className: "detail-panel__button detail-panel__button--danger",
4935
4987
  onClick: handleDeleteModel,
4936
- "aria-label": "Confirm remove model",
4988
+ "aria-label": "Confirm remove table",
4937
4989
  children: "Yes, Remove"
4938
4990
  }
4939
4991
  ),
@@ -4952,9 +5004,9 @@ function DetailPanel() {
4952
5004
  {
4953
5005
  className: "detail-panel__button detail-panel__button--danger",
4954
5006
  onClick: () => setConfirmingDelete(true),
4955
- title: "Remove this model from the domain",
4956
- "aria-label": "Remove this model from the domain",
4957
- 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"
4958
5010
  }
4959
5011
  ) }),
4960
5012
  /* @__PURE__ */ jsx22("div", { className: "detail-panel__section", children: /* @__PURE__ */ jsx22(
@@ -5000,6 +5052,18 @@ function DetailPanel() {
5000
5052
  ] })
5001
5053
  ] }),
5002
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
+ ),
5003
5067
  !isReadOnly && /* @__PURE__ */ jsx22(
5004
5068
  "button",
5005
5069
  {
@@ -5044,6 +5108,18 @@ function DetailPanel() {
5044
5108
  /* @__PURE__ */ jsx22("span", { className: "detail-panel__rel-local", title: columnsText(rel, "to"), children: columnsText(rel, "to") })
5045
5109
  ] }),
5046
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
+ ),
5047
5123
  !isReadOnly && /* @__PURE__ */ jsx22(
5048
5124
  "button",
5049
5125
  {
@@ -5155,7 +5231,7 @@ function Legend() {
5155
5231
  ] }),
5156
5232
  /* @__PURE__ */ jsxs20("div", { className: "legend__item", children: [
5157
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" }) }),
5158
- /* @__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" })
5159
5235
  ] })
5160
5236
  ] })
5161
5237
  ] }),
@@ -5293,10 +5369,10 @@ function Legend() {
5293
5369
 
5294
5370
  // src/ErdCanvas.tsx
5295
5371
  import { jsx as jsx24, jsxs as jsxs21 } from "react/jsx-runtime";
5296
- function toRenderableDomain(domain) {
5372
+ function toRenderableSubjectArea(domain) {
5297
5373
  let modelsChanged = false;
5298
5374
  const models = (domain.models ?? []).map((model) => {
5299
- const renderable = toRenderableModel(model);
5375
+ const renderable = toRenderableTable(model);
5300
5376
  if (renderable !== model) modelsChanged = true;
5301
5377
  return renderable;
5302
5378
  });
@@ -5312,7 +5388,7 @@ function toRenderableDomain(domain) {
5312
5388
  readOnly: true
5313
5389
  };
5314
5390
  }
5315
- function toRenderableModel(model) {
5391
+ function toRenderableTable(model) {
5316
5392
  if (model.columns == null) return { ...model, columns: [] };
5317
5393
  if (model.columns.every((col) => col.dataType != null)) return model;
5318
5394
  return { ...model, columns: model.columns.map(toRenderableColumn) };
@@ -5501,7 +5577,7 @@ function ErdCanvasInner({
5501
5577
  );
5502
5578
  }
5503
5579
  var ErdCanvas = forwardRef(function ErdCanvas2(props, ref) {
5504
- const renderable = useMemo7(() => toRenderableDomain(props.domain), [props.domain]);
5580
+ const renderable = useMemo7(() => toRenderableSubjectArea(props.domain), [props.domain]);
5505
5581
  const [store] = useState20(() => createCanvasStore({ domain: renderable }));
5506
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 }) }) }) });
5507
5583
  });
@@ -5526,10 +5602,10 @@ export {
5526
5602
  relationshipDisplayKey,
5527
5603
  relationshipEdgeId,
5528
5604
  foldComposites,
5529
- computeModelLabels,
5530
- matchesModelSearch,
5605
+ computeTableLabels,
5606
+ matchesTableSearch,
5531
5607
  pickHandleSides,
5532
- transformDomain,
5608
+ transformSubjectArea,
5533
5609
  applyNodeOverlays,
5534
5610
  useCanvasGraph,
5535
5611
  STAGE_HEX,
@@ -5551,7 +5627,7 @@ export {
5551
5627
  ADDITIVE_TITLE,
5552
5628
  DISCREPANCY_PHRASE,
5553
5629
  edgeHoverText,
5554
- ModelNode,
5630
+ TableNode,
5555
5631
  swapCardinality,
5556
5632
  FkEdge,
5557
5633
  ANNOTATION_COLORS,
package/dist/editor.js CHANGED
@@ -18,7 +18,6 @@ import {
18
18
  KeyBadge,
19
19
  KeyBadgeGroup,
20
20
  Legend,
21
- ModelNode,
22
21
  NOOP_CANVAS_HOST,
23
22
  SCD_BADGE,
24
23
  SCD_TITLE,
@@ -27,17 +26,18 @@ import {
27
26
  SOURCE_ORDER,
28
27
  SOURCE_PHRASE,
29
28
  STAGE_HEX,
29
+ TableNode,
30
30
  applyNodeOverlays,
31
31
  canvasEdgeTypes,
32
32
  canvasNodeTypes,
33
33
  columnPairs,
34
- computeModelLabels,
34
+ computeTableLabels,
35
35
  diagramExportFileName,
36
36
  edgeHoverText,
37
37
  exportDiagram,
38
38
  foldComposites,
39
39
  getDataTypeColor,
40
- matchesModelSearch,
40
+ matchesTableSearch,
41
41
  nodeRect,
42
42
  pickHandleSides,
43
43
  relationshipDisplayKey,
@@ -47,7 +47,7 @@ import {
47
47
  swapCardinality,
48
48
  toDbml,
49
49
  toMermaid,
50
- transformDomain,
50
+ transformSubjectArea,
51
51
  useCanvasGraph,
52
52
  useCanvasHost,
53
53
  useColumnReorder,
@@ -55,7 +55,7 @@ import {
55
55
  useIsViewer,
56
56
  useLongPressDrag,
57
57
  useSend
58
- } from "./chunk-IAFK7ZZV.js";
58
+ } from "./chunk-HWA32XG5.js";
59
59
  import "./chunk-QZGU5NZH.js";
60
60
  import "./chunk-6XQXDZ4R.js";
61
61
  import {
@@ -97,7 +97,6 @@ export {
97
97
  KeyBadge,
98
98
  KeyBadgeGroup,
99
99
  Legend,
100
- ModelNode,
101
100
  NODE_THRESHOLD,
102
101
  NODE_WIDTH,
103
102
  NOOP_CANVAS_HOST,
@@ -108,11 +107,12 @@ export {
108
107
  SOURCE_ORDER,
109
108
  SOURCE_PHRASE,
110
109
  STAGE_HEX,
110
+ TableNode,
111
111
  applyNodeOverlays,
112
112
  canvasEdgeTypes,
113
113
  canvasNodeTypes,
114
114
  columnPairs,
115
- computeModelLabels,
115
+ computeTableLabels,
116
116
  countVisibleColumnRows,
117
117
  createCanvasSlice,
118
118
  createCanvasStore,
@@ -123,7 +123,7 @@ export {
123
123
  exportDiagram,
124
124
  foldComposites,
125
125
  getDataTypeColor,
126
- matchesModelSearch,
126
+ matchesTableSearch,
127
127
  nodeRect,
128
128
  pickHandleSides,
129
129
  relationshipDisplayKey,
@@ -134,7 +134,7 @@ export {
134
134
  swapCardinality,
135
135
  toDbml,
136
136
  toMermaid,
137
- transformDomain,
137
+ transformSubjectArea,
138
138
  useCanvasGraph,
139
139
  useCanvasHost,
140
140
  useColumnExpansion,