@coldtea/pr-lens-renderer 0.3.5 → 0.4.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.
Files changed (62) hide show
  1. package/dist/atlas.d.ts +2 -0
  2. package/dist/atlas.d.ts.map +1 -1
  3. package/dist/atlas.js +1 -1
  4. package/dist/atlas.js.map +1 -1
  5. package/dist/corrections.d.ts.map +1 -1
  6. package/dist/corrections.js +7 -1
  7. package/dist/corrections.js.map +1 -1
  8. package/dist/design.d.ts +25 -0
  9. package/dist/design.d.ts.map +1 -1
  10. package/dist/design.js +25 -0
  11. package/dist/design.js.map +1 -1
  12. package/dist/layout/architecture.d.ts +6 -2
  13. package/dist/layout/architecture.d.ts.map +1 -1
  14. package/dist/layout/architecture.js +12 -19
  15. package/dist/layout/architecture.js.map +1 -1
  16. package/dist/layout/datamodel.d.ts +70 -0
  17. package/dist/layout/datamodel.d.ts.map +1 -0
  18. package/dist/layout/datamodel.js +381 -0
  19. package/dist/layout/datamodel.js.map +1 -0
  20. package/dist/layout/edges.d.ts +8 -0
  21. package/dist/layout/edges.d.ts.map +1 -1
  22. package/dist/layout/edges.js +2 -2
  23. package/dist/layout/edges.js.map +1 -1
  24. package/dist/render.d.ts.map +1 -1
  25. package/dist/render.js +18 -1
  26. package/dist/render.js.map +1 -1
  27. package/dist/scope.d.ts +4 -1
  28. package/dist/scope.d.ts.map +1 -1
  29. package/dist/scope.js +28 -2
  30. package/dist/scope.js.map +1 -1
  31. package/dist/svg/architecture.d.ts +5 -2
  32. package/dist/svg/architecture.d.ts.map +1 -1
  33. package/dist/svg/architecture.js +10 -11
  34. package/dist/svg/architecture.js.map +1 -1
  35. package/dist/svg/datamodel.d.ts +11 -0
  36. package/dist/svg/datamodel.d.ts.map +1 -0
  37. package/dist/svg/datamodel.js +252 -0
  38. package/dist/svg/datamodel.js.map +1 -0
  39. package/dist/svg/icons.d.ts +2 -0
  40. package/dist/svg/icons.d.ts.map +1 -1
  41. package/dist/svg/icons.js +14 -0
  42. package/dist/svg/icons.js.map +1 -1
  43. package/dist/svg/styles.d.ts +3 -1
  44. package/dist/svg/styles.d.ts.map +1 -1
  45. package/dist/svg/styles.js +6 -0
  46. package/dist/svg/styles.js.map +1 -1
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/package.json +3 -3
  50. package/src/atlas.ts +3 -1
  51. package/src/corrections.ts +7 -1
  52. package/src/design.ts +26 -0
  53. package/src/layout/architecture.ts +16 -10
  54. package/src/layout/datamodel.ts +524 -0
  55. package/src/layout/edges.ts +2 -2
  56. package/src/render.ts +18 -1
  57. package/src/scope.ts +46 -3
  58. package/src/svg/architecture.ts +12 -13
  59. package/src/svg/datamodel.ts +344 -0
  60. package/src/svg/icons.ts +20 -0
  61. package/src/svg/styles.ts +8 -1
  62. package/src/version.ts +1 -1
@@ -1,5 +1,5 @@
1
1
  import { assertNever } from "@coldtea/pr-lens-schema";
2
- import type { GraphEdge, GraphNode, LayoutHints } from "@coldtea/pr-lens-schema";
2
+ import type { Delta, GraphEdge, LayoutHints } from "@coldtea/pr-lens-schema";
3
3
  import { truncate } from "../text.js";
4
4
  import { glyphGroup } from "./icons.js";
5
5
  import type { Palette } from "../theme.js";
