@coldtea/pr-lens-renderer 0.2.2 → 0.2.4

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.
@@ -1,5 +1,35 @@
1
- import type { GraphEdge, GraphNode, LayoutHints } from "@coldtea/pr-lens-schema";
2
1
  import { assertNever } from "@coldtea/pr-lens-schema";
2
+ import type { GraphEdge, GraphNode, LayoutHints } from "@coldtea/pr-lens-schema";
3
+ import { truncate } from "../text.js";
4
+ import { glyphGroup } from "./icons.js";
5
+ import type { Palette } from "../theme.js";
6
+ import { DIAGRAM_MARGIN } from "../design.js";
7
+ import { travellingPulses } from "./pulse.js";
8
+ import type { ScopedGraph } from "../scope.js";
9
+ import { canvasFor, union } from "../bounds.js";
10
+ import { coord, type Box } from "../geometry.js";
11
+ import { placeLabelPills } from "../layout/labels.js";
12
+ import { relieveCongestion } from "../layout/congestion.js";
13
+ import { lines, tag, textNode, wrap } from "./primitives.js";
14
+ import { curveBounds, type RoutedEdge } from "../layout/edges.js";
15
+ import { atlasBoxes, emptyAtlas, type RenderAtlas } from "../atlas.js";
16
+ import { markerFor, shifted, toneColour, toneFor, type Tone } from "./document.js";
17
+ import {
18
+ badgeColours,
19
+ cardAttributes,
20
+ cardGroupAttributes,
21
+ edgeAttributes,
22
+ stylesFor,
23
+ } from "./styles.js";
24
+ import {
25
+ badgeRow,
26
+ badgeWidth,
27
+ cardBadges,
28
+ cardTextWidth,
29
+ deltaBadgeText,
30
+ laneHeaderText,
31
+ type PlacedNode,
32
+ } from "../layout/architecture.js";
3
33
  import {
4
34
  BADGE_GAP,
5
35
  BADGE_HEIGHT,
@@ -16,35 +46,13 @@ import {
16
46
  LANE_RADIUS,
17
47
  SUBTITLE_SIZE,
18
48
  } from "../design.js";
19
- import { atlasBoxes, emptyAtlas, type RenderAtlas } from "../atlas.js";
20
- import { canvasFor, union } from "../bounds.js";
21
- import { DIAGRAM_MARGIN } from "../design.js";
22
- import { coord, type Box } from "../geometry.js";
23
- import {
24
- badgeRow,
25
- badgeWidth,
26
- cardBadges,
27
- cardTextWidth,
28
- deltaBadgeText,
29
- laneHeaderText,
30
- type PlacedNode,
31
- } from "../layout/architecture.js";
32
- import { relieveCongestion } from "../layout/congestion.js";
33
- import { curveBounds, type RoutedEdge } from "../layout/edges.js";
34
- import { placeLabelPills } from "../layout/labels.js";
35
- import type { ScopedGraph } from "../scope.js";
36
- import { truncate } from "../text.js";
37
- import type { Palette } from "../theme.js";
38
- import { markerFor, shifted, toneColour, toneFor, type Tone } from "./document.js";
39
- import { glyphGroup } from "./icons.js";
40
- import { lines, tag, textNode, wrap } from "./primitives.js";
41
- import { travellingPulses } from "./pulse.js";
42
49
 
43
50
  const badgeTone = (node: GraphNode, text: string): Tone =>
44
51
  text === deltaBadgeText(node.delta) ? toneFor(node.delta) : "neutral";
45
52
 
46
53
  /** The badge row, laid left to right across the strip the layout reserved. */
47
- const paintBadges = (placed: PlacedNode): string => {
54
+ const paintBadges = (placed: PlacedNode, palette: Palette): string => {
55
+ const styles = stylesFor(palette);
48
56
  const row = badgeRow(placed);
49
57
  if (row === undefined) return "";
50
58
 
@@ -52,6 +60,7 @@ const paintBadges = (placed: PlacedNode): string => {
52
60
  return lines(
53
61
  row.badges.map((text) => {
54
62
  const width = badgeWidth(text);
63
+ const colours = badgeColours(badgeTone(placed.node, text), palette);
55
64
  const painted = wrap(
56
65
  "g",
57
66
  { class: `bdg bdg-${badgeTone(placed.node, text)}` },
@@ -61,12 +70,17 @@ const paintBadges = (placed: PlacedNode): string => {
61
70
  width: coord(width),
62
71
  height: BADGE_HEIGHT,
63
72
  rx: BADGE_RADIUS,
73
+ fill: colours.fill,
74
+ stroke: colours.stroke,
75
+ "stroke-width": 1,
64
76
  }) +
65
77
  textNode(
66
78
  {
67
79
  x: coord(x + width / 2),
68
80
  y: coord(row.box.y + BADGE_HEIGHT / 2 + 3),
69
81
  "text-anchor": "middle",
82
+ ...styles.badgeText,
83
+ fill: colours.text,
70
84
  },
71
85
  text,
72
86
  ),
@@ -91,7 +105,8 @@ const cardOutlineClass = (node: GraphNode): string => {
91
105
  }
92
106
  };
93
107
 
94
- export const paintCard = (placed: PlacedNode): string => {
108
+ export const paintCard = (placed: PlacedNode, palette: Palette): string => {
109
+ const styles = stylesFor(palette);
95
110
  const { node, box, showIcon, titleSize } = placed;
96
111
  const textX = box.x + CARD_PADDING_X + (showIcon ? ICON_CHIP_SIZE + ICON_CHIP_GAP : 0);
97
112
  const textWidth = cardTextWidth(box.width, showIcon);
@@ -106,11 +121,13 @@ export const paintCard = (placed: PlacedNode): string => {
106
121
  width: ICON_CHIP_SIZE,
107
122
  height: ICON_CHIP_SIZE,
108
123
  rx: ICON_CHIP_RADIUS,
124
+ ...styles.chip,
109
125
  }) +
110
126
  glyphGroup(
111
127
  node.kind,
112
128
  box.x + CARD_PADDING_X + ICON_CHIP_SIZE / 2,
113
129
  box.y + box.height / 2,
130
+ styles,
114
131
  )
115
132
  : "";
116
133
 
@@ -120,6 +137,8 @@ export const paintCard = (placed: PlacedNode): string => {
120
137
  x: coord(textX),
121
138
  y: coord(titleBaseline),
122
139
  "font-size": titleSize,
140
+ ...styles.title,
141
+ "text-decoration": node.delta === "removed" ? "line-through" : undefined,
123
142
  },
124
143
  truncate(node.label, "sans-bold", titleSize, textWidth),
125
144
  );
@@ -128,7 +147,7 @@ export const paintCard = (placed: PlacedNode): string => {
128
147
  node.subtitle === undefined
129
148
  ? ""
130
149
  : textNode(
131
- { class: "nsub", x: coord(textX), y: coord(box.y + 45) },
150
+ { class: "nsub", x: coord(textX), y: coord(box.y + 45), ...styles.subtitle },
132
151
  truncate(node.subtitle, "mono", SUBTITLE_SIZE, textWidth),
133
152
  );
134
153
 
@@ -137,7 +156,7 @@ export const paintCard = (placed: PlacedNode): string => {
137
156
 
138
157
  return wrap(
139
158
  "g",
140
- { class: groupClass },
159
+ { class: groupClass, ...cardGroupAttributes(node.delta, palette) },
141
160
  lines([
142
161
  tag("rect", {
143
162
  class: cardOutlineClass(node),
@@ -146,11 +165,12 @@ export const paintCard = (placed: PlacedNode): string => {
146
165
  width: coord(box.width),
147
166
  height: coord(box.height),
148
167
  rx: CARD_RADIUS,
168
+ ...cardAttributes(node.delta, palette),
149
169
  }),
150
170
  chip,
151
171
  title,
152
172
  subtitle,
153
- paintBadges(placed),
173
+ paintBadges(placed, palette),
154
174
  ]),
155
175
  );
156
176
  };
@@ -181,6 +201,7 @@ const paintEdge = (
181
201
  label: Box | undefined,
182
202
  ): { markup: string; pill: string } => {
183
203
  const { edge, path } = routed;
204
+ const styles = stylesFor(palette);
184
205
  const tone = toneFor(edge.delta);
185
206
  const hero = edge.emphasis === "hero";
186
207
 
@@ -190,24 +211,25 @@ const paintEdge = (
190
211
  if (edge.delta === "unchanged") classes.push("context");
191
212
 
192
213
  const glow = hero
193
- ? tag("path", { class: "glow", stroke: toneColour(palette, tone), d: path })
214
+ ? tag("path", { class: "glow", stroke: toneColour(palette, tone), d: path, ...styles.glow })
194
215
  : "";
195
216
 
196
217
  return {
197
218
  markup: lines([
198
219
  glow,
199
- tag("path", { class: classes.join(" "), d: path, "marker-end": markerFor(tone) }),
220
+ tag("path", { class: classes.join(" "), d: path, "marker-end": markerFor(tone), ...edgeAttributes(edge, palette) }),
200
221
  pulses(edge, path, palette),
201
222
  ]),
202
223
  pill:
203
224
  label === undefined || edge.label === undefined
204
225
  ? ""
205
- : paintLabelPill(edge.label, label, tone),
226
+ : paintLabelPill(edge.label, label, tone, palette),
206
227
  };
207
228
  };
208
229
 
209
- export const paintLabelPill = (text: string, box: Box, tone: Tone): string =>
210
- wrap(
230
+ export const paintLabelPill = (text: string, box: Box, tone: Tone, palette: Palette): string => {
231
+ const styles = stylesFor(palette);
232
+ return wrap(
211
233
  "g",
212
234
  {},
213
235
  tag("rect", {
@@ -217,6 +239,7 @@ export const paintLabelPill = (text: string, box: Box, tone: Tone): string =>
217
239
  width: coord(box.width),
218
240
  height: coord(box.height),
219
241
  rx: box.height / 2,
242
+ ...styles.pill,
220
243
  }) +
221
244
  textNode(
222
245
  {
@@ -224,10 +247,13 @@ export const paintLabelPill = (text: string, box: Box, tone: Tone): string =>
224
247
  x: coord(box.x + box.width / 2),
225
248
  y: coord(box.y + box.height / 2 + 3.5),
226
249
  "text-anchor": "middle",
250
+ ...styles.pillText,
251
+ fill: badgeColours(tone, palette).text,
227
252
  },
228
253
  text,
229
254
  ),
230
255
  );
256
+ };
231
257
 
232
258
  export type ArchitecturePainting = {
233
259
  width: number;
@@ -242,6 +268,7 @@ export const paintArchitecture = (
242
268
  palette: Palette,
243
269
  ): ArchitecturePainting => {
244
270
  const { layout, routed } = relieveCongestion(graph, hints);
271
+ const styles = stylesFor(palette);
245
272
  const pills = placeLabelPills(routed);
246
273
 
247
274
  const drawn: Box[] = layout.nodes.flatMap((node) => {
@@ -267,9 +294,10 @@ export const paintArchitecture = (
267
294
  width: coord(box.width),
268
295
  height: coord(box.height),
269
296
  rx: LANE_RADIUS,
297
+ ...styles.lane,
270
298
  }),
271
299
  textNode(
272
- { class: "lanelabel", x: coord(box.x + LANE_PADDING_X), y: LANE_HEADER_BASELINE },
300
+ { class: "lanelabel", x: coord(box.x + LANE_PADDING_X), y: LANE_HEADER_BASELINE, ...styles.laneLabel },
273
301
  laneHeaderText(lane),
274
302
  ),
275
303
  ]),
@@ -288,7 +316,7 @@ export const paintArchitecture = (
288
316
  const painted = lines([
289
317
  wrap("g", {}, lines(lanes)),
290
318
  wrap("g", {}, lines(edgeMarkup)),
291
- wrap("g", {}, lines(layout.nodes.map(paintCard))),
319
+ wrap("g", {}, lines(layout.nodes.map((node) => paintCard(node, palette)))),
292
320
  wrap("g", {}, lines(pillMarkup)),
293
321
  ]);
294
322
 
@@ -1,5 +1,16 @@
1
- import type { Flow, FlowMessage, GraphNode, MessageKind } from "@coldtea/pr-lens-schema";
2
1
  import { assertNever } from "@coldtea/pr-lens-schema";
2
+ import type { Flow, FlowMessage, GraphNode, MessageKind } from "@coldtea/pr-lens-schema";
3
+ import { measure } from "../text.js";
4
+ import { coord } from "../geometry.js";
5
+ import type { Box } from "../geometry.js";
6
+ import type { Palette } from "../theme.js";
7
+ import { lines, tag, wrap } from "./primitives.js";
8
+ import { PULSE_RADIUS, TRAIN_RADIUS } from "./pulse.js";
9
+ import { messageAttributes, stylesFor } from "./styles.js";
10
+ import { paintCard, paintLabelPill } from "./architecture.js";
11
+ import { atlasBoxes, emptyAtlas, type RenderAtlas } from "../atlas.js";
12
+ import { canvasFor, covering, union, type Canvas } from "../bounds.js";
13
+ import { markerFor, openMarkerFor, shifted, toneColour, toneFor, type Tone } from "./document.js";
3
14
  import {
4
15
  DIAGRAM_MARGIN,
5
16
  FLOW_CYCLE_MAX,
@@ -12,11 +23,6 @@ import {
12
23
  PILL_TEXT_SIZE,
13
24
  TITLE_SIZE,
14
25
  } from "../design.js";
15
- import { atlasBoxes, emptyAtlas, type RenderAtlas } from "../atlas.js";
16
- import { canvasFor, covering, union, type Canvas } from "../bounds.js";
17
- import type { Box } from "../geometry.js";
18
- import { measure } from "../text.js";
19
- import { coord } from "../geometry.js";
20
26
  import {
21
27
  ACTIVATION_HALF_WIDTH,
22
28
  FLOW_BAND_PAD_X,
@@ -32,11 +38,6 @@ import {
32
38
  type FlowLayout,
33
39
  type PlacedMessage,
34
40
  } from "../layout/dataflow.js";
35
- import type { Palette } from "../theme.js";
36
- import { paintCard, paintLabelPill } from "./architecture.js";
37
- import { markerFor, openMarkerFor, shifted, toneColour, toneFor, type Tone } from "./document.js";
38
- import { lines, tag, wrap } from "./primitives.js";
39
- import { PULSE_RADIUS, TRAIN_RADIUS } from "./pulse.js";
40
41
 
41
42
  /** A ratio inside the animation cycle, written to a fixed number of places. */
42
43
  const ratio = (value: number): string => String(Math.round(value * 10000) / 10000);
@@ -236,16 +237,17 @@ const paintMessage = (
236
237
  const tone = toneFor(placed.message.delta);
237
238
  const direction = travelDirection(placed.message.kind, placed.fromX, placed.toX);
238
239
  const head = headFor(placed.message.kind, tone);
240
+ const attributes = messageAttributes(placed.message, palette);
239
241
 
240
242
  if (direction === 0) {
241
243
  const activated = activeAt(placed.message.from, placed.y);
242
244
  const path = selfPath(placed.fromX, placed.y, activated);
243
245
  return {
244
246
  line: lines([
245
- tag("path", { class: messageClasses(placed.message, tone), d: path, "marker-end": head }),
247
+ tag("path", { class: messageClasses(placed.message, tone), d: path, "marker-end": head, ...attributes }),
246
248
  pulsesFor(placed, path, slotCount, palette),
247
249
  ]),
248
- pill: paintLabelPill(placed.label, selfPillBox(placed, activated), tone),
250
+ pill: paintLabelPill(placed.label, selfPillBox(placed, activated), tone, palette),
249
251
  };
250
252
  }
251
253
 
@@ -254,10 +256,10 @@ const paintMessage = (
254
256
 
255
257
  return {
256
258
  line: lines([
257
- tag("path", { class: messageClasses(placed.message, tone), d: path, "marker-end": head }),
259
+ tag("path", { class: messageClasses(placed.message, tone), d: path, "marker-end": head, ...attributes }),
258
260
  pulsesFor(placed, path, slotCount, palette),
259
261
  ]),
260
- pill: paintLabelPill(placed.label, pillBox(placed, ends), tone),
262
+ pill: paintLabelPill(placed.label, pillBox(placed, ends), tone, palette),
261
263
  };
262
264
  };
263
265
 
@@ -283,6 +285,7 @@ const paintFlow = (
283
285
  palette: Palette,
284
286
  ): string => {
285
287
  const activeAt = activationLookup(layout);
288
+ const styles = stylesFor(palette);
286
289
 
287
290
  const lifelineBottom = layout.top + layout.height;
288
291
 
@@ -295,6 +298,7 @@ const paintFlow = (
295
298
  width: coord(box.width),
296
299
  height: coord(box.height),
297
300
  rx: LANE_RADIUS,
301
+ ...styles.lane,
298
302
  });
299
303
  });
300
304
 
@@ -306,6 +310,7 @@ const paintFlow = (
306
310
  y1: coord(layout.lifelineTop),
307
311
  x2: coord(participant.centreX),
308
312
  y2: coord(lifelineBottom),
313
+ ...styles.lifeline,
309
314
  }),
310
315
  ...participant.activations.map((bar) =>
311
316
  tag("rect", {
@@ -315,20 +320,24 @@ const paintFlow = (
315
320
  width: ACTIVATION_HALF_WIDTH * 2,
316
321
  height: coord(bar.bottom - bar.top),
317
322
  rx: 4,
323
+ ...styles.activation,
318
324
  }),
319
325
  ),
320
326
  ]),
321
327
  );
322
328
 
323
329
  const cards = layout.participants.map((participant, index) =>
324
- paintCard({
325
- node: participant.node,
326
- box: participant.card,
327
- showIcon: true,
328
- titleSize: TITLE_SIZE,
329
- row: 0,
330
- laneIndex: index,
331
- }),
330
+ paintCard(
331
+ {
332
+ node: participant.node,
333
+ box: participant.card,
334
+ showIcon: true,
335
+ titleSize: TITLE_SIZE,
336
+ row: 0,
337
+ laneIndex: index,
338
+ },
339
+ palette,
340
+ ),
332
341
  );
333
342
 
334
343
  const lineMarkup: string[] = [];
@@ -1,9 +1,9 @@
1
1
  import type { Delta } from "@coldtea/pr-lens-schema";
2
2
  import { assertNever } from "@coldtea/pr-lens-schema";
3
- import type { Canvas } from "../bounds.js";
4
3
  import { coord } from "../geometry.js";
4
+ import { SANS_STACK } from "../text.js";
5
+ import type { Canvas } from "../bounds.js";
5
6
  import type { Palette } from "../theme.js";
6
- import { MONO_STACK, SANS_STACK } from "../text.js";
7
7
  import { escapeXml, lines, tag, wrap } from "./primitives.js";
8
8
 
9
9
  /**
@@ -45,55 +45,6 @@ export const toneColour = (palette: Palette, tone: Tone): string => {
45
45
 
46
46
  const TONES = ["added", "modified", "removed", "neutral"] as const satisfies readonly Tone[];
47
47
 
48
- const stylesheet = (palette: Palette): string =>
49
- [
50
- `text{font-family:${SANS_STACK}}`,
51
- `.lanebox{fill:${palette.lane}}`,
52
- `.lanelabel{font-size:10px;font-weight:700;letter-spacing:.12em;fill:${palette.muted}}`,
53
- `.card{fill:${palette.card};stroke:${palette.cardBorder};stroke-width:1}`,
54
- `.card-added{stroke:${palette.added};stroke-opacity:.55}`,
55
- `.card-modified{stroke:${palette.modified};stroke-opacity:.5}`,
56
- `.ghost{opacity:.55}`,
57
- `.ghost .card{stroke:${palette.removed};stroke-dasharray:4 3;stroke-opacity:.6}`,
58
- `.context{opacity:.82}`,
59
- `.ntitle{font-weight:600;fill:${palette.foreground}}`,
60
- `.strike{text-decoration:line-through}`,
61
- `.nsub{font-size:9.5px;fill:${palette.muted};font-family:${MONO_STACK}}`,
62
- `.chip{fill:${palette.chip}}`,
63
- `.glyph{fill:${palette.muted}}`,
64
- `.glyph-stroke{stroke:${palette.muted};stroke-width:1.4;fill:none}`,
65
- `.bdg text{font-size:8.5px;font-weight:700;letter-spacing:.06em}`,
66
- `.bdg rect{stroke-width:1}`,
67
- `.bdg-added rect{fill:${palette.addedFill};stroke:${palette.addedBorder}}`,
68
- `.bdg-added text{fill:${palette.addedText}}`,
69
- `.bdg-modified rect{fill:${palette.modifiedFill};stroke:${palette.modifiedBorder}}`,
70
- `.bdg-modified text{fill:${palette.modifiedText}}`,
71
- `.bdg-removed rect{fill:${palette.removedFill};stroke:${palette.removedBorder}}`,
72
- `.bdg-removed text{fill:${palette.removedText}}`,
73
- `.bdg-neutral rect{fill:${palette.neutralFill};stroke:${palette.cardBorder}}`,
74
- `.bdg-neutral text{fill:${palette.muted}}`,
75
- `.edge{fill:none;stroke-width:1.5}`,
76
- `.edge-added{stroke:${palette.added}}`,
77
- `.edge-modified{stroke:${palette.modified}}`,
78
- `.edge-removed{stroke:${palette.removed};stroke-dasharray:5 4;opacity:.7}`,
79
- `.edge-neutral{stroke:${palette.edge}}`,
80
- `.hero{stroke-width:2.25}`,
81
- `.faded{opacity:.45}`,
82
- `.glow{fill:none;stroke-width:7;opacity:.14}`,
83
- `.msg-self{font-size:11px;fill:${palette.foreground}}`,
84
- `.lpill{fill:${palette.pill};stroke:${palette.pillBorder};stroke-width:1}`,
85
- `.ltext{font-size:9.5px;font-weight:600;fill:${palette.muted}}`,
86
- `.ltext-added{fill:${palette.addedText}}`,
87
- `.ltext-modified{fill:${palette.modifiedText}}`,
88
- `.ltext-removed{fill:${palette.removedText}}`,
89
- `.cardsh{filter:drop-shadow(0 1px 2px ${palette.shadow})}`,
90
- `.lifeline{stroke:${palette.lifeline};stroke-width:1;stroke-dasharray:3 4}`,
91
- `.actbar{fill:${palette.addedFill};stroke:${palette.addedBorder}}`,
92
- `.msg{fill:none;stroke-width:1.5}`,
93
- `.msg-return{stroke-dasharray:4 3;opacity:.8}`,
94
- `.msg-strong{stroke-width:2.25}`,
95
- ].join("");
96
-
97
48
  /**
98
49
  * Two arrowhead forms per tone, the classic sequence-diagram pair: a filled
99
50
  * head for a message the sender waits on, an open line-form head for one it
@@ -174,10 +125,10 @@ export const svgDocument = (input: {
174
125
 
175
126
  return lines([
176
127
  `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${coord(width)} ${coord(height)}" ` +
177
- `width="${coord(width)}" height="${coord(height)}" role="img" aria-label="${escapeXml(title)}">`,
128
+ `width="${coord(width)}" height="${coord(height)}" role="img" aria-label="${escapeXml(title)}" ` +
129
+ `font-family="${escapeXml(SANS_STACK)}">`,
178
130
  wrap("title", {}, escapeXml(title)),
179
131
  description === undefined ? "" : wrap("desc", {}, escapeXml(description)),
180
- wrap("style", {}, stylesheet(palette)),
181
132
  defs,
182
133
  tag("rect", { x: 0, y: 0, width: coord(width), height: coord(height), rx: 12, fill: palette.background }),
183
134
  tag("rect", { x: 0, y: 0, width: coord(width), height: coord(height), rx: 12, fill: "url(#dots)" }),
package/src/svg/icons.ts CHANGED
@@ -1,33 +1,23 @@
1
- import type { NodeKind } from "@coldtea/pr-lens-schema";
2
1
  import { assertNever } from "@coldtea/pr-lens-schema";
2
+ import type { NodeKind } from "@coldtea/pr-lens-schema";
3
3
  import { coord } from "../geometry.js";
4
4
  import { MONO_STACK } from "../text.js";
5
+ import type { SvgStyles } from "./styles.js";
6
+ import type { Attributes } from "./primitives.js";
5
7
  import { lines, tag, textNode, wrap } from "./primitives.js";
6
8
 
7
- const stroked = (path: string): string => tag("path", { class: "glyph-stroke", d: path });
8
-
9
- const glyphText = (cx: number, cy: number, content: string, style: string): string =>
9
+ const glyphText = (cx: number, cy: number, content: string, attributes: Attributes): string =>
10
10
  textNode(
11
11
  {
12
12
  class: "glyph",
13
13
  x: coord(cx),
14
14
  y: coord(cy + 4.5),
15
15
  "text-anchor": "middle",
16
- style,
16
+ ...attributes,
17
17
  },
18
18
  content,
19
19
  );
20
20
 
21
- const box = (cx: number, cy: number, width: number, height: number, radius?: number): string =>
22
- tag("rect", {
23
- class: "glyph-stroke",
24
- x: coord(cx - width / 2),
25
- y: coord(cy - height / 2),
26
- width: coord(width),
27
- height: coord(height),
28
- rx: radius === undefined ? undefined : coord(radius),
29
- });
30
-
31
21
  /**
32
22
  * The chip glyph for a node's kind, centred on a point.
33
23
  *
@@ -35,12 +25,26 @@ const box = (cx: number, cy: number, width: number, height: number, radius?: num
35
25
  * is why the kind enum is coarse. Every kind draws something: a card with an
36
26
  * empty chip reads as a rendering fault rather than as an unknown kind.
37
27
  */
38
- const kindGlyph = (kind: NodeKind, cx: number, cy: number): string => {
28
+ const kindGlyph = (kind: NodeKind, cx: number, cy: number, styles: SvgStyles): string => {
29
+ const stroked = (path: string): string =>
30
+ tag("path", { class: "glyph-stroke", d: path, ...styles.glyphStroke });
31
+
32
+ const box = (cx: number, cy: number, width: number, height: number, radius?: number): string =>
33
+ tag("rect", {
34
+ class: "glyph-stroke",
35
+ x: coord(cx - width / 2),
36
+ y: coord(cy - height / 2),
37
+ width: coord(width),
38
+ height: coord(height),
39
+ rx: radius === undefined ? undefined : coord(radius),
40
+ ...styles.glyphStroke,
41
+ });
42
+
39
43
  switch (kind) {
40
44
  case "service":
41
45
  return lines([
42
46
  box(cx, cy, 13, 13, 3),
43
- tag("circle", { class: "glyph", cx: coord(cx), cy: coord(cy), r: 2 }),
47
+ tag("circle", { class: "glyph", ...styles.glyph, cx: coord(cx), cy: coord(cy), r: 2 }),
44
48
  ]);
45
49
  case "app":
46
50
  return lines([
@@ -48,17 +52,23 @@ const kindGlyph = (kind: NodeKind, cx: number, cy: number): string => {
48
52
  stroked(`M${coord(cx - 7)},${coord(cy - 2)} h14`),
49
53
  ]);
50
54
  case "module":
51
- return glyphText(cx, cy, "{ }", `font-size:11px;font-family:${MONO_STACK}`);
55
+ return glyphText(cx, cy, "{ }", { ...styles.glyph, "font-size": 11, "font-family": MONO_STACK });
52
56
  case "function":
53
- return glyphText(cx, cy, "ƒ", "font-size:14px;font-style:italic;font-family:Georgia,serif");
57
+ return glyphText(cx, cy, "ƒ", {
58
+ ...styles.glyph,
59
+ "font-size": 14,
60
+ "font-style": "italic",
61
+ "font-family": "Georgia,serif",
62
+ });
54
63
  case "route":
55
64
  return tag("path", {
56
65
  class: "glyph",
66
+ ...styles.glyph,
57
67
  d: `M${coord(cx - 7)},${coord(cy - 6)} L${coord(cx + 7)},${coord(cy)} L${coord(cx - 7)},${coord(cy + 6)} Z`,
58
68
  });
59
69
  case "job":
60
70
  return lines([
61
- tag("circle", { class: "glyph-stroke", cx: coord(cx), cy: coord(cy), r: 6.5 }),
71
+ tag("circle", { class: "glyph-stroke", ...styles.glyphStroke, cx: coord(cx), cy: coord(cy), r: 6.5 }),
62
72
  stroked(`M${coord(cx)},${coord(cy - 3.5)} V${coord(cy)} H${coord(cx + 3)}`),
63
73
  ]);
64
74
  case "queue":
@@ -69,7 +79,7 @@ const kindGlyph = (kind: NodeKind, cx: number, cy: number): string => {
69
79
  ]);
70
80
  case "datastore":
71
81
  return lines([
72
- tag("ellipse", { class: "glyph-stroke", cx: coord(cx), cy: coord(cy - 4.5), rx: 6.5, ry: 2.6 }),
82
+ tag("ellipse", { class: "glyph-stroke", ...styles.glyphStroke, cx: coord(cx), cy: coord(cy - 4.5), rx: 6.5, ry: 2.6 }),
73
83
  stroked(
74
84
  `M${coord(cx - 6.5)},${coord(cy - 4.5)} v9 a6.5,2.6 0 0 0 13 0 v-9`,
75
85
  ),
@@ -77,6 +87,7 @@ const kindGlyph = (kind: NodeKind, cx: number, cy: number): string => {
77
87
  case "cache":
78
88
  return tag("path", {
79
89
  class: "glyph",
90
+ ...styles.glyph,
80
91
  d: `M${coord(cx + 1)},${coord(cy - 7)} L${coord(cx - 6)},${coord(cy + 1)} H${coord(cx - 1)} L${coord(cx - 1)},${coord(cy + 7)} L${coord(cx + 6)},${coord(cy - 1)} H${coord(cx + 1)} Z`,
81
92
  });
82
93
  case "external":
@@ -93,8 +104,8 @@ const kindGlyph = (kind: NodeKind, cx: number, cy: number): string => {
93
104
  return lines([
94
105
  stroked(`M${coord(cx - 7)},${coord(cy - 4)} h14`),
95
106
  stroked(`M${coord(cx - 7)},${coord(cy + 4)} h14`),
96
- tag("circle", { class: "glyph", cx: coord(cx - 2), cy: coord(cy - 4), r: 2.2 }),
97
- tag("circle", { class: "glyph", cx: coord(cx + 3), cy: coord(cy + 4), r: 2.2 }),
107
+ tag("circle", { class: "glyph", ...styles.glyph, cx: coord(cx - 2), cy: coord(cy - 4), r: 2.2 }),
108
+ tag("circle", { class: "glyph", ...styles.glyph, cx: coord(cx + 3), cy: coord(cy + 4), r: 2.2 }),
98
109
  ]);
99
110
  case "test":
100
111
  return stroked(`M${coord(cx - 6)},${coord(cy)} l4,4 l8,-8`);
@@ -108,14 +119,14 @@ const kindGlyph = (kind: NodeKind, cx: number, cy: number): string => {
108
119
  ]);
109
120
  case "other":
110
121
  return lines([
111
- tag("circle", { class: "glyph", cx: coord(cx - 5), cy: coord(cy), r: 1.8 }),
112
- tag("circle", { class: "glyph", cx: coord(cx), cy: coord(cy), r: 1.8 }),
113
- tag("circle", { class: "glyph", cx: coord(cx + 5), cy: coord(cy), r: 1.8 }),
122
+ tag("circle", { class: "glyph", ...styles.glyph, cx: coord(cx - 5), cy: coord(cy), r: 1.8 }),
123
+ tag("circle", { class: "glyph", ...styles.glyph, cx: coord(cx), cy: coord(cy), r: 1.8 }),
124
+ tag("circle", { class: "glyph", ...styles.glyph, cx: coord(cx + 5), cy: coord(cy), r: 1.8 }),
114
125
  ]);
115
126
  default:
116
127
  return assertNever(kind, "Unhandled node kind");
117
128
  }
118
129
  };
119
130
 
120
- export const glyphGroup = (kind: NodeKind, cx: number, cy: number): string =>
121
- wrap("g", {}, kindGlyph(kind, cx, cy));
131
+ export const glyphGroup = (kind: NodeKind, cx: number, cy: number, styles: SvgStyles): string =>
132
+ wrap("g", {}, kindGlyph(kind, cx, cy, styles));