@nowline/renderer 0.5.1 → 0.7.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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { AssetBytes, AssetResolver, RenderOptions, } from './svg/render.js';
1
+ export type { AssetBytes, AssetResolver, FontFamilies, RenderOptions, } from './svg/render.js';
2
2
  export { renderSvg } from './svg/render.js';
3
3
  export { sanitizeSvg } from './svg/sanitize.js';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACR,UAAU,EACV,aAAa,EACb,aAAa,GAChB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACR,UAAU,EACV,aAAa,EACb,YAAY,EACZ,aAAa,GAChB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
@@ -4,17 +4,31 @@ export interface AssetBytes {
4
4
  mime: string;
5
5
  }
6
6
  export type AssetResolver = (ref: string) => Promise<AssetBytes>;
7
+ /**
8
+ * Per-role `font-family` strings the renderer stamps onto `<text>` elements.
9
+ * Defaults to the shared, portable `FONT_STACK` (generic CSS stacks). Raster
10
+ * and preview callers override this with a *pinned* family (e.g. the bundled
11
+ * `DejaVu Sans` / `DejaVu Sans Mono`) so the rendered SVG names exactly the
12
+ * font that resvg / the webview `@font-face` actually provide — the WYSIWYG
13
+ * contract. The `.svg` file export keeps the default portable stack.
14
+ */
15
+ export type FontFamilies = Record<'sans' | 'serif' | 'mono', string>;
7
16
  export interface RenderOptions {
8
17
  assetResolver?: AssetResolver;
9
18
  noLinks?: boolean;
10
19
  strict?: boolean;
11
20
  warn?: (message: string) => void;
12
21
  idPrefix?: string;
22
+ /**
23
+ * Override per-role `font-family` strings. Defaults to the portable
24
+ * `FONT_STACK`. Set to a pinned family for raster/preview WYSIWYG.
25
+ */
26
+ fontFamilies?: FontFamilies;
13
27
  }
14
- declare function renderHeader(h: PositionedHeader, idPrefix: string, palette: Theme): string;
15
- declare function renderTimeline(t: PositionedTimelineScale, palette: Theme): string;
16
- declare function renderItem(i: PositionedItem, options: RenderOptions, idPrefix: string, palette: Theme): string;
17
- declare function renderSwimlane(s: PositionedSwimlane, options: RenderOptions, idPrefix: string, palette: Theme): string;
28
+ declare function renderHeader(h: PositionedHeader, idPrefix: string, palette: Theme, fonts: FontFamilies): string;
29
+ declare function renderTimeline(t: PositionedTimelineScale, palette: Theme, fonts: FontFamilies): string;
30
+ declare function renderItem(i: PositionedItem, options: RenderOptions, idPrefix: string, palette: Theme, fonts: FontFamilies): string;
31
+ declare function renderSwimlane(s: PositionedSwimlane, options: RenderOptions, idPrefix: string, palette: Theme, fonts: FontFamilies): string;
18
32
  declare function renderEdge(e: PositionedDependencyEdge, palette: Theme): string;
19
33
  export declare function renderSvg(model: PositionedRoadmap, options?: RenderOptions): Promise<string>;