@@ -23,11 +23,11 @@ import {
23
23
  import {
24
24
  badgeRow,
25
25
  badgeWidth,
26
- cardBadges,
27
26
  cardTextWidth,
28
27
  deltaBadgeText,
29
28
  laneHeaderText,
30
29
  occupiedBoxes,
30
+ type BadgeRow,
31
31
  type PlacedNode,
32
32
  } from "../layout/architecture.js";
33
33
  import {
@@ -47,23 +47,22 @@ import {
47
47
  SUBTITLE_SIZE,
48
48
  } from "../design.js";
49
49
 
50
- const badgeTone = (node: GraphNode, text: string): Tone =>
51
- text === deltaBadgeText(node.delta) ? toneFor(node.delta) : "neutral";
50
+ const badgeTone = (delta: Delta, text: string): Tone =>
51
+ text === deltaBadgeText(delta) ? toneFor(delta) : "neutral";
52
52
 
53
53
  /** The badge row, laid left to right across the strip the layout reserved. */
54
- const paintBadges = (placed: PlacedNode, palette: Palette): string => {
54
+ export const paintBadgeStrip = (row: BadgeRow | undefined, delta: Delta, palette: Palette): string => {
55
55
  const styles = stylesFor(palette);
56
- const row = badgeRow(placed);
57
56
  if (row === undefined) return "";
58
57
 
59
58
  let x = row.box.x;
60
59
  return lines(
61
60
  row.badges.map((text) => {
62
61
  const width = badgeWidth(text);
63
- const colours = badgeColours(badgeTone(placed.node, text), palette);
62
+ const colours = badgeColours(badgeTone(delta, text), palette);
64
63
  const painted = wrap(
65
64
  "g",
66
- { class: `bdg bdg-${badgeTone(placed.node, text)}` },
65
+ { class: `bdg bdg-${badgeTone(delta, text)}` },
67
66
  tag("rect", {
68
67
  x: coord(x),
69
68
  y: coord(row.box.y),
@@ -91,8 +90,8 @@ const paintBadges = (placed: PlacedNode, palette: Palette): string => {
91
90
  );
92
91
  };
93
92
 
94
- const cardOutlineClass = (node: GraphNode): string => {
95
- switch (node.delta) {
93
+ export const cardOutlineClass = (delta: Delta): string => {
94
+ switch (delta) {
96
95
  case "added":
97
96
  return "card card-added";
98
97
  case "modified":
@@ -101,7 +100,7 @@ const cardOutlineClass = (node: GraphNode): string => {
101
100
  case "unchanged":
102
101
  return "card";
103
102
  default:
104
- return assertNever(node.delta, "Unhandled delta");
103
+ return assertNever(delta, "Unhandled delta");
105
104
  }
106
105
  };
107
106
 
@@ -159,7 +158,7 @@ export const paintCard = (placed: PlacedNode, palette: Palette): string => {
159
158
  { class: groupClass, ...cardGroupAttributes(node.delta, palette) },
160
159
  lines([
161
160
  tag("rect", {
162
- class: cardOutlineClass(node),
161
+ class: cardOutlineClass(node.delta),
163
162
  x: coord(box.x),
164
163
  y: coord(box.y),
165
164
  width: coord(box.width),
@@ -170,7 +169,7 @@ export const paintCard = (placed: PlacedNode, palette: Palette): string => {
170
169
  chip,
171
170
  title,
172
171
  subtitle,
173
- paintBadges(placed, palette),
172
+ paintBadgeStrip(badgeRow(placed), node.delta, palette),
174
173
  ]),
175
174
  );
176
175
  };
@@ -0,0 +1,344 @@
1
+ import { assertNever } from "@coldtea/pr-lens-schema";
2
+ import type { ColumnKey, Lane } from "@coldtea/pr-lens-schema";
3
+ import { measure, truncate } from "../text.js";
4
+ import type { Palette } from "../theme.js";
5
+ import type { ScopedGraph } from "../scope.js";
6
+ import { canvasFor, union } from "../bounds.js";
7
+ import { coord, type Box } from "../geometry.js";
8
+ import { curveBounds } from "../layout/edges.js";
9
+ import { lines, tag, textNode, wrap } from "./primitives.js";
10
+ import { tableGlyph } from "./icons.js";
11
+ import { atlasBoxes, emptyAtlas, type RenderAtlas } from "../atlas.js";
12
+ import { markerFor, shifted, toneColour, toneFor } from "./document.js";
13
+ import { cardAttributes, cardGroupAttributes, relationAttributes, stylesFor } from "./styles.js";
14
+ import { cardOutlineClass, paintBadgeStrip, paintLabelPill } from "./architecture.js";
15
+ import { badgeStrip, badgesFor, cardTextWidth, laneHeaderText } from "../layout/architecture.js";
16
+ import {
17
+ layoutDataModel,
18
+ routeRelations,
19
+ type PlacedTable,
20
+ type RoutedRelation,
21
+ } from "../layout/datamodel.js";
22
+ import {
23
+ CARD_HEIGHT_WITH_SUBTITLE,
24
+ CARD_PADDING_X,
25
+ CARD_RADIUS,
26
+ DIAGRAM_MARGIN,
27
+ ICON_CHIP_GAP,
28
+ ICON_CHIP_RADIUS,
29
+ ICON_CHIP_SIZE,
30
+ KEY_CHIP_GAP,
31
+ KEY_CHIP_HEIGHT,
32
+ KEY_CHIP_WIDTH,
33
+ LANE_HEADER_BASELINE,
34
+ LANE_PADDING_X,
35
+ LANE_RADIUS,
36
+ LANE_TOP,
37
+ SUBTITLE_SIZE,
38
+ TABLE_ROW_TEXT_SIZE,
39
+ TABLE_ROW_TYPE_SIZE,
40
+ } from "../design.js";
41
+
42
+ const keyChipText = (key: ColumnKey): string => {
43
+ switch (key) {
44
+ case "primary":
45
+ return "PK";
46
+ case "foreign":
47
+ return "FK";
48
+ case "index":
49
+ return "IX";
50
+ default:
51
+ return assertNever(key, "Unhandled column key");
52
+ }
53
+ };
54
+
55
+ /** The subtitle under a table's name: its physical name, and how many columns it has. */
56
+ const tableSubtitle = (placed: PlacedTable): string => {
57
+ const count = `${placed.table.columns.length} ${placed.table.columns.length === 1 ? "column" : "columns"}`;
58
+ return placed.table.name === undefined ? count : `${placed.table.name} · ${count}`;
59
+ };
60
+
61
+ const paintColumns = (placed: PlacedTable, palette: Palette): string => {
62
+ const styles = stylesFor(palette);
63
+ const { box } = placed;
64
+ const left = box.x + CARD_PADDING_X;
65
+ const nameX = left + KEY_CHIP_WIDTH + KEY_CHIP_GAP;
66
+ const right = box.x + box.width - CARD_PADDING_X;
67
+
68
+ return lines(
69
+ placed.columns.map(({ column, centre }) => {
70
+ const tone = toneFor(column.delta);
71
+ const ink = (() => {
72
+ switch (column.delta) {
73
+ case "added":
74
+ return palette.addedText;
75
+ case "removed":
76
+ return palette.removedText;
77
+ case "modified":
78
+ case "unchanged":
79
+ return palette.foreground;
80
+ default:
81
+ return assertNever(column.delta, "Unhandled delta");
82
+ }
83
+ })();
84
+
85
+ const chip =
86
+ column.key === undefined
87
+ ? ""
88
+ : tag("rect", {
89
+ class: "chip",
90
+ x: coord(left),
91
+ y: coord(centre - KEY_CHIP_HEIGHT / 2),
92
+ width: KEY_CHIP_WIDTH,
93
+ height: KEY_CHIP_HEIGHT,
94
+ rx: KEY_CHIP_HEIGHT / 2,
95
+ ...styles.chip,
96
+ }) +
97
+ textNode(
98
+ {
99
+ class: "keytext",
100
+ x: coord(left + KEY_CHIP_WIDTH / 2),
101
+ y: coord(centre + 3),
102
+ "text-anchor": "middle",
103
+ "font-size": 8,
104
+ "font-weight": 700,
105
+ "letter-spacing": ".06em",
106
+ fill: palette.muted,
107
+ },
108
+ keyChipText(column.key),
109
+ );
110
+
111
+ // A changed type says what it was: the old spelling struck, then the new one.
112
+ // Non-breaking spaces, because a tspan swallows the ordinary kind at its edges.
113
+ const typeText = column.previousType === undefined ? column.type : `${column.previousType}\u00a0→\u00a0${column.type}`;
114
+ const typeWidth = measure(typeText, "mono", TABLE_ROW_TYPE_SIZE);
115
+ const typeFill = tone === "modified" ? palette.modifiedText : tone === "added" ? palette.addedText : palette.muted;
116
+ const type =
117
+ column.previousType === undefined
118
+ ? textNode(
119
+ { class: "coltype", x: coord(right), y: coord(centre + 3.5), "text-anchor": "end", "font-size": TABLE_ROW_TYPE_SIZE, "font-family": styles.subtitle["font-family"], fill: typeFill },
120
+ typeText,
121
+ )
122
+ : wrap(
123
+ "text",
124
+ { class: "coltype", x: coord(right), y: coord(centre + 3.5), "text-anchor": "end", "font-size": TABLE_ROW_TYPE_SIZE, "font-family": styles.subtitle["font-family"], fill: typeFill },
125
+ wrap("tspan", { fill: palette.muted, "text-decoration": "line-through" }, column.previousType) +
126
+ wrap("tspan", {}, `\u00a0→\u00a0${column.type}`),
127
+ );
128
+
129
+ const nameRoom = right - typeWidth - KEY_CHIP_GAP - nameX;
130
+ const name = textNode(
131
+ {
132
+ class: column.delta === "removed" ? "colname strike" : "colname",
133
+ x: coord(nameX),
134
+ y: coord(centre + 4),
135
+ "font-size": TABLE_ROW_TEXT_SIZE,
136
+ "font-family": styles.subtitle["font-family"],
137
+ fill: ink,
138
+ "text-decoration": column.delta === "removed" ? "line-through" : undefined,
139
+ },
140
+ truncate(column.name, "mono", TABLE_ROW_TEXT_SIZE, nameRoom),
141
+ );
142
+
143
+ return lines([chip, name, type]);
144
+ }),
145
+ );
146
+ };
147
+
148
+ /** The closing row of a folded card: a dashed rule, then how many columns stayed out of the picture. */
149
+ const paintFold = (placed: PlacedTable, palette: Palette): string => {
150
+ const { fold, box } = placed;
151
+ if (fold === undefined) return "";
152
+ const styles = stylesFor(palette);
153
+ const label = `+ ${fold.hidden} ${fold.unchangedOnly ? "unchanged columns" : "more columns"}`;
154
+ return lines([
155
+ tag("path", {
156
+ class: "rule",
157
+ d: `M${coord(box.x + CARD_PADDING_X)},${coord(fold.top)} h${coord(box.width - CARD_PADDING_X * 2)}`,
158
+ stroke: palette.cardBorder,
159
+ "stroke-width": 1,
160
+ "stroke-dasharray": "3 3",
161
+ }),
162
+ textNode(
163
+ {
164
+ class: "fold",
165
+ x: coord(box.x + CARD_PADDING_X + KEY_CHIP_WIDTH + KEY_CHIP_GAP),
166
+ y: coord(fold.centre + 4),
167
+ "font-size": TABLE_ROW_TEXT_SIZE,
168
+ "font-family": styles.subtitle["font-family"],
169
+ fill: palette.muted,
170
+ },
171
+ label,
172
+ ),
173
+ ]);
174
+ };
175
+
176
+ const paintTable = (placed: PlacedTable, palette: Palette): string => {
177
+ const styles = stylesFor(palette);
178
+ const { table, box, titleSize } = placed;
179
+ const textX = box.x + CARD_PADDING_X + ICON_CHIP_SIZE + ICON_CHIP_GAP;
180
+ const textWidth = cardTextWidth(box.width, true);
181
+ const headerBottom = box.y + CARD_HEIGHT_WITH_SUBTITLE;
182
+
183
+ const groupClass =
184
+ table.delta === "removed" ? "cardsh ghost" : table.delta === "unchanged" ? "cardsh context" : "cardsh";
185
+
186
+ return wrap(
187
+ "g",
188
+ { class: groupClass, ...cardGroupAttributes(table.delta, palette) },
189
+ lines([
190
+ tag("rect", {
191
+ class: cardOutlineClass(table.delta),
192
+ x: coord(box.x),
193
+ y: coord(box.y),
194
+ width: coord(box.width),
195
+ height: coord(box.height),
196
+ rx: CARD_RADIUS,
197
+ ...cardAttributes(table.delta, palette),
198
+ }),
199
+ tag("rect", {
200
+ class: "chip",
201
+ x: coord(box.x + CARD_PADDING_X),
202
+ y: coord(box.y + (CARD_HEIGHT_WITH_SUBTITLE - ICON_CHIP_SIZE) / 2),
203
+ width: ICON_CHIP_SIZE,
204
+ height: ICON_CHIP_SIZE,
205
+ rx: ICON_CHIP_RADIUS,
206
+ ...styles.chip,
207
+ }),
208
+ tableGlyph(box.x + CARD_PADDING_X + ICON_CHIP_SIZE / 2, box.y + CARD_HEIGHT_WITH_SUBTITLE / 2, styles),
209
+ textNode(
210
+ {
211
+ class: table.delta === "removed" ? "ntitle strike" : "ntitle",
212
+ x: coord(textX),
213
+ y: coord(box.y + 27),
214
+ "font-size": titleSize,
215
+ ...styles.title,
216
+ "text-decoration": table.delta === "removed" ? "line-through" : undefined,
217
+ },
218
+ truncate(table.label, "sans-bold", titleSize, textWidth),
219
+ ),
220
+ textNode(
221
+ { class: "nsub", x: coord(textX), y: coord(box.y + 45), ...styles.subtitle },
222
+ truncate(tableSubtitle(placed), "mono", SUBTITLE_SIZE, textWidth),
223
+ ),
224
+ tag("path", {
225
+ class: "rule",
226
+ d: `M${coord(box.x)},${coord(headerBottom)} h${coord(box.width)}`,
227
+ stroke: palette.cardBorder,
228
+ "stroke-width": 1,
229
+ }),
230
+ paintColumns(placed, palette),
231
+ paintFold(placed, palette),
232
+ paintBadgeStrip(badgeStrip(box, badgesFor(table.badges, table.delta)), table.delta, palette),
233
+ ]),
234
+ );
235
+ };
236
+
237
+ const paintRelation = (routed: RoutedRelation, palette: Palette): { markup: string; pill: string } => {
238
+ const { relation, path, curve } = routed;
239
+ const tone = toneFor(relation.delta);
240
+ const classes = ["edge", `edge-${tone}`];
241
+ if (relation.delta === "unchanged") classes.push("context");
242
+
243
+ return {
244
+ markup: lines([
245
+ tag("path", {
246
+ class: classes.join(" "),
247
+ d: path,
248
+ "marker-end": routed.head ? markerFor(tone) : undefined,
249
+ ...relationAttributes(relation, palette),
250
+ }),
251
+ tag("circle", {
252
+ class: "port",
253
+ cx: coord(curve.from.x),
254
+ cy: coord(curve.from.y),
255
+ r: 2.5,
256
+ fill: toneColour(palette, tone),
257
+ opacity: relation.delta === "removed" ? 0.7 : undefined,
258
+ }),
259
+ ]),
260
+ pill:
261
+ routed.pill === undefined || relation.label === undefined
262
+ ? ""
263
+ : paintLabelPill(relation.label, routed.pill, tone, palette),
264
+ };
265
+ };
266
+
267
+ export type DataModelPainting = {
268
+ width: number;
269
+ height: number;
270
+ body: string;
271
+ atlas: RenderAtlas;
272
+ };
273
+
274
+ export const paintDataModel = (graph: ScopedGraph, palette: Palette): DataModelPainting => {
275
+ const layout = layoutDataModel(graph);
276
+ const routed = routeRelations(layout, graph.relations);
277
+ const styles = stylesFor(palette);
278
+
279
+ const drawn: Box[] = layout.tables.flatMap(({ table, box }) => {
280
+ const strip = badgeStrip(box, badgesFor(table.badges, table.delta));
281
+ return strip === undefined ? [box] : [box, strip.box];
282
+ });
283
+
284
+ const edgeMarkup: string[] = [];
285
+ const pillMarkup: string[] = [];
286
+ for (const relation of routed) {
287
+ const { markup, pill } = paintRelation(relation, palette);
288
+ edgeMarkup.push(markup);
289
+ pillMarkup.push(pill);
290
+ if (relation.pill !== undefined) drawn.push(relation.pill);
291
+ }
292
+
293
+ const headerBaseline = layout.laneTop + (LANE_HEADER_BASELINE - LANE_TOP);
294
+ const lanes = layout.stores.map(({ store, box }) => {
295
+ const header: Lane = { id: store.id, label: store.label, subtitle: store.platform };
296
+ return lines([
297
+ tag("rect", {
298
+ class: "lanebox",
299
+ x: coord(box.x),
300
+ y: coord(box.y),
301
+ width: coord(box.width),
302
+ height: coord(box.height),
303
+ rx: LANE_RADIUS,
304
+ ...styles.lane,
305
+ }),
306
+ textNode(
307
+ { class: "lanelabel", x: coord(box.x + LANE_PADDING_X), y: coord(headerBaseline), ...styles.laneLabel },
308
+ laneHeaderText(header),
309
+ ),
310
+ ]);
311
+ });
312
+
313
+ const canvas = canvasFor(
314
+ layout,
315
+ union([
316
+ ...layout.stores.map(({ box }) => box),
317
+ ...drawn,
318
+ ...routed.map(({ curve }) => curveBounds(curve)),
319
+ ]),
320
+ DIAGRAM_MARGIN,
321
+ );
322
+
323
+ const painted = lines([
324
+ wrap("g", {}, lines(lanes)),
325
+ wrap("g", {}, lines(edgeMarkup)),
326
+ wrap("g", {}, lines(layout.tables.map((table) => paintTable(table, palette)))),
327
+ wrap("g", {}, lines(pillMarkup)),
328
+ ]);
329
+
330
+ return {
331
+ width: canvas.width,
332
+ height: canvas.height,
333
+ body: shifted(canvas, painted),
334
+ atlas: {
335
+ ...emptyAtlas(),
336
+ lanes: atlasBoxes(layout.stores.map(({ store, box }) => ({ id: store.id, box })), canvas),
337
+ tables: atlasBoxes(layout.tables.map(({ table, box }) => ({ id: table.id, box })), canvas),
338
+ edges: atlasBoxes(
339
+ routed.map(({ relation, curve }) => ({ id: relation.id, box: curveBounds(curve) })),
340
+ canvas,
341
+ ),
342
+ },
343
+ };
344
+ };
package/src/svg/icons.ts CHANGED
@@ -128,5 +128,25 @@ const kindGlyph = (kind: NodeKind, cx: number, cy: number, styles: SvgStyles): s
128
128
  }
129
129
  };
130
130
 
131
+ /** A table: a box with a header rule and a column divider. */
132
+ export const tableGlyph = (cx: number, cy: number, styles: SvgStyles): string =>
133
+ wrap(
134
+ "g",
135
+ {},
136
+ lines([
137
+ tag("rect", {
138
+ class: "glyph-stroke",
139
+ x: coord(cx - 7),
140
+ y: coord(cy - 6),
141
+ width: 14,
142
+ height: 12,
143
+ rx: 2,
144
+ ...styles.glyphStroke,
145
+ }),
146
+ tag("path", { class: "glyph-stroke", d: `M${coord(cx - 7)},${coord(cy - 2)} h14`, ...styles.glyphStroke }),
147
+ tag("path", { class: "glyph-stroke", d: `M${coord(cx - 2)},${coord(cy - 2)} v8`, ...styles.glyphStroke }),
148
+ ]),
149
+ );
150
+
131
151
  export const glyphGroup = (kind: NodeKind, cx: number, cy: number, styles: SvgStyles): string =>
132
152
  wrap("g", {}, kindGlyph(kind, cx, cy, styles));
package/src/svg/styles.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { assertNever } from "@coldtea/pr-lens-schema";
2
- import type { Delta, FlowMessage, GraphEdge } from "@coldtea/pr-lens-schema";
2
+ import type { Delta, FlowMessage, GraphEdge, Relation } from "@coldtea/pr-lens-schema";
3
3
  import { MONO_STACK } from "../text.js";
4
4
  import type { Palette } from "../theme.js";
5
5
  import type { Attributes } from "./primitives.js";
@@ -104,6 +104,13 @@ export const edgeAttributes = (edge: GraphEdge, palette: Palette): Attributes =>
104
104
  }
105
105
  };
106
106
 
107
+ /** A key between tables: an edge, dotted when nothing enforces it. */
108
+ export const relationAttributes = (relation: Relation, palette: Palette): Attributes => ({
109
+ ...strokeAttributes(relation.delta, palette),
110
+ ...(relation.delta === "unchanged" ? { opacity: 0.82 } : {}),
111
+ ...(relation.enforced ? {} : { "stroke-dasharray": "2 4" }),
112
+ });
113
+
107
114
  export const messageAttributes = (message: FlowMessage, palette: Palette): Attributes => {
108
115
  const attributes = {
109
116
  ...strokeAttributes(message.delta, palette),
package/src/version.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  */
7
7
  export const RENDERER_NAME = "@coldtea/pr-lens-renderer";
8
8
 
9
- export const RENDERER_VERSION = "0.3.5";
9
+ export const RENDERER_VERSION = "0.4.0";