20
34
  export declare const __internal: {
@@ -1 +1 @@
1
- {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/svg/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAKR,wBAAwB,EAGxB,gBAAgB,EAEhB,cAAc,EAId,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,EAGvB,KAAK,EACR,MAAM,iBAAiB,CAAC;AAsEzB,MAAM,WAAW,UAAU;IACvB,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAEjE,MAAM,WAAW,aAAa;IAC1B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAEjC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB;AA8VD,iBAAS,YAAY,CAAC,CAAC,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,GAAG,MAAM,CAyEnF;AA6ED,iBAAS,cAAc,CAAC,CAAC,EAAE,uBAAuB,EAAE,OAAO,EAAE,KAAK,GAAG,MAAM,CA2G1E;AAoID,iBAAS,UAAU,CACf,CAAC,EAAE,cAAc,EACjB,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,KAAK,GACf,MAAM,CA8SR;AAgRD,iBAAS,cAAc,CACnB,CAAC,EAAE,kBAAkB,EACrB,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,KAAK,GACf,MAAM,CAyHR;AAkHD,iBAAS,UAAU,CAAC,CAAC,EAAE,wBAAwB,EAAE,OAAO,EAAE,KAAK,GAAG,MAAM,CAmBvE;AAuYD,wBAAsB,SAAS,CAC3B,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE,aAAkB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAmHjB;AAGD,eAAO,MAAM,UAAU;;;;;;CAMtB,CAAC"}
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../../src/svg/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAKR,wBAAwB,EAGxB,gBAAgB,EAEhB,cAAc,EAId,iBAAiB,EACjB,kBAAkB,EAClB,uBAAuB,EAGvB,KAAK,EACR,MAAM,iBAAiB,CAAC;AAsEzB,MAAM,WAAW,UAAU;IACvB,KAAK,EAAE,UAAU,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;AAEjE;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,GAAG,OAAO,GAAG,MAAM,EAAE,MAAM,CAAC,CAAC;AAErE,MAAM,WAAW,aAAa;IAC1B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAEjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;CAC/B;AAkWD,iBAAS,YAAY,CACjB,CAAC,EAAE,gBAAgB,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,KAAK,EACd,KAAK,EAAE,YAAY,GACpB,MAAM,CAyER;AA6ED,iBAAS,cAAc,CAAC,CAAC,EAAE,uBAAuB,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,CA2G/F;AAwID,iBAAS,UAAU,CACf,CAAC,EAAE,cAAc,EACjB,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,KAAK,EACd,KAAK,EAAE,YAAY,GACpB,MAAM,CA8SR;AAmRD,iBAAS,cAAc,CACnB,CAAC,EAAE,kBAAkB,EACrB,OAAO,EAAE,aAAa,EACtB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,KAAK,EACd,KAAK,EAAE,YAAY,GACpB,MAAM,CAyHR;AAkHD,iBAAS,UAAU,CAAC,CAAC,EAAE,wBAAwB,EAAE,OAAO,EAAE,KAAK,GAAG,MAAM,CAmBvE;AA6YD,wBAAsB,SAAS,CAC3B,KAAK,EAAE,iBAAiB,EACxB,OAAO,GAAE,aAAkB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAyHjB;AAGD,eAAO,MAAM,UAAU;;;;;;CAMtB,CAAC"}
@@ -23,9 +23,9 @@ const WEIGHT_NUM = {
23
23
  function textSizePx(bucket) {
24
24
  return TEXT_SIZE_PX[bucket] ?? 14;
25
25
  }
26
- function fontAttrs(style, overrideSize) {
26
+ function fontAttrs(style, fonts, overrideSize) {
27
27
  return {
28
- 'font-family': FONT_STACK[style.font],
28
+ 'font-family': fonts[style.font],
29
29
  'font-size': overrideSize ?? textSizePx(style.textSize),
30
30
  'font-weight': WEIGHT_NUM[style.weight] ?? 400,
31
31
  'font-style': style.italic ? 'italic' : 'normal',
@@ -302,7 +302,7 @@ function rectFrame(x, y, w, h, style, extra = {}) {
302
302
  ...extra,
303
303
  });
304
304
  }
305
- function renderHeader(h, idPrefix, palette) {
305
+ function renderHeader(h, idPrefix, palette, fonts) {
306
306
  // The layout has already sized the card to its (wrapped) text content
307
307
  // and stashed the bounds in `h.cardBox`, with `h.titleLines` /
308
308
  // `h.authorLines` ready to render line-by-line. See sizeBesideHeader
@@ -333,7 +333,7 @@ function renderHeader(h, idPrefix, palette) {
333
333
  titleParts.push(textTag({
334
334
  x: num(cardX + HEADER_CARD_PADDING_X),
335
335
  y: num(cardY + HEADER_CARD_PADDING_TOP + i * HEADER_TITLE_LINE_HEIGHT_PX),
336
- 'font-family': FONT_STACK[h.style.font],
336
+ 'font-family': fonts[h.style.font],
337
337
  'font-size': HEADER_TITLE_FONT_SIZE_PX,
338
338
  'font-weight': 600,
339
339
  fill: h.style.text,
@@ -351,7 +351,7 @@ function renderHeader(h, idPrefix, palette) {
351
351
  y: num(lastTitleY +
352
352
  HEADER_TITLE_TO_AUTHOR_GAP_PX +
353
353
  j * HEADER_AUTHOR_LINE_HEIGHT_PX),
354
- 'font-family': FONT_STACK[h.style.font],
354
+ 'font-family': fonts[h.style.font],
355
355
  'font-size': HEADER_AUTHOR_FONT_SIZE_PX,
356
356
  fill: authorColor,
357
357
  }, line));
@@ -431,7 +431,7 @@ function renderGridLines(t, swimlaneTopY, palette) {
431
431
  }
432
432
  return tag('g', { 'data-layer': 'grid' }, parts.join(''));
433
433
  }
434
- function renderTimeline(t, palette) {
434
+ function renderTimeline(t, palette, fonts) {
435
435
  const panelFill = palette.timeline.panelFill;
436
436
  const borderColor = palette.timeline.border;
437
437
  const labelColor = palette.timeline.labelText;
@@ -505,7 +505,7 @@ function renderTimeline(t, palette) {
505
505
  parts.push(textTag({
506
506
  x: num(tick.labelX),
507
507
  y: num(tickPanelY + TIMELINE_TICK_LABEL_BASELINE_OFFSET_PX),
508
- 'font-family': FONT_STACK.sans,
508
+ 'font-family': fonts.sans,
509
509
  'font-size': 10,
510
510
  fill: labelColor,
511
511
  'text-anchor': 'middle',
@@ -515,7 +515,7 @@ function renderTimeline(t, palette) {
515
515
  parts.push(textTag({
516
516
  x: num(tick.labelX),
517
517
  y: num(bottomTickPanelY + TIMELINE_TICK_LABEL_BASELINE_OFFSET_PX),
518
- 'font-family': FONT_STACK.sans,
518
+ 'font-family': fonts.sans,
519
519
  'font-size': 10,
520
520
  fill: labelColor,
521
521
  'text-anchor': 'middle',
@@ -524,7 +524,7 @@ function renderTimeline(t, palette) {
524
524
  }
525
525
  return tag('g', { 'data-layer': 'timeline' }, parts.join(''));
526
526
  }
527
- function renderNowline(n, palette) {
527
+ function renderNowline(n, palette, fonts) {
528
528
  if (!n)
529
529
  return '';
530
530
  const color = palette.nowline.stroke;
@@ -549,7 +549,7 @@ function renderNowline(n, palette) {
549
549
  // The squared edge IS the line; the rounded edge points into the
550
550
  // chart, so the pill always hugs the line and never overflows.
551
551
  const pillBg = renderNowPillBg(n, color);
552
- const label = renderNowPillLabel(n, labelTextColor);
552
+ const label = renderNowPillLabel(n, labelTextColor, fonts);
553
553
  return tag('g', { 'data-layer': 'nowline' }, line + pillBg + label);
554
554
  }
555
555
  /**
@@ -625,7 +625,7 @@ function renderNowPillBg(n, color) {
625
625
  ].join(' ');
626
626
  return tag('path', { d, fill: color });
627
627
  }
628
- function renderNowPillLabel(n, labelTextColor) {
628
+ function renderNowPillLabel(n, labelTextColor, fonts) {
629
629
  const baselineY = n.pillTopY + NOW_PILL_LABEL_BASELINE_OFFSET_PX;
630
630
  const edgeX = squaredEdgeX(n);
631
631
  let labelX;
@@ -645,14 +645,14 @@ function renderNowPillLabel(n, labelTextColor) {
645
645
  return textTag({
646
646
  x: num(labelX),
647
647
  y: num(baselineY),
648
- 'font-family': FONT_STACK.sans,
648
+ 'font-family': fonts.sans,
649
649
  'font-size': NOW_PILL_LABEL_FONT_SIZE_PX,
650
650
  'font-weight': 700,
651
651
  fill: labelTextColor,
652
652
  'text-anchor': textAnchor,
653
653
  }, n.label);
654
654
  }
655
- function renderItem(i, options, idPrefix, palette) {
655
+ function renderItem(i, options, idPrefix, palette, fonts) {
656
656
  const parts = [];
657
657
  const shadow = shadowFilterUrl(idPrefix, i.style.shadow);
658
658
  parts.push(rectFrame(i.box.x, i.box.y, i.box.width, i.box.height, i.style, {
@@ -747,7 +747,7 @@ function renderItem(i, options, idPrefix, palette) {
747
747
  parts.push(textTag({
748
748
  x: num(captionX),
749
749
  y: num(i.box.y + ITEM_CAPTION_TITLE_BASELINE_OFFSET_PX),
750
- 'font-family': FONT_STACK[i.style.font],
750
+ 'font-family': fonts[i.style.font],
751
751
  'font-size': ITEM_CAPTION_TITLE_FONT_SIZE_PX,
752
752
  'font-weight': 600,
753
753
  fill: titleColor,
@@ -767,7 +767,7 @@ function renderItem(i, options, idPrefix, palette) {
767
767
  x: captionX,
768
768
  baselineY: i.box.y + ITEM_CAPTION_META_BASELINE_OFFSET_PX,
769
769
  fontSize: ITEM_CAPTION_META_FONT_SIZE_PX,
770
- fontFamily: FONT_STACK[i.style.font],
770
+ fontFamily: fonts[i.style.font],
771
771
  color: metaColor,
772
772
  }));
773
773
  }
@@ -795,7 +795,7 @@ function renderItem(i, options, idPrefix, palette) {
795
795
  parts.push(textTag({
796
796
  x: num(fx),
797
797
  y: num(footnoteY),
798
- 'font-family': FONT_STACK.sans,
798
+ 'font-family': fonts.sans,
799
799
  'font-size': 10,
800
800
  'font-weight': 700,
801
801
  fill: captionOutsideTextColor,
@@ -810,7 +810,7 @@ function renderItem(i, options, idPrefix, palette) {
810
810
  parts.push(textTag({
811
811
  x: num(fx),
812
812
  y: num(footnoteY),
813
- 'font-family': FONT_STACK.sans,
813
+ 'font-family': fonts.sans,
814
814
  'font-size': 10,
815
815
  'font-weight': 700,
816
816
  fill: i.style.text,
@@ -887,7 +887,7 @@ function renderItem(i, options, idPrefix, palette) {
887
887
  parts.push(textTag({
888
888
  x: num(i.overflowBox.x + i.overflowBox.width / 2),
889
889
  y: num(i.overflowBox.y + i.overflowBox.height / 2 + 3),
890
- 'font-family': FONT_STACK.sans,
890
+ 'font-family': fonts.sans,
891
891
  'font-size': 9,
892
892
  'font-weight': 700,
893
893
  fill: captionColor,
@@ -912,13 +912,13 @@ function renderItem(i, options, idPrefix, palette) {
912
912
  parts.push(textTag({
913
913
  x: num(chip.box.x + chip.box.width / 2),
914
914
  y: num(chip.box.y + chip.box.height / 2 + 3),
915
- ...fontAttrs(chip.style, TEXT_SIZE_PX.xs),
915
+ ...fontAttrs(chip.style, fonts, TEXT_SIZE_PX.xs),
916
916
  'text-anchor': 'middle',
917
917
  }, chip.text));
918
918
  }
919
919
  return tag('g', { 'data-layer': 'item', 'data-id': i.id ?? null }, parts.join(''));
920
920
  }
921
- function renderGroup(g, options, idPrefix, palette) {
921
+ function renderGroup(g, options, idPrefix, palette, fonts) {
922
922
  const parts = [];
923
923
  const hasFill = g.style.bg !== 'none' && g.style.bg !== '#ffffff';
924
924
  if (hasFill) {
@@ -966,7 +966,7 @@ function renderGroup(g, options, idPrefix, palette) {
966
966
  parts.push(textTag({
967
967
  x: num(tabX + GROUP_TITLE_TAB_PAD_X_PX),
968
968
  y: num(tabY + GROUP_TITLE_TAB_LABEL_BASELINE_OFFSET_PX),
969
- 'font-family': FONT_STACK[g.style.font],
969
+ 'font-family': fonts[g.style.font],
970
970
  'font-size': GROUP_TITLE_TAB_LABEL_FONT_SIZE_PX,
971
971
  'font-weight': 600,
972
972
  fill: '#ffffff',
@@ -1008,7 +1008,7 @@ function renderGroup(g, options, idPrefix, palette) {
1008
1008
  parts.push(textTag({
1009
1009
  x: num(g.box.x + 6),
1010
1010
  y: num(g.box.y - 2),
1011
- ...fontAttrs(g.style, TEXT_SIZE_PX.xs),
1011
+ ...fontAttrs(g.style, fonts, TEXT_SIZE_PX.xs),
1012
1012
  'fill-opacity': 0.7,
1013
1013
  }, g.title));
1014
1014
  }
@@ -1019,12 +1019,12 @@ function renderGroup(g, options, idPrefix, palette) {
1019
1019
  // bounding box.
1020
1020
  parts.push(renderInlineDatePins(g.inlineDatePins, g.style.fg));
1021
1021
  for (const c of g.children) {
1022
- parts.push(renderTrackChild(c, options, idPrefix, palette));
1022
+ parts.push(renderTrackChild(c, options, idPrefix, palette, fonts));
1023
1023
  }
1024
1024
  void palette;
1025
1025
  return tag('g', { 'data-layer': 'group', 'data-id': g.id ?? null }, parts.join(''));
1026
1026
  }
1027
- function renderParallel(p, options, idPrefix, palette) {
1027
+ function renderParallel(p, options, idPrefix, palette, fonts) {
1028
1028
  const parts = [];
1029
1029
  // `bracket: solid|dashed` parallels render explicit [ ] brackets framing
1030
1030
  // the nested tracks with 12 px vertical padding above/below.
@@ -1057,7 +1057,7 @@ function renderParallel(p, options, idPrefix, palette) {
1057
1057
  parts.push(textTag({
1058
1058
  x: num(p.box.x + 4),
1059
1059
  y: num(p.box.y - 2),
1060
- ...fontAttrs(p.style, TEXT_SIZE_PX.xs),
1060
+ ...fontAttrs(p.style, fonts, TEXT_SIZE_PX.xs),
1061
1061
  'fill-opacity': 0.7,
1062
1062
  }, p.title));
1063
1063
  }
@@ -1065,16 +1065,16 @@ function renderParallel(p, options, idPrefix, palette) {
1065
1065
  // `before:DATE`). Painted before children so child bars sit on top.
1066
1066
  parts.push(renderInlineDatePins(p.inlineDatePins, p.style.fg));
1067
1067
  for (const c of p.children) {
1068
- parts.push(renderTrackChild(c, options, idPrefix, palette));
1068
+ parts.push(renderTrackChild(c, options, idPrefix, palette, fonts));
1069
1069
  }
1070
1070
  return tag('g', { 'data-layer': 'parallel', 'data-id': p.id ?? null }, parts.join(''));
1071
1071
  }
1072
- function renderTrackChild(c, options, idPrefix, palette) {
1072
+ function renderTrackChild(c, options, idPrefix, palette, fonts) {
1073
1073
  if (c.kind === 'item')
1074
- return renderItem(c, options, idPrefix, palette);
1074
+ return renderItem(c, options, idPrefix, palette, fonts);
1075
1075
  if (c.kind === 'group')
1076
- return renderGroup(c, options, idPrefix, palette);
1077
- return renderParallel(c, options, idPrefix, palette);
1076
+ return renderGroup(c, options, idPrefix, palette, fonts);
1077
+ return renderParallel(c, options, idPrefix, palette, fonts);
1078
1078
  }
1079
1079
  // Renders only the swimlane's background tint rect. Emitted before the
1080
1080
  // chart-body grid lines so those lines visibly span the full chart width.
@@ -1136,7 +1136,7 @@ function renderSwimlaneBg(s, palette) {
1136
1136
  'stroke-width': 1,
1137
1137
  }));
1138
1138
  }
1139
- function renderSwimlane(s, options, idPrefix, palette) {
1139
+ function renderSwimlane(s, options, idPrefix, palette, fonts) {
1140
1140
  const tabFill = palette.swimlane.tabFill;
1141
1141
  const tabStroke = palette.swimlane.tabStroke;
1142
1142
  const tabText = palette.swimlane.tabText;
@@ -1181,7 +1181,7 @@ function renderSwimlane(s, options, idPrefix, palette) {
1181
1181
  parts.push(textTag({
1182
1182
  x: num(tab.titleX),
1183
1183
  y: num(labelY),
1184
- 'font-family': FONT_STACK[s.style.font],
1184
+ 'font-family': fonts[s.style.font],
1185
1185
  'font-size': 12,
1186
1186
  'font-weight': 600,
1187
1187
  fill: tabText,
@@ -1190,7 +1190,7 @@ function renderSwimlane(s, options, idPrefix, palette) {
1190
1190
  parts.push(textTag({
1191
1191
  x: num(tab.ownerX),
1192
1192
  y: num(labelY),
1193
- 'font-family': FONT_STACK[s.style.font],
1193
+ 'font-family': fonts[s.style.font],
1194
1194
  'font-size': 10,
1195
1195
  fill: ownerText,
1196
1196
  }, `owner: ${s.owner}`));
@@ -1200,13 +1200,13 @@ function renderSwimlane(s, options, idPrefix, palette) {
1200
1200
  // item-level suffixes (m6) so multiplier / built-in SVG /
1201
1201
  // inline literal / dereferenced-custom-glyph paths stay
1202
1202
  // consistent across both contexts.
1203
- parts.push(renderCapacitySuffix(s.capacity, undefined, tab.badgeX, labelY, LANE_BADGE_FONT_SIZE_PX, FONT_STACK[s.style.font], ownerText));
1203
+ parts.push(renderCapacitySuffix(s.capacity, undefined, tab.badgeX, labelY, LANE_BADGE_FONT_SIZE_PX, fonts[s.style.font], ownerText));
1204
1204
  }
1205
1205
  if (footnoteIndicatorText) {
1206
1206
  parts.push(textTag({
1207
1207
  x: num(tab.footnoteRightX),
1208
1208
  y: num(tabY + 14),
1209
- 'font-family': FONT_STACK.sans,
1209
+ 'font-family': fonts.sans,
1210
1210
  'font-size': LANE_BADGE_FONT_SIZE_PX,
1211
1211
  'font-weight': 700,
1212
1212
  fill: footnoteColor,
@@ -1215,7 +1215,7 @@ function renderSwimlane(s, options, idPrefix, palette) {
1215
1215
  }
1216
1216
  }
1217
1217
  for (const c of s.children) {
1218
- parts.push(renderTrackChild(c, options, idPrefix, palette));
1218
+ parts.push(renderTrackChild(c, options, idPrefix, palette, fonts));
1219
1219
  }
1220
1220
  // m13: tri-state utilization underline along the band's bottom edge.
1221
1221
  // Painted after items so it overlays any item that happens to extend
@@ -1224,7 +1224,7 @@ function renderSwimlane(s, options, idPrefix, palette) {
1224
1224
  parts.push(renderLaneUtilization(s, palette));
1225
1225
  return tag('g', { 'data-layer': 'swimlane', 'data-id': s.id ?? null }, parts.join(''));
1226
1226
  }
1227
- function renderAnchor(a, palette) {
1227
+ function renderAnchor(a, palette, fonts) {
1228
1228
  const size = a.radius;
1229
1229
  const cx = a.center.x;
1230
1230
  const cy = a.center.y;
@@ -1249,7 +1249,7 @@ function renderAnchor(a, palette) {
1249
1249
  const labelAttrs = {
1250
1250
  x: num(labelX),
1251
1251
  y: num(cy + 4),
1252
- 'font-family': FONT_STACK.sans,
1252
+ 'font-family': fonts.sans,
1253
1253
  'font-size': 10,
1254
1254
  fill: labelColor,
1255
1255
  };
@@ -1270,7 +1270,7 @@ function renderAnchorCutLine(a, palette) {
1270
1270
  'stroke-dasharray': '1 3',
1271
1271
  });
1272
1272
  }
1273
- function renderMilestone(m, palette) {
1273
+ function renderMilestone(m, palette, fonts) {
1274
1274
  const cx = m.center.x;
1275
1275
  const cy = m.center.y;
1276
1276
  const r = m.radius;
@@ -1289,7 +1289,7 @@ function renderMilestone(m, palette) {
1289
1289
  const labelAttrs = {
1290
1290
  x: num(labelX),
1291
1291
  y: num(cy + 4),
1292
- 'font-family': FONT_STACK.sans,
1292
+ 'font-family': fonts.sans,
1293
1293
  'font-size': 10,
1294
1294
  'font-weight': 600,
1295
1295
  fill: labelColor,
@@ -1384,7 +1384,7 @@ function roundedOrthogonalPath(points, radius) {
1384
1384
  }
1385
1385
  return parts.join(' ');
1386
1386
  }
1387
- function renderFootnotes(f, idPrefix, palette) {
1387
+ function renderFootnotes(f, idPrefix, palette, fonts) {
1388
1388
  if (f.entries.length === 0)
1389
1389
  return '';
1390
1390
  const panelFill = palette.footnotePanel.fill;
@@ -1409,7 +1409,7 @@ function renderFootnotes(f, idPrefix, palette) {
1409
1409
  parts.push(textTag({
1410
1410
  x: num(f.box.x + FOOTNOTE_PANEL_PADDING_PX),
1411
1411
  y: num(f.box.y + FOOTNOTE_HEADER_BASELINE_OFFSET_PX),
1412
- 'font-family': FONT_STACK.sans,
1412
+ 'font-family': fonts.sans,
1413
1413
  'font-size': 12,
1414
1414
  'font-weight': 700,
1415
1415
  fill: headerColor,
@@ -1424,7 +1424,7 @@ function renderFootnotes(f, idPrefix, palette) {
1424
1424
  parts.push(textTag({
1425
1425
  x: num(numberX),
1426
1426
  y: num(y),
1427
- 'font-family': FONT_STACK.sans,
1427
+ 'font-family': fonts.sans,
1428
1428
  'font-size': 10,
1429
1429
  'font-weight': 700,
1430
1430
  fill: numberColor,
@@ -1432,7 +1432,7 @@ function renderFootnotes(f, idPrefix, palette) {
1432
1432
  parts.push(textTag({
1433
1433
  x: num(titleX),
1434
1434
  y: num(y),
1435
- 'font-family': FONT_STACK.sans,
1435
+ 'font-family': fonts.sans,
1436
1436
  'font-size': 11,
1437
1437
  'font-weight': 600,
1438
1438
  fill: titleColor,
@@ -1441,7 +1441,7 @@ function renderFootnotes(f, idPrefix, palette) {
1441
1441
  parts.push(textTag({
1442
1442
  x: num(titleX + Math.max(120, e.title.length * 6)),
1443
1443
  y: num(y),
1444
- 'font-family': FONT_STACK.sans,
1444
+ 'font-family': fonts.sans,
1445
1445
  'font-size': 11,
1446
1446
  fill: descColor,
1447
1447
  }, `— ${e.description}`));
@@ -1449,7 +1449,7 @@ function renderFootnotes(f, idPrefix, palette) {
1449
1449
  });
1450
1450
  return tag('g', { 'data-layer': 'footnotes' }, parts.join(''));
1451
1451
  }
1452
- function renderIncludeRegion(r, options, idPrefix, palette) {
1452
+ function renderIncludeRegion(r, options, idPrefix, palette, fonts) {
1453
1453
  const border = palette.includeRegion.border;
1454
1454
  const fill = palette.includeRegion.fill;
1455
1455
  const tabFill = palette.includeRegion.tabFill;
@@ -1495,7 +1495,7 @@ function renderIncludeRegion(r, options, idPrefix, palette) {
1495
1495
  const tabLabel = textTag({
1496
1496
  x: num(chrome.tabLabelX),
1497
1497
  y: num(tabY + FRAME_TAB_LABEL_BASELINE_OFFSET_PX),
1498
- 'font-family': FONT_STACK.sans,
1498
+ 'font-family': fonts.sans,
1499
1499
  'font-size': 11,
1500
1500
  'font-weight': 600,
1501
1501
  fill: tabText,
@@ -1548,13 +1548,13 @@ function renderIncludeRegion(r, options, idPrefix, palette) {
1548
1548
  const sourceText = textTag({
1549
1549
  x: num(chrome.sourceTextX),
1550
1550
  y: num(sourceTextY),
1551
- 'font-family': FONT_STACK.mono,
1551
+ 'font-family': fonts.mono,
1552
1552
  'font-size': sourceFontSize,
1553
1553
  fill: badgeText,
1554
1554
  }, r.sourcePath);
1555
1555
  // Nested swimlanes (laid out by buildIncludeRegions against the parent's timeline).
1556
1556
  const nested = r.nestedSwimlanes
1557
- .map((s) => renderSwimlane(s, options, idPrefix, palette))
1557
+ .map((s) => renderSwimlane(s, options, idPrefix, palette, fonts))
1558
1558
  .join('');
1559
1559
  return tag('g', { 'data-layer': 'include' }, region + nested + tab + tabLabel + badge + glyph + sourceHalo + sourceText);
1560
1560
  }
@@ -1564,7 +1564,7 @@ function renderIncludeRegion(r, options, idPrefix, palette) {
1564
1564
  // entire string is clickable. Glyph anatomy (positions, widths, scale)
1565
1565
  // lives in `themes/shared.ts` (`ATTRIBUTION_*`); the layout reserves a
1566
1566
  // box of exactly that size at canvas-bottom-right.
1567
- function renderAttributionMark(model) {
1567
+ function renderAttributionMark(model, fonts) {
1568
1568
  const muted = model.palette.attribution.mark;
1569
1569
  const accent = model.palette.attribution.link;
1570
1570
  if (model.swimlanes.length === 0)
@@ -1578,7 +1578,7 @@ function renderAttributionMark(model) {
1578
1578
  const inner = textTag({
1579
1579
  x: '0',
1580
1580
  y: baselineY,
1581
- 'font-family': FONT_STACK.sans,
1581
+ 'font-family': fonts.sans,
1582
1582
  'font-size': ATTRIBUTION_PREFIX_FONT_SIZE,
1583
1583
  'font-weight': 400,
1584
1584
  fill: muted,
@@ -1586,7 +1586,7 @@ function renderAttributionMark(model) {
1586
1586
  textTag({
1587
1587
  x: ATTRIBUTION_NOW_LOGICAL_X,
1588
1588
  y: baselineY,
1589
- 'font-family': FONT_STACK.sans,
1589
+ 'font-family': fonts.sans,
1590
1590
  'font-size': ATTRIBUTION_WORDMARK_FONT_SIZE,
1591
1591
  'font-weight': 700,
1592
1592
  fill: muted,
@@ -1601,7 +1601,7 @@ function renderAttributionMark(model) {
1601
1601
  textTag({
1602
1602
  x: ATTRIBUTION_INE_LOGICAL_X,
1603
1603
  y: baselineY,
1604
- 'font-family': FONT_STACK.sans,
1604
+ 'font-family': fonts.sans,
1605
1605
  'font-size': ATTRIBUTION_WORDMARK_FONT_SIZE,
1606
1606
  'font-weight': 400,
1607
1607
  fill: muted,
@@ -1672,6 +1672,10 @@ export async function renderSvg(model, options = {}) {
1672
1672
  const ids = new IdGenerator(options.idPrefix ?? 'nl');
1673
1673
  const idPrefix = ids.next('root');
1674
1674
  const palette = model.palette;
1675
+ // Per-role family strings stamped onto every <text>. Defaults to the
1676
+ // portable FONT_STACK; raster/preview callers pass a pinned bundled
1677
+ // family so the SVG names exactly the font the consumer provides.
1678
+ const fonts = options.fontFamilies ?? FONT_STACK;
1675
1679
  const parts = [];
1676
1680
  // <defs> — shadows + arrowhead markers (palette-driven fills baked in).
1677
1681
  const arrowFillNeutral = palette.arrowhead.neutral;
@@ -1697,7 +1701,7 @@ export async function renderSvg(model, options = {}) {
1697
1701
  // swimlane background rects emitted later — so the major dotted
1698
1702
  // and minor grid lines never actually rendered in the chart body.
1699
1703
  // They now ship as their own layer below.
1700
- parts.push(renderTimeline(model.timeline, palette));
1704
+ parts.push(renderTimeline(model.timeline, palette, fonts));
1701
1705
  // Swimlane backgrounds — emitted as their own pass so the grid
1702
1706
  // lines can be drawn on top of them, then the swimlane content
1703
1707
  // (frame tab + items) sits on top of the grid.
@@ -1722,11 +1726,11 @@ export async function renderSvg(model, options = {}) {
1722
1726
  }
1723
1727
  // Swimlane content (frame tabs + items) on top of the grid lines.
1724
1728
  for (const s of model.swimlanes)
1725
- parts.push(renderSwimlane(s, options, idPrefix, palette));
1729
+ parts.push(renderSwimlane(s, options, idPrefix, palette, fonts));
1726
1730
  // Include regions (drawn after own swimlanes so the dashed border + tab
1727
1731
  // overlay the chart, with their own nested swimlanes inside).
1728
1732
  for (const r of model.includes)
1729
- parts.push(renderIncludeRegion(r, options, idPrefix, palette));
1733
+ parts.push(renderIncludeRegion(r, options, idPrefix, palette, fonts));
1730
1734
  // Normal / overflow dependency edges on top of items but below
1731
1735
  // cut-lines / nowline. Under-bar edges already painted above.
1732
1736
  for (const e of model.edges) {
@@ -1741,15 +1745,15 @@ export async function renderSvg(model, options = {}) {
1741
1745
  parts.push(renderMilestoneCutLine(m, palette));
1742
1746
  // Marker-row diamonds + labels.
1743
1747
  for (const a of model.anchors)
1744
- parts.push(renderAnchor(a, palette));
1748
+ parts.push(renderAnchor(a, palette, fonts));
1745
1749
  for (const m of model.milestones)
1746
- parts.push(renderMilestone(m, palette));
1750
+ parts.push(renderMilestone(m, palette, fonts));
1747
1751
  // Now-line
1748
- parts.push(renderNowline(model.nowline, palette));
1752
+ parts.push(renderNowline(model.nowline, palette, fonts));
1749
1753
  // Footnotes + header last (always on top)
1750
- parts.push(renderFootnotes(model.footnotes, idPrefix, palette));
1751
- parts.push(renderHeader(model.header, idPrefix, palette));
1752
- parts.push(renderAttributionMark(model));
1754
+ parts.push(renderFootnotes(model.footnotes, idPrefix, palette, fonts));
1755
+ parts.push(renderHeader(model.header, idPrefix, palette, fonts));
1756
+ parts.push(renderAttributionMark(model, fonts));
1753
1757
  // Logo (if header carries one)
1754
1758
  if (model.header.logo && options.assetResolver) {
1755
1759
  const logoSvg = await embedLogo(model.header.logo.assetRef ?? '', options.assetResolver, idPrefix, options, model.header.logo.box.x, model.header.logo.box.y, Math.max(model.header.logo.box.width, model.header.logo.box.height